From ab44b19cf747c348099c5aa86fcaf8bf0be628c9 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 28 Nov 2021 09:55:20 +0200 Subject: Cleanup --- src/prefs.h | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/prefs.h b/src/prefs.h index 2fbff9de..c9abfc8e 100644 --- a/src/prefs.h +++ b/src/prefs.h @@ -33,6 +33,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ iDeclareType(Prefs) enum iPrefsString { + /* General */ uiLanguage_PrefsString, downloadDir_PrefsString, searchUrl_PrefsString, @@ -56,25 +57,27 @@ enum iPrefsString { convention for notifications. */ struct Impl_Prefs { iString strings[max_PrefsString]; - /* UI state */ + /* UI state (belongs to state.lgr...) */ int dialogTab; int langFrom; int langTo; - /* Window */ + /* Colors */ iBool useSystemTheme; enum iColorTheme systemPreferredColorTheme[2]; /* dark, light */ enum iColorTheme theme; enum iColorAccent accent; + /* Window and User Interface */ iBool customFrame; /* when LAGRANGE_ENABLE_CUSTOM_FRAME is defined */ iBool retainWindowSize; iBool uiAnimations; float uiScale; + iBool hideToolbarOnScroll; + /* Document presentation */ int zoomPercent; iBool sideIcon; - iBool hideToolbarOnScroll; - int pinSplit; /* 0: no pinning, 1: left doc, 2: right doc */ iBool time24h; /* Behavior */ + int pinSplit; /* 0: no pinning, 1: left doc, 2: right doc */ int returnKey; iBool hoverLink; iBool smoothScrolling; -- cgit v1.2.3 From 1efe2aab4fdc811c1dd37c76d1f5d37063745649 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 28 Nov 2021 14:03:33 +0200 Subject: Bookmarks: Internal tags have a dot prefix Internal behavior tags are now written in bookmarks.ini with a dot prefix (like hidden files on Unix), and at runtime they are removed from the tags string. This makes things more efficient as it isn't necessary to compile regular expressions all the time. TODO: Add "Edit Feed..." into the Bookmarks context menu, and a new menu item for listing all subscriptions. IssueID #331 --- src/app.c | 4 +- src/bookmarks.c | 135 ++++++++++++++++++++++++++++++++++++++++-------- src/bookmarks.h | 77 ++++++++++++++------------- src/feeds.c | 12 ++--- src/ui/documentwidget.c | 4 +- src/ui/sidebarwidget.c | 85 +++++++++++++----------------- src/ui/util.c | 18 +++---- 7 files changed, 204 insertions(+), 131 deletions(-) diff --git a/src/app.c b/src/app.c index a2de03ca..0c6e38f1 100644 --- a/src/app.c +++ b/src/app.c @@ -2980,10 +2980,8 @@ iBool handleCommand_App(const char *cmd) { } else if (equal_Command(cmd, "navigate.home")) { /* Look for bookmarks tagged "homepage". */ - iRegExp *pattern = iClob(new_RegExp("\\b" homepage_BookmarkTag "\\b", - caseInsensitive_RegExpOption)); const iPtrArray *homepages = - list_Bookmarks(d->bookmarks, NULL, filterTagsRegExp_Bookmarks, pattern); + list_Bookmarks(d->bookmarks, NULL, filterHomepage_Bookmark, NULL); if (isEmpty_PtrArray(homepages)) { postCommand_Root(get_Root(), "open url:about:lagrange"); } diff --git a/src/bookmarks.c b/src/bookmarks.c index d4b20162..2606d975 100644 --- a/src/bookmarks.c +++ b/src/bookmarks.c @@ -37,6 +37,7 @@ void init_Bookmark(iBookmark *d) { init_String(&d->url); init_String(&d->title); init_String(&d->tags); + iZap(d->flags); iZap(d->when); d->parentId = 0; d->order = 0; @@ -48,6 +49,7 @@ void deinit_Bookmark(iBookmark *d) { deinit_String(&d->url); } +#if 0 iBool hasTag_Bookmark(const iBookmark *d, const char *tag) { if (!d) return iFalse; iRegExp *pattern = new_RegExp(format_CStr("\\b%s\\b", tag), caseSensitive_RegExpOption); @@ -60,7 +62,7 @@ iBool hasTag_Bookmark(const iBookmark *d, const char *tag) { void addTag_Bookmark(iBookmark *d, const char *tag) { if (!isEmpty_String(&d->tags)) { - appendChar_String(&d->tags, ' '); + appendCStr_String(&d->tags, " "); } appendCStr_String(&d->tags, tag); } @@ -72,6 +74,93 @@ void removeTag_Bookmark(iBookmark *d, const char *tag) { trim_String(&d->tags); } } +#endif + +static struct { + uint32_t bit; + const char *tag; + iRegExp * pattern; + iRegExp * oldPattern; +} +specialTags_[] = { + { homepage_BookmarkFlag, ".homepage" }, + { remoteSource_BookmarkFlag, ".remotesource" }, + { linkSplit_BookmarkFlag, ".linksplit" }, + { userIcon_BookmarkFlag, ".usericon" }, + { subscribed_BookmarkFlag, ".subscribed" }, + { headings_BookmarkFlag, ".headings" }, + { ignoreWeb_BookmarkFlag, ".ignoreweb" }, + /* `remote_BookmarkFlag` not included because it's runtime only */ +}; + +static void updatePatterns_(size_t index) { + if (!specialTags_[index].pattern) { + specialTags_[index].pattern = new_RegExp(format_CStr("(?flags); + iForIndices(i, specialTags_) { + updatePatterns_(i); + iRegExpMatch m; + init_RegExpMatch(&m); + iBool isSet = matchString_RegExp(specialTags_[i].pattern, &d->tags, &m); + if (!isSet) { + init_RegExpMatch(&m); + isSet = matchString_RegExp(specialTags_[i].oldPattern, &d->tags, &m); + } + iChangeFlags(d->flags, specialTags_[i].bit, isSet); + if (isSet) { + remove_Block(&d->tags.chars, m.range.start, size_Range(&m.range)); + } + } + normalizeSpacesInTags_(&d->tags); +} + +static iString *packedDotTags_Bookmark_(const iBookmark *d) { + iString *withDot = copy_String(&d->tags); + iForIndices(i, specialTags_) { + if (d->flags & specialTags_[i].bit) { + if (!isEmpty_String(withDot)) { + appendCStr_String(withDot, " "); + } + appendCStr_String(withDot, specialTags_[i].tag); + } + } + return withDot; +} iDefineTypeConstruction(Bookmark) @@ -176,6 +265,7 @@ static void loadOldFormat_Bookmarks(iBookmarks *d, const char *dirPath) { setRange_String(&bm->title, line); nextSplit_Rangecc(src, "\n", &line); setRange_String(&bm->tags, line); + unpackDotTags_Bookmark_(bm); insert_Bookmarks_(d, bm); } } @@ -220,6 +310,10 @@ static void handleKeyValue_BookmarkLoader_(void *context, const iString *table, } else if (!cmp_String(key, "tags") && tv->type == string_TomlType) { set_String(&bm->tags, tv->value.string); + if (strstr(cstr_String(&bm->tags), "subscribed")) { + printf("a\n"); + } + unpackDotTags_Bookmark_(bm); } else if (!cmp_String(key, "icon") && tv->type == int64_TomlType) { bm->icon = (iChar) tv->value.int64; @@ -292,7 +386,6 @@ void load_Bookmarks(iBookmarks *d, const char *dirPath) { void save_Bookmarks(const iBookmarks *d, const char *dirPath) { lock_Mutex(d->mtx); - iRegExp *remotePattern = iClob(new_RegExp("\\bremote\\b", caseSensitive_RegExpOption)); iFile *f = newCStr_File(concatPath_CStr(dirPath, fileName_Bookmarks_)); if (open_File(f, writeOnly_FileMode | text_FileMode)) { iString *str = collectNew_String(); @@ -300,13 +393,12 @@ void save_Bookmarks(const iBookmarks *d, const char *dirPath) { writeData_File(f, cstr_String(str), size_String(str)); iConstForEach(Hash, i, &d->bookmarks) { const iBookmark *bm = (const iBookmark *) i.value; - iRegExpMatch m; - init_RegExpMatch(&m); - if (matchString_RegExp(remotePattern, &bm->tags, &m)) { + if (bm->flags & remote_BookmarkFlag) { /* Remote bookmarks are not saved. */ continue; } iBeginCollect(); + const iString *packedTags = collect_String(packedDotTags_Bookmark_(bm)); format_String(str, "[%d]\n" "url = \"%s\"\n" @@ -317,7 +409,7 @@ void save_Bookmarks(const iBookmarks *d, const char *dirPath) { id_Bookmark(bm), cstrCollect_String(quote_String(&bm->url, iFalse)), cstrCollect_String(quote_String(&bm->title, iFalse)), - cstrCollect_String(quote_String(&bm->tags, iFalse)), + cstrCollect_String(quote_String(packedTags, iFalse)), bm->icon, seconds_Time(&bm->when), cstrCollect_String(format_Time(&bm->when, "%Y-%m-%d"))); @@ -397,7 +489,7 @@ iBool updateBookmarkIcon_Bookmarks(iBookmarks *d, const iString *url, iChar icon const uint32_t id = findUrl_Bookmarks(d, url); if (id) { iBookmark *bm = get_Bookmarks(d, id); - if (!hasTag_Bookmark(bm, remote_BookmarkTag) && !hasTag_Bookmark(bm, userIcon_BookmarkTag)) { + if (~bm->flags & remote_BookmarkFlag && ~bm->flags & userIcon_BookmarkFlag) { if (icon != bm->icon) { bm->icon = icon; changed = iTrue; @@ -422,19 +514,13 @@ iChar siteIcon_Bookmarks(const iBookmarks *d, const iString *url) { if (isEmpty_String(url)) { return 0; } - static iRegExp *tagPattern_; - if (!tagPattern_) { - tagPattern_ = new_RegExp("\\b" userIcon_BookmarkTag "\\b", caseSensitive_RegExpOption); - } const iRangecc urlRoot = urlRoot_String(url); size_t matchingSize = iInvalidSize; /* we'll pick the shortest matching */ iChar icon = 0; lock_Mutex(d->mtx); iConstForEach(Hash, i, &d->bookmarks) { const iBookmark *bm = (const iBookmark *) i.value; - iRegExpMatch m; - init_RegExpMatch(&m); - if (bm->icon && matchString_RegExp(tagPattern_, &bm->tags, &m)) { + if (bm->icon && bm->flags & userIcon_BookmarkFlag) { const iRangecc bmRoot = urlRoot_String(&bm->url); if (equalRangeCase_Rangecc(urlRoot, bmRoot)) { const size_t n = size_String(&bm->url); @@ -467,10 +553,15 @@ void reorder_Bookmarks(iBookmarks *d, uint32_t id, int newOrder) { unlock_Mutex(d->mtx); } -iBool filterTagsRegExp_Bookmarks(void *regExp, const iBookmark *bm) { - iRegExpMatch m; - init_RegExpMatch(&m); - return matchString_RegExp(regExp, &bm->tags, &m); +//iBool filterTagsRegExp_Bookmarks(void *regExp, const iBookmark *bm) { +// iRegExpMatch m; +// init_RegExpMatch(&m); +// return matchString_RegExp(regExp, &bm->tags, &m); +//} + +iBool filterHomepage_Bookmark(void *d, const iBookmark *bm) { + iUnused(d); + return (bm->flags & homepage_BookmarkFlag) != 0; } static iBool matchUrl_(void *url, const iBookmark *bm) { @@ -618,7 +709,7 @@ const iString *bookmarkListPage_Bookmarks(const iBookmarks *d, enum iBookmarkLis static iBool isRemoteSource_Bookmark_(void *context, const iBookmark *d) { iUnused(context); - return hasTag_Bookmark(d, remoteSource_BookmarkTag); + return (d->flags & remoteSource_BookmarkFlag) != 0; } void remoteRequestFinished_Bookmarks_(iBookmarks *d, iGmRequest *req) { @@ -642,7 +733,6 @@ void requestFinished_Bookmarks(iBookmarks *d, iGmRequest *req) { initCurrent_Time(&now); iRegExp *linkPattern = new_RegExp("^=>\\s*([^\\s]+)(\\s+(.*))?", 0); iString src; - const iString *remoteTag = collectNewCStr_String("remote"); initBlock_String(&src, body_GmRequest(req)); iRangecc srcLine = iNullRange; while (nextSplit_Rangecc(range_String(&src), "\n", &srcLine)) { @@ -660,8 +750,9 @@ void requestFinished_Bookmarks(iBookmarks *d, iGmRequest *req) { if (isEmpty_String(titleStr)) { setRange_String(titleStr, urlHost_String(urlStr)); } - const uint32_t bmId = add_Bookmarks(d, absUrl, titleStr, remoteTag, 0x2913); + const uint32_t bmId = add_Bookmarks(d, absUrl, titleStr, NULL, 0x2913); iBookmark *bm = get_Bookmarks(d, bmId); + bm->flags |= remote_BookmarkFlag; bm->parentId = *(uint32_t *) userData_Object(req); delete_String(titleStr); } @@ -690,7 +781,7 @@ void fetchRemote_Bookmarks(iBookmarks *d) { size_t numRemoved = 0; iForEach(Hash, i, &d->bookmarks) { iBookmark *bm = (iBookmark *) i.value; - if (hasTag_Bookmark(bm, remote_BookmarkTag)) { + if (bm->flags & remote_BookmarkFlag) { remove_HashIterator(&i); delete_Bookmark(bm); numRemoved++; diff --git a/src/bookmarks.h b/src/bookmarks.h index 6cb5c8a9..08afdd8b 100644 --- a/src/bookmarks.h +++ b/src/bookmarks.h @@ -31,27 +31,30 @@ iDeclareType(GmRequest) iDeclareType(Bookmark) iDeclareTypeConstruction(Bookmark) - -/* TODO: Make the special internal tags a bitfield, separate from user's tags. */ - -#define headings_BookmarkTag "headings" -#define ignoreWeb_BookmarkTag "ignoreweb" -#define homepage_BookmarkTag "homepage" -#define linkSplit_BookmarkTag "linksplit" -#define remote_BookmarkTag "remote" -#define remoteSource_BookmarkTag "remotesource" -#define subscribed_BookmarkTag "subscribed" -#define userIcon_BookmarkTag "usericon" + +/* These values are not serialized as-is in bookmarks.ini. Instead, they are included in `tags` + with a dot prefix. This helps retain backwards and forwards compatibility. */ +enum iBookmarkFlags { + homepage_BookmarkFlag = iBit(1), + remoteSource_BookmarkFlag = iBit(2), + linkSplit_BookmarkFlag = iBit(3), + userIcon_BookmarkFlag = iBit(4), + subscribed_BookmarkFlag = iBit(17), + headings_BookmarkFlag = iBit(18), + ignoreWeb_BookmarkFlag = iBit(19), + remote_BookmarkFlag = iBit(31), +}; struct Impl_Bookmark { iHashNode node; - iString url; - iString title; - iString tags; - iChar icon; - iTime when; - uint32_t parentId; /* remote source or folder */ - int order; /* sort order */ + iString url; + iString title; + iString tags; + uint32_t flags; + iChar icon; + iTime when; + uint32_t parentId; /* remote source or folder */ + int order; /* sort order */ }; iLocalDef uint32_t id_Bookmark (const iBookmark *d) { return d->node.key; } @@ -59,23 +62,24 @@ iLocalDef iBool isFolder_Bookmark (const iBookmark *d) { return isEmpty_St iBool hasParent_Bookmark (const iBookmark *, uint32_t parentId); int depth_Bookmark (const iBookmark *); -iBool hasTag_Bookmark (const iBookmark *, const char *tag); -void addTag_Bookmark (iBookmark *, const char *tag); -void removeTag_Bookmark (iBookmark *, const char *tag); - -iLocalDef void addTagIfMissing_Bookmark(iBookmark *d, const char *tag) { - if (!hasTag_Bookmark(d, tag)) { - addTag_Bookmark(d, tag); - } -} -iLocalDef void addOrRemoveTag_Bookmark(iBookmark *d, const char *tag, iBool add) { - if (add) { - addTagIfMissing_Bookmark(d, tag); - } - else { - removeTag_Bookmark(d, tag); - } -} + +//iBool hasTag_Bookmark (const iBookmark *, const char *tag); +//void addTag_Bookmark (iBookmark *, const char *tag); +//void removeTag_Bookmark (iBookmark *, const char *tag); + +//iLocalDef void addTagIfMissing_Bookmark(iBookmark *d, const char *tag) { +// if (!hasTag_Bookmark(d, tag)) { +// addTag_Bookmark(d, tag); +// } +//} +//iLocalDef void addOrRemoveTag_Bookmark(iBookmark *d, const char *tag, iBool add) { +// if (add) { +// addTagIfMissing_Bookmark(d, tag); +// } +// else { +// removeTag_Bookmark(d, tag); +// } +//} int cmpTitleAscending_Bookmark (const iBookmark **, const iBookmark **); int cmpTree_Bookmark (const iBookmark **, const iBookmark **); @@ -109,7 +113,8 @@ iChar siteIcon_Bookmarks (const iBookmarks *, const iString *url) uint32_t findUrl_Bookmarks (const iBookmarks *, const iString *url); /* O(n) */ uint32_t recentFolder_Bookmarks (const iBookmarks *); -iBool filterTagsRegExp_Bookmarks (void *regExp, const iBookmark *); +//iBool filterTagsRegExp_Bookmarks (void *regExp, const iBookmark *); +iBool filterHomepage_Bookmark (void *, const iBookmark *); /** * Lists all or a subset of the bookmarks in a sorted array of Bookmark pointers. diff --git a/src/feeds.c b/src/feeds.c index 26b3d6db..b7ce739b 100644 --- a/src/feeds.c +++ b/src/feeds.c @@ -97,8 +97,8 @@ static void init_FeedJob(iFeedJob *d, const iBookmark *bookmark) { init_PtrArray(&d->results); iZap(d->startTime); d->isFirstUpdate = iFalse; - d->checkHeadings = hasTag_Bookmark(bookmark, headings_BookmarkTag); - d->ignoreWeb = hasTag_Bookmark(bookmark, ignoreWeb_BookmarkTag); + d->checkHeadings = (bookmark->flags & headings_BookmarkFlag) != 0; + d->ignoreWeb = (bookmark->flags & ignoreWeb_BookmarkFlag) != 0; } static void deinit_FeedJob(iFeedJob *d) { @@ -146,13 +146,7 @@ static void submit_FeedJob_(iFeedJob *d) { static iBool isSubscribed_(void *context, const iBookmark *bm) { iUnused(context); - static iRegExp *pattern_ = NULL; - if (!pattern_) { - pattern_ = new_RegExp("\\bsubscribed\\b", caseSensitive_RegExpOption); - } - iRegExpMatch m; - init_RegExpMatch(&m); - return matchString_RegExp(pattern_, &bm->tags, &m); + return (bm->flags & subscribed_BookmarkFlag) != 0; } static const iPtrArray *listSubscriptions_(void) { diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index aac77572..39dd3aab 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -1108,7 +1108,7 @@ static void documentWasChanged_DocumentWidget_(iDocumentWidget *d) { const uint16_t bmid = findUrl_Bookmarks(bookmarks_App(), d->mod.url); if (bmid) { const iBookmark *bm = get_Bookmarks(bookmarks_App(), bmid); - if (hasTag_Bookmark(bm, linkSplit_BookmarkTag)) { + if (bm->flags & linkSplit_BookmarkFlag) { d->flags |= otherRootByDefault_DocumentWidgetFlag; } } @@ -3383,7 +3383,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) linkUrl_GmDocument(d->doc, run->linkId))) != 0) { const iBookmark *bm = get_Bookmarks(bookmarks_App(), bmid); /* We can import local copies of remote bookmarks. */ - if (!hasTag_Bookmark(bm, remote_BookmarkTag)) { + if (~bm->flags & remote_BookmarkFlag) { remove_PtrArrayIterator(&i); } } diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index 550fccde..b6aa06b1 100644 --- a/src/ui/sidebarwidget.c +++ b/src/ui/sidebarwidget.c @@ -407,11 +407,6 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) { break; } case bookmarks_SidebarMode: { - iRegExp *homeTag = iClob(new_RegExp("\\b" homepage_BookmarkTag "\\b", caseSensitive_RegExpOption)); - iRegExp *subTag = iClob(new_RegExp("\\b" subscribed_BookmarkTag "\\b", caseSensitive_RegExpOption)); - 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)) { const iBookmark *bm = i.ptr; if (isBookmarkFolded_SidebarWidget_(d, bm)) { @@ -430,27 +425,21 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) { } set_String(&item->url, &bm->url); set_String(&item->label, &bm->title); - /* Icons for special tags. */ { - iRegExpMatch m; - init_RegExpMatch(&m); - if (matchString_RegExp(subTag, &bm->tags, &m)) { + /* Icons for special behaviors. */ { + if (bm->flags & subscribed_BookmarkFlag) { appendChar_String(&item->meta, 0x2605); } - init_RegExpMatch(&m); - if (matchString_RegExp(homeTag, &bm->tags, &m)) { + if (bm->flags & homepage_BookmarkFlag) { appendChar_String(&item->meta, 0x1f3e0); } - init_RegExpMatch(&m); - if (matchString_RegExp(remoteTag, &bm->tags, &m)) { + if (bm->flags & remote_BookmarkFlag) { item->listItem.isDraggable = iFalse; } - init_RegExpMatch(&m); - if (matchString_RegExp(remoteSourceTag, &bm->tags, &m)) { + if (bm->flags & remoteSource_BookmarkFlag) { appendChar_String(&item->meta, 0x2913); item->isBold = iTrue; } - init_RegExpMatch(&m); - if (matchString_RegExp(linkSplitTag, &bm->tags, &m)) { + if (bm->flags & linkSplit_BookmarkFlag) { appendChar_String(&item->meta, 0x25e7); } } @@ -1042,19 +1031,16 @@ iBool handleBookmarkEditorCommands_SidebarWidget_(iWidget *editor, const char *c set_String(&bm->url, url); set_String(&bm->tags, tags); if (isEmpty_String(icon)) { - removeTag_Bookmark(bm, userIcon_BookmarkTag); + bm->flags &= ~userIcon_BookmarkFlag; bm->icon = 0; } else { - addTagIfMissing_Bookmark(bm, userIcon_BookmarkTag); + bm->flags |= userIcon_BookmarkFlag; bm->icon = first_String(icon); } - addOrRemoveTag_Bookmark(bm, homepage_BookmarkTag, - isSelected_Widget(findChild_Widget(editor, "bmed.tag.home"))); - addOrRemoveTag_Bookmark(bm, remoteSource_BookmarkTag, - isSelected_Widget(findChild_Widget(editor, "bmed.tag.remote"))); - addOrRemoveTag_Bookmark(bm, linkSplit_BookmarkTag, - isSelected_Widget(findChild_Widget(editor, "bmed.tag.linksplit"))); + iChangeFlags(bm->flags, homepage_BookmarkFlag, isSelected_Widget(findChild_Widget(editor, "bmed.tag.home"))); + iChangeFlags(bm->flags, remoteSource_BookmarkFlag, isSelected_Widget(findChild_Widget(editor, "bmed.tag.remote"))); + iChangeFlags(bm->flags, linkSplit_BookmarkFlag, isSelected_Widget(findChild_Widget(editor, "bmed.tag.linksplit"))); } const iBookmark *folder = userData_Object(findChild_Widget(editor, "bmed.folder")); if (!folder || !hasParent_Bookmark(folder, id_Bookmark(bm))) { @@ -1152,7 +1138,7 @@ static void bookmarkMoved_SidebarWidget_(iSidebarWidget *d, size_t index, size_t : dstIndex); if (isLast && isBefore) isBefore = iFalse; const iBookmark *dst = get_Bookmarks(bookmarks_App(), dstItem->id); - if (hasParent_Bookmark(dst, movingItem->id) || hasTag_Bookmark(dst, remote_BookmarkTag)) { + if (hasParent_Bookmark(dst, movingItem->id) || dst->flags & remote_BookmarkFlag) { /* Can't move a folder inside itself, and remote bookmarks cannot be reordered. */ return; } @@ -1176,7 +1162,8 @@ static void bookmarkMovedOntoFolder_SidebarWidget_(iSidebarWidget *d, size_t ind static size_t numBookmarks_(const iPtrArray *bmList) { size_t num = 0; iConstForEach(PtrArray, i, bmList) { - if (!isFolder_Bookmark(i.ptr) && !hasTag_Bookmark(i.ptr, remote_BookmarkTag)) { + const iBookmark *bm = i.ptr; + if (!isFolder_Bookmark(bm) && ~bm->flags & remote_BookmarkFlag) { num++; } } @@ -1349,13 +1336,13 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) if (!isFolder_Bookmark(bm)) { setText_InputWidget(urlInput, &bm->url); setText_InputWidget(tagsInput, &bm->tags); - if (hasTag_Bookmark(bm, userIcon_BookmarkTag)) { + if (bm->flags & userIcon_BookmarkFlag) { setText_InputWidget(iconInput, collect_String(newUnicodeN_String(&bm->icon, 1))); } - setToggle_Widget(homeTag, hasTag_Bookmark(bm, homepage_BookmarkTag)); - setToggle_Widget(remoteSourceTag, hasTag_Bookmark(bm, remoteSource_BookmarkTag)); - setToggle_Widget(linkSplitTag, hasTag_Bookmark(bm, linkSplit_BookmarkTag)); + setToggle_Widget(homeTag, bm->flags & homepage_BookmarkFlag); + setToggle_Widget(remoteSourceTag, bm->flags & remoteSource_BookmarkFlag); + setToggle_Widget(linkSplitTag, bm->flags & linkSplit_BookmarkFlag); } else { setFlags_Widget(findChild_Widget(dlg, "bmed.special"), @@ -1376,7 +1363,7 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) const iSidebarItem *item = d->contextItem; if (d->mode == bookmarks_SidebarMode && item) { iBookmark *bm = get_Bookmarks(bookmarks_App(), item->id); - const iBool isRemote = hasTag_Bookmark(bm, remote_BookmarkTag); + const iBool isRemote = (bm->flags & remote_BookmarkFlag) != 0; iChar icon = isRemote ? 0x1f588 : bm->icon; iWidget *dlg = makeBookmarkCreation_Widget(&bm->url, &bm->title, icon); setId_Widget(dlg, format_CStr("bmed.%s", cstr_String(id_Widget(w)))); @@ -1390,17 +1377,16 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) else if (isCommand_Widget(w, ev, "bookmark.tag")) { const iSidebarItem *item = d->contextItem; if (d->mode == bookmarks_SidebarMode && item) { - const char *tag = cstr_String(string_Command(cmd, "tag")); + const iRangecc tag = range_Command(cmd, "tag"); + const int flag = + (equal_Rangecc(tag, "homepage") ? homepage_BookmarkFlag : 0) | + (equal_Rangecc(tag, "subscribed") ? subscribed_BookmarkFlag : 0) | + (equal_Rangecc(tag, "remotesource") ? remoteSource_BookmarkFlag : 0); iBookmark *bm = get_Bookmarks(bookmarks_App(), item->id); - if (hasTag_Bookmark(bm, tag)) { - removeTag_Bookmark(bm, tag); - if (!iCmpStr(tag, subscribed_BookmarkTag)) { - removeEntries_Feeds(item->id); - } - } - else { - addTag_Bookmark(bm, tag); + if (flag == subscribed_BookmarkFlag && (bm->flags & flag)) { + removeEntries_Feeds(item->id); /* get rid of unsubscribed entries */ } + bm->flags ^= flag; postCommand_App("bookmarks.changed"); } return iTrue; @@ -1525,7 +1511,7 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) } if (isCommand_Widget(w, ev, "feed.entry.unsubscribe")) { if (arg_Command(cmd)) { - removeTag_Bookmark(feedBookmark, subscribed_BookmarkTag); + feedBookmark->flags &= ~subscribed_BookmarkFlag; removeEntries_Feeds(id_Bookmark(feedBookmark)); updateItems_SidebarWidget_(d); } @@ -1733,17 +1719,17 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) if (bm) { setMenuItemLabel_Widget(d->menu, "bookmark.tag tag:homepage", - hasTag_Bookmark(bm, homepage_BookmarkTag) + bm->flags & homepage_BookmarkFlag ? home_Icon " ${bookmark.untag.home}" : home_Icon " ${bookmark.tag.home}"); setMenuItemLabel_Widget(d->menu, "bookmark.tag tag:subscribed", - hasTag_Bookmark(bm, subscribed_BookmarkTag) + bm->flags & subscribed_BookmarkFlag ? star_Icon " ${bookmark.untag.sub}" : star_Icon " ${bookmark.tag.sub}"); setMenuItemLabel_Widget(d->menu, "bookmark.tag tag:remotesource", - hasTag_Bookmark(bm, remoteSource_BookmarkTag) + bm->flags & remoteSource_BookmarkFlag ? downArrowBar_Icon " ${bookmark.untag.remote}" : downArrowBar_Icon " ${bookmark.tag.remote}"); } @@ -1797,13 +1783,12 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) const iSidebarItem *hoverItem = hoverItem_ListWidget(d->list); iAssert(hoverItem); const iBookmark * bm = get_Bookmarks(bookmarks_App(), hoverItem->id); - const iBool isRemote = hasTag_Bookmark(bm, remote_BookmarkTag); + const iBool isRemote = (bm->flags & remote_BookmarkFlag) != 0; static const char *localOnlyCmds[] = { "bookmark.edit", "bookmark.delete", - "bookmark.tag tag:" subscribed_BookmarkTag, - "bookmark.tag tag:" homepage_BookmarkTag, - "bookmark.tag tag:" remoteSource_BookmarkTag, - "bookmark.tag tag:" subscribed_BookmarkTag }; + "bookmark.tag tag:subscribed", + "bookmark.tag tag:homepage", + "bookmark.tag tag:remotesource" }; iForIndices(i, localOnlyCmds) { setFlags_Widget(as_Widget(findMenuItem_Widget(d->menu, localOnlyCmds[i])), disabled_WidgetFlag, diff --git a/src/ui/util.c b/src/ui/util.c index baa05082..6f5eced3 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -2908,16 +2908,16 @@ static iBool handleBookmarkCreationCommands_SidebarWidget_(iWidget *editor, cons const uint32_t id = add_Bookmarks(bookmarks_App(), url, title, tags, first_String(icon)); iBookmark * bm = get_Bookmarks(bookmarks_App(), id); if (!isEmpty_String(icon)) { - addTagIfMissing_Bookmark(bm, userIcon_BookmarkTag); + bm->flags |= userIcon_BookmarkFlag; } if (isSelected_Widget(findChild_Widget(editor, "bmed.tag.home"))) { - addTag_Bookmark(bm, homepage_BookmarkTag); + bm->flags |= homepage_BookmarkFlag; } if (isSelected_Widget(findChild_Widget(editor, "bmed.tag.remote"))) { - addTag_Bookmark(bm, remoteSource_BookmarkTag); + bm->flags |= remoteSource_BookmarkFlag; } if (isSelected_Widget(findChild_Widget(editor, "bmed.tag.linksplit"))) { - addTag_Bookmark(bm, linkSplit_BookmarkTag); + bm->flags |= linkSplit_BookmarkFlag; } bm->parentId = folder ? id_Bookmark(folder) : 0; setRecentFolder_Bookmarks(bookmarks_App(), bm->parentId); @@ -2983,9 +2983,9 @@ static iBool handleFeedSettingCommands_(iWidget *dlg, const char *cmd) { iBookmark *bm = get_Bookmarks(bookmarks_App(), id); iAssert(bm); set_String(&bm->title, feedTitle); - addOrRemoveTag_Bookmark(bm, subscribed_BookmarkTag, iTrue); - addOrRemoveTag_Bookmark(bm, headings_BookmarkTag, headings); - addOrRemoveTag_Bookmark(bm, ignoreWeb_BookmarkTag, ignoreWeb); + bm->flags |= subscribed_BookmarkFlag; + iChangeFlags(bm->flags, headings_BookmarkFlag, headings); + iChangeFlags(bm->flags, ignoreWeb_BookmarkFlag, ignoreWeb); postCommand_App("bookmarks.changed"); setupSheetTransition_Mobile(dlg, iFalse); destroy_Widget(dlg); @@ -3048,13 +3048,13 @@ iWidget *makeFeedSettings_Widget(uint32_t bookmarkId) { setText_InputWidget(findChild_Widget(dlg, "feedcfg.title"), bm ? &bm->title : feedTitle_DocumentWidget(document_App())); setFlags_Widget(findChild_Widget(dlg, - hasTag_Bookmark(bm, headings_BookmarkTag) + bm->flags & headings_BookmarkFlag ? "feedcfg.type.headings" : "feedcfg.type.gemini"), selected_WidgetFlag, iTrue); setToggle_Widget(findChild_Widget(dlg, "feedcfg.ignoreweb"), - hasTag_Bookmark(bm, ignoreWeb_BookmarkTag)); + bm->flags & ignoreWeb_BookmarkFlag); setCommandHandler_Widget(dlg, handleFeedSettingCommands_); } setupSheetTransition_Mobile(dlg, incoming_TransitionFlag); -- cgit v1.2.3 From 63e9a36d64b75c7cbab17ccd861f8017b8a1e136 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 28 Nov 2021 18:58:02 +0200 Subject: Bookmarks: Crash when creating a bookmark IssueID #402 --- src/bookmarks.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bookmarks.c b/src/bookmarks.c index 2606d975..f733dac3 100644 --- a/src/bookmarks.c +++ b/src/bookmarks.c @@ -502,7 +502,7 @@ iBool updateBookmarkIcon_Bookmarks(iBookmarks *d, const iString *url, iChar icon void setRecentFolder_Bookmarks(iBookmarks *d, uint32_t folderId) { iBookmark *bm = get_Bookmarks(d, folderId); - if (isFolder_Bookmark(bm)) { + if (bm && isFolder_Bookmark(bm)) { d->recentFolderId = folderId; } else { -- cgit v1.2.3 From 5d6e591986be72dcda5e3fc5e8241f315346b01b Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 30 Nov 2021 13:10:52 +0200 Subject: Bumped version numbers to 1.10 --- CMakeLists.txt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5bf52a22..5c225c9b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,15 +18,15 @@ cmake_minimum_required (VERSION 3.9) project (Lagrange - VERSION 1.9.2 + VERSION 1.10.0 DESCRIPTION "A Beautiful Gemini Client" LANGUAGES C ) set (COPYRIGHT_YEAR 2021) if (IOS) - set (PROJECT_VERSION 1.7) - set (IOS_BUNDLE_VERSION 23) - set (IOS_BUILD_DATE "2021-10-23") + set (PROJECT_VERSION 1.10) + set (IOS_BUNDLE_VERSION 1) + set (IOS_BUILD_DATE "2021-12-05") endif () # Defaults that depend on environment. @@ -225,7 +225,7 @@ set (SOURCES res/about/version.gmi ${EMB_BIN} ) -if (NOT APPLE) # macos.m has Sparkle updater +if (IOS OR NOT APPLE) # macos.m has Sparkle updater list (APPEND SOURCES src/updater.c) endif () if (ENABLE_IPC) @@ -264,7 +264,7 @@ if (MSYS) configure_file (res/lagrange.rc.in ${CMAKE_CURRENT_BINARY_DIR}/lagrange.rc NEWLINE_STYLE WIN32) list (APPEND SOURCES src/win32.c src/win32.h ${CMAKE_CURRENT_BINARY_DIR}/lagrange.rc) endif () -set_source_files_properties (${RESOURCES} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) +#set_source_files_properties (${RESOURCES} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) if (MSYS OR (APPLE AND NOT MOBILE) OR (UNIX AND NOT MOBILE)) add_definitions (-DiPlatformPcDesktop=1) endif () -- cgit v1.2.3 From ef1065ac5ae60890d33db772396cb3165ecfa952 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 30 Nov 2021 13:11:40 +0200 Subject: iOS: Use libwebpdecoder in the build --- Depends-iOS.cmake | 2 ++ Depends.cmake | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Depends-iOS.cmake b/Depends-iOS.cmake index 96a2f584..912251ca 100644 --- a/Depends-iOS.cmake +++ b/Depends-iOS.cmake @@ -22,6 +22,8 @@ set (SDL2_LDFLAGS "-framework UIKit" ) +pkg_check_modules (WEBP IMPORTED_TARGET libwebpdecoder) + set (FRIBIDI_FOUND YES) set (FRIBIDI_LIBRARIES ${IOS_DIR}/lib/libfribidi.a) diff --git a/Depends.cmake b/Depends.cmake index ca01b6c5..10e434e7 100644 --- a/Depends.cmake +++ b/Depends.cmake @@ -1,9 +1,10 @@ +find_package (PkgConfig) + if (IOS) include (Depends-iOS.cmake) return () endif () -find_package (PkgConfig) find_program (MESON_EXECUTABLE meson DOC "Meson build system") find_program (NINJA_EXECUTABLE ninja DOC "Ninja build tool") include (ExternalProject) -- cgit v1.2.3 From 147b7e1e379a07f7e0a64e805ad1137c93043784 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 30 Nov 2021 13:12:12 +0200 Subject: iOS: Cleanup Skip the font download suggestion, and fixed an uninitialized value. --- src/app.c | 2 ++ src/ui/text.c | 1 + 2 files changed, 3 insertions(+) diff --git a/src/app.c b/src/app.c index 0c6e38f1..9f4d60e3 100644 --- a/src/app.c +++ b/src/app.c @@ -401,11 +401,13 @@ static void loadPrefs_App_(iApp *d) { iRelease(f); /* Upgrade checks. */ if (cmp_Version(&upgradedFromAppVersion, &(iVersion){ 1, 8, 0 }) < 0) { +#if !defined (iPlatformAppleMobile) /* When upgrading to v1.8.0, the old hardcoded font library is gone and that means UI strings may not have the right fonts available for the UI to remain usable. */ postCommandf_App("uilang id:en"); postCommand_App("~fontpack.suggest.classic"); +#endif } #if !defined (LAGRANGE_ENABLE_CUSTOM_FRAME) d->prefs.customFrame = iFalse; diff --git a/src/ui/text.c b/src/ui/text.c index 977cac9c..76a1ee19 100644 --- a/src/ui/text.c +++ b/src/ui/text.c @@ -398,6 +398,7 @@ void init_Text(iText *d, SDL_Renderer *render) { d->ansiEscape = new_RegExp("[[()][?]?([0-9;AB]*?)([ABCDEFGHJKSTfhilmn])", 0); d->baseFontId = -1; d->baseFgColorId = -1; + d->missingGlyphs = iFalse; d->render = render; /* A grayscale palette for rasterized glyphs. */ { SDL_Color colors[256]; -- cgit v1.2.3 From 3dde6704217d0bc2b0f59991f25ebbf8c22d3dbe Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 30 Nov 2021 13:37:28 +0200 Subject: SidebarWidget: Icons for the Unread/All filter menu --- src/ui/sidebarwidget.c | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index 1aeb4163..8fa36291 100644 --- a/src/ui/sidebarwidget.c +++ b/src/ui/sidebarwidget.c @@ -356,19 +356,21 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) { iClob(new_LabelWidget("${sidebar.action.show}", NULL)), frameless_WidgetFlag | tight_WidgetFlag)); const iMenuItem items[] = { - { "${sidebar.action.feeds.showall}", SDLK_u, KMOD_SHIFT, "feeds.mode arg:0" }, - { "${sidebar.action.feeds.showunread}", SDLK_u, 0, "feeds.mode arg:1" }, + { page_Icon " ${sidebar.action.feeds.showall}", SDLK_u, KMOD_SHIFT, "feeds.mode arg:0" }, + { circle_Icon " ${sidebar.action.feeds.showunread}", SDLK_u, 0, "feeds.mode arg:1" }, }; iWidget *dropButton = addChild_Widget( d->actions, iClob(makeMenuButton_LabelWidget(items[d->feedsMode].label, items, 2))); + checkIcon_LabelWidget((iLabelWidget *) dropButton); setFixedSize_Widget( dropButton, - init_I2(iMaxi(20 * gap_UI, measure_Text( - default_FontId, - translateCStr_Lang(items[findWidestLabel_MenuItem(items, 2)].label)) - .advance.x + - 6 * gap_UI), + init_I2(iMaxi(20 * gap_UI, + measure_Text(default_FontId, + translateCStr_Lang( + items[findWidestLabel_MenuItem(items, 2)].label)) + .advance.x + + 13 * gap_UI), -1)); } d->menu = makeMenu_Widget( -- cgit v1.2.3 From dd1291a8d1ed53db9a3948de966b2d892ecad5b6 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 30 Nov 2021 15:30:51 +0200 Subject: Mobile: No Downloads menu item --- src/ui/root.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui/root.c b/src/ui/root.c index cf13169d..c2161d80 100644 --- a/src/ui/root.c +++ b/src/ui/root.c @@ -101,7 +101,7 @@ static const iMenuItem tabletNavMenuItems_[] = { { book_Icon " ${menu.bookmarks.list}", 0, 0, "!open url:about:bookmarks" }, { "${menu.bookmarks.bytag}", 0, 0, "!open url:about:bookmarks?tags" }, { "${menu.feeds.entrylist}", 0, 0, "!open url:about:feeds" }, - { "${menu.downloads}", 0, 0, "downloads.open" }, + //{ "${menu.downloads}", 0, 0, "downloads.open" }, { "---" }, { gear_Icon " ${menu.preferences}", SDLK_COMMA, KMOD_PRIMARY, "preferences" }, { "${menu.help}", SDLK_F1, 0, "!open url:about:help" }, @@ -118,7 +118,7 @@ static const iMenuItem phoneNavMenuItems_[] = { { leftHalf_Icon " ${menu.sidebar}", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" }, { "---" }, { book_Icon " ${menu.bookmarks.list}", 0, 0, "!open url:about:bookmarks" }, - { "${menu.downloads}", 0, 0, "downloads.open" }, + //{ "${menu.downloads}", 0, 0, "downloads.open" }, { "${menu.feeds.entrylist}", 0, 0, "!open url:about:feeds" }, { "---" }, { gear_Icon " ${menu.settings}", SDLK_COMMA, KMOD_PRIMARY, "preferences" }, -- cgit v1.2.3 From 2e9f8b247472bc6be3963ee7aad223f895085185 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 30 Nov 2021 15:34:38 +0200 Subject: SidebarWidget: Crash when switching Feeds mode Updating items immediately destroys the action widgets. --- src/ui/sidebarwidget.c | 24 ++++++++++++++++++------ src/ui/util.c | 7 +++++++ src/ui/widget.c | 2 +- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index 8fa36291..9cfc507a 100644 --- a/src/ui/sidebarwidget.c +++ b/src/ui/sidebarwidget.c @@ -264,10 +264,12 @@ static iBool isBookmarkFolded_SidebarWidget_(const iSidebarWidget *d, const iBoo return iFalse; } -static void updateItems_SidebarWidget_(iSidebarWidget *d) { +static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepActions) { clear_ListWidget(d->list); releaseChildren_Widget(d->blank); - releaseChildren_Widget(d->actions); + if (!keepActions) { + releaseChildren_Widget(d->actions); + } d->actions->rect.size.y = 0; destroy_Widget(d->menu); destroy_Widget(d->modeMenu); @@ -348,7 +350,8 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) { break; } } - /* Actions. */ { + /* Actions. */ + if (!keepActions) { addActionButton_SidebarWidget_( d, check_Icon " ${sidebar.action.feeds.markallread}", "feeds.markallread", expand_WidgetFlag | tight_WidgetFlag); @@ -362,6 +365,7 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) { iWidget *dropButton = addChild_Widget( d->actions, iClob(makeMenuButton_LabelWidget(items[d->feedsMode].label, items, 2))); + setId_Widget(dropButton, "feeds.modebutton"); checkIcon_LabelWidget((iLabelWidget *) dropButton); setFixedSize_Widget( dropButton, @@ -373,6 +377,10 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) { 13 * gap_UI), -1)); } + else { + updateDropdownSelection_LabelWidget(findChild_Widget(d->actions, "feeds.modebutton"), + format_CStr(" arg:%d", d->feedsMode)); + } d->menu = makeMenu_Widget( as_Widget(d), (iMenuItem[]){ { openTab_Icon " ${feeds.entry.newtab}", 0, 0, "feed.entry.opentab" }, @@ -644,6 +652,10 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) { updateMouseHover_ListWidget(d->list); } +static void updateItems_SidebarWidget_(iSidebarWidget *d) { + updateItemsWithFlags_SidebarWidget_(d, iFalse); +} + static size_t findItem_SidebarWidget_(const iSidebarWidget *d, int id) { /* Note that this is O(n), so only meant for infrequent use. */ for (size_t i = 0; i < numItems_ListWidget(d->list); i++) { @@ -1304,9 +1316,9 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) } return iTrue; } - else if (isCommand_Widget(w, ev, "menu.closed")) { +// else if (isCommand_Widget(w, ev, "menu.closed")) { // invalidateItem_ListWidget(d->list, d->contextIndex); - } +// } else if (isCommand_Widget(w, ev, "bookmark.open")) { const iSidebarItem *item = d->contextItem; if (d->mode == bookmarks_SidebarMode && item) { @@ -1462,7 +1474,7 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) } else if (equalWidget_Command(cmd, w, "feeds.mode")) { d->feedsMode = arg_Command(cmd); - updateItems_SidebarWidget_(d); + updateItemsWithFlags_SidebarWidget_(d, iTrue); return iTrue; } else if (equal_Command(cmd, "feeds.markallread") && d->mode == feeds_SidebarMode) { diff --git a/src/ui/util.c b/src/ui/util.c index 50294068..0e792fa3 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -1269,6 +1269,9 @@ const iString *removeMenuItemLabelPrefixes_String(const iString *d) { } void updateDropdownSelection_LabelWidget(iLabelWidget *dropButton, const char *selectedCommand) { + if (!dropButton) { + return; + } iWidget *menu = findChild_Widget(as_Widget(dropButton), "menu"); if (flags_Widget(menu) & nativeMenu_WidgetFlag) { unselectAllNativeMenuItems_Widget(menu); @@ -1277,6 +1280,7 @@ void updateDropdownSelection_LabelWidget(iLabelWidget *dropButton, const char *s setSelected_NativeMenuItem(item, iTrue); updateText_LabelWidget( dropButton, removeMenuItemLabelPrefixes_String(collectNewCStr_String(item->label))); + checkIcon_LabelWidget(dropButton); } return; } @@ -1287,6 +1291,7 @@ void updateDropdownSelection_LabelWidget(iLabelWidget *dropButton, const char *s setFlags_Widget(as_Widget(item), selected_WidgetFlag, isSelected); if (isSelected) { updateText_LabelWidget(dropButton, sourceText_LabelWidget(item)); + checkIcon_LabelWidget(dropButton); } } } @@ -2352,6 +2357,7 @@ iWidget *makePreferences_Widget(void) { { "radio device:1 id:prefs.pinsplit", 0, 0, (const void *) pinSplitItems }, { "padding" }, { "dropdown id:prefs.uilang", 0, 0, (const void *) langItems }, + { "toggle id:prefs.time.24h" }, { NULL } }; const iMenuItem uiPanelItems[] = { @@ -2436,6 +2442,7 @@ iWidget *makePreferences_Widget(void) { const iMenuItem aboutPanelItems[] = { { format_CStr("heading text:%s", cstr_String(aboutText)) }, { "button text:" clock_Icon " ${menu.releasenotes}", 0, 0, "!open url:about:version" }, + { "padding" }, { "button text:" globe_Icon " ${menu.website}", 0, 0, "!open url:https://gmi.skyjake.fi/lagrange" }, { "button text:" envelope_Icon " @jk@skyjake.fi", 0, 0, "!open url:https://skyjake.fi/@jk" }, { "padding" }, diff --git a/src/ui/widget.c b/src/ui/widget.c index a171a6cd..cedda461 100644 --- a/src/ui/widget.c +++ b/src/ui/widget.c @@ -1528,7 +1528,7 @@ static void addToPotentiallyVisible_Widget_(const iWidget *d, iPtrArray *pvs, iR } if (isFullyContainedByOther_Rect(bounds, *fullyMasked)) { return; /* can't be seen */ - } + } pushBack_PtrArray(pvs, d); if (d->bgColor >= 0 && ~d->flags & noBackground_WidgetFlag && isFullyContainedByOther_Rect(*fullyMasked, bounds)) { -- cgit v1.2.3 From 66bce4868f4fcd25867b0f0b0a9b9a88bb89f16d Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 30 Nov 2021 16:39:24 +0200 Subject: Mobile: Fixed scrolling of long popup menus --- src/ui/util.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ui/util.c b/src/ui/util.c index 0e792fa3..695e43aa 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -1036,7 +1036,8 @@ void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, int menuOpenFlags) { setFlags_Widget(findChild_Widget(d, "menu.cancel"), disabled_WidgetFlag, iFalse); arrange_Widget(d); /* need to know the height */ iBool allowOverflow = iFalse; - /* A vertical offset determined by a possible selected label in the menu. */ { + /* A vertical offset determined by a possible selected label in the menu. */ + if (deviceType_App() == desktop_AppDeviceType) { iConstForEach(ObjectList, child, children_Widget(d)) { const iWidget *item = constAs_Widget(child.object); if (flags_Widget(item) & selected_WidgetFlag) { -- cgit v1.2.3 From b3550138da3a669999c06da41720f2be664d5e86 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Wed, 1 Dec 2021 13:31:24 +0200 Subject: Cleanup --- src/gmutil.c | 2 +- src/ui/color.c | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/gmutil.c b/src/gmutil.c index 70a3608e..643fbea7 100644 --- a/src/gmutil.c +++ b/src/gmutil.c @@ -651,7 +651,7 @@ const iString *withSpacesEncoded_String(const iString *d) { const iString *canonicalUrl_String(const iString *d) { /* The "canonical" form, used for internal storage and comparisons, is: - all non-reserved characters decoded (i.e., it's an IRI) - - expect for spaces, which are always `%20` + - except spaces, which are always `%20` This means a canonical URL can be used on a gemtext link line without modifications. */ iString *canon = NULL; iUrl parts; diff --git a/src/ui/color.c b/src/ui/color.c index 3ea98d8c..ed17f580 100644 --- a/src/ui/color.c +++ b/src/ui/color.c @@ -90,8 +90,8 @@ void setThemePalette_Color(enum iColorTheme theme) { const int accentLo = (prefs->accent == cyan_ColorAccent ? teal_ColorId : brown_ColorId); const int altAccentHi = (prefs->accent == cyan_ColorAccent ? orange_ColorId : cyan_ColorId); const int altAccentLo = (prefs->accent == cyan_ColorAccent ? brown_ColorId : teal_ColorId); - const iColor accentMid = mix_Color(get_Color(accentHi), get_Color(accentLo), 0.5f); - const iColor altAccentMid = mix_Color(get_Color(altAccentHi), get_Color(altAccentLo), 0.5f); + //const iColor accentMid = mix_Color(get_Color(accentHi), get_Color(accentLo), 0.5f); + //const iColor altAccentMid = mix_Color(get_Color(altAccentHi), get_Color(altAccentLo), 0.5f); switch (theme) { case pureBlack_ColorTheme: { copy_(uiBackground_ColorId, black_ColorId); -- cgit v1.2.3 From f4942e1b4da6dc1334dcdb4f2daae670bfa1f813 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Wed, 1 Dec 2021 13:55:11 +0200 Subject: Added switching to the identity toolbar menu One can now use the identity toolbar menu to switch between client certificates that have been used on the current site. The five latest ones are remembered. --- po/en.po | 9 +++++ res/about/version.gmi | 5 ++- res/lang/cs.bin | Bin 30899 -> 30962 bytes res/lang/de.bin | Bin 29906 -> 29969 bytes res/lang/en.bin | Bin 26003 -> 26066 bytes res/lang/eo.bin | Bin 24957 -> 25020 bytes res/lang/es.bin | Bin 29730 -> 29793 bytes res/lang/es_MX.bin | Bin 27062 -> 27125 bytes res/lang/fi.bin | Bin 29563 -> 29626 bytes res/lang/fr.bin | Bin 30710 -> 30773 bytes res/lang/gl.bin | Bin 28915 -> 28978 bytes res/lang/hu.bin | Bin 30735 -> 30798 bytes res/lang/ia.bin | Bin 28062 -> 28125 bytes res/lang/ie.bin | Bin 28650 -> 28713 bytes res/lang/isv.bin | Bin 24723 -> 24786 bytes res/lang/pl.bin | Bin 29338 -> 29401 bytes res/lang/ru.bin | Bin 44098 -> 44161 bytes res/lang/sk.bin | Bin 25059 -> 25122 bytes res/lang/sr.bin | Bin 43524 -> 43587 bytes res/lang/tok.bin | Bin 26772 -> 26835 bytes res/lang/tr.bin | Bin 28956 -> 29019 bytes res/lang/uk.bin | Bin 43443 -> 43506 bytes res/lang/zh_Hans.bin | Bin 24957 -> 25020 bytes res/lang/zh_Hant.bin | Bin 25155 -> 25218 bytes src/app.c | 19 +++++++++++ src/gmcerts.c | 9 +++++ src/gmcerts.h | 5 +-- src/sitespec.c | 81 ++++++++++++++++++++++++++++++++++++++++----- src/sitespec.h | 20 +++++++----- src/ui/root.c | 89 +++++++++++++++++++++++++++++++++++++++++++++++--- 30 files changed, 214 insertions(+), 23 deletions(-) diff --git a/po/en.po b/po/en.po index 066c4eca..9a154c5c 100644 --- a/po/en.po +++ b/po/en.po @@ -321,6 +321,10 @@ msgstr "Show History" msgid "menu.show.identities" msgstr "Show Identities" +# Active identity toolbar menu. +msgid "menu.hide.identities" +msgstr "Hide Identities" +"" # Used in the View menu on macOS. Shows sidebar and switches sidebar tab. msgid "menu.show.outline" msgstr "Show Page Outline" @@ -629,6 +633,11 @@ msgstr "Stop Using Everywhere" msgid "ident.export" msgstr "Export" +# The %s represents the name of an identity. +#, c-format +msgid "ident.switch" +msgstr "Switch to %s" + msgid "heading.ident.use" msgstr "IDENTITY USAGE" diff --git a/res/about/version.gmi b/res/about/version.gmi index a9043b2c..7a14e79b 100644 --- a/res/about/version.gmi +++ b/res/about/version.gmi @@ -6,6 +6,10 @@ ``` # Release notes +## 1.10 +New features: +* Identity toolbar menu can be used to switch between alternate identities that have been used on the site. If you have multiple accounts on a site, this makes it more convenient to switch between them. + ## 1.9.2 * Windows: Use the correct version number for update checks. * Shorter label for "Mark All as Read" in Feeds sidebar actions. @@ -22,7 +26,6 @@ * Fixed the New Tab button not staying at the right edge of the window, depending on how many tabs are open. ## 1.9 - New features: * Added a toolbar button for toggling the left sidebar. * Added an unsplit button in the toolbar when in split view mode. diff --git a/res/lang/cs.bin b/res/lang/cs.bin index 647e1f34..d3e06c73 100644 Binary files a/res/lang/cs.bin and b/res/lang/cs.bin differ diff --git a/res/lang/de.bin b/res/lang/de.bin index b2bb35a0..b9d155a7 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 fbf4c73c..5bb8299a 100644 Binary files a/res/lang/en.bin and b/res/lang/en.bin differ diff --git a/res/lang/eo.bin b/res/lang/eo.bin index 7156b7c8..ff1ddb01 100644 Binary files a/res/lang/eo.bin and b/res/lang/eo.bin differ diff --git a/res/lang/es.bin b/res/lang/es.bin index 1acb50d1..ba9b2343 100644 Binary files a/res/lang/es.bin and b/res/lang/es.bin differ diff --git a/res/lang/es_MX.bin b/res/lang/es_MX.bin index 078de89d..f4ddcc72 100644 Binary files a/res/lang/es_MX.bin and b/res/lang/es_MX.bin differ diff --git a/res/lang/fi.bin b/res/lang/fi.bin index ccd3e133..f73891d6 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 96c1148e..0851f535 100644 Binary files a/res/lang/fr.bin and b/res/lang/fr.bin differ diff --git a/res/lang/gl.bin b/res/lang/gl.bin index 4269b5ed..80215589 100644 Binary files a/res/lang/gl.bin and b/res/lang/gl.bin differ diff --git a/res/lang/hu.bin b/res/lang/hu.bin index 7b7edb50..7d7ed94e 100644 Binary files a/res/lang/hu.bin and b/res/lang/hu.bin differ diff --git a/res/lang/ia.bin b/res/lang/ia.bin index 4750b545..572015c2 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 e4c90ef9..bb485f4e 100644 Binary files a/res/lang/ie.bin and b/res/lang/ie.bin differ diff --git a/res/lang/isv.bin b/res/lang/isv.bin index 80754fc5..f90a1e7d 100644 Binary files a/res/lang/isv.bin and b/res/lang/isv.bin differ diff --git a/res/lang/pl.bin b/res/lang/pl.bin index c0affedf..753e595b 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 966deaea..edcebb14 100644 Binary files a/res/lang/ru.bin and b/res/lang/ru.bin differ diff --git a/res/lang/sk.bin b/res/lang/sk.bin index deda3b69..b843a383 100644 Binary files a/res/lang/sk.bin and b/res/lang/sk.bin differ diff --git a/res/lang/sr.bin b/res/lang/sr.bin index 8d8591e6..1ef302d9 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 1b0c0733..6e3c7af7 100644 Binary files a/res/lang/tok.bin and b/res/lang/tok.bin differ diff --git a/res/lang/tr.bin b/res/lang/tr.bin index 60ef518d..71b9382f 100644 Binary files a/res/lang/tr.bin and b/res/lang/tr.bin differ diff --git a/res/lang/uk.bin b/res/lang/uk.bin index 2ed16909..f7040f2f 100644 Binary files a/res/lang/uk.bin and b/res/lang/uk.bin differ diff --git a/res/lang/zh_Hans.bin b/res/lang/zh_Hans.bin index 3a83dd40..3ccab576 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 7da4b273..39393417 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 9f4d60e3..540c46d8 100644 --- a/src/app.c +++ b/src/app.c @@ -3149,6 +3149,25 @@ iBool handleCommand_App(const char *cmd) { postCommand_App("idents.changed"); return iTrue; } + else if (equal_Command(cmd, "ident.switch")) { + /* This is different than "ident.signin" in that the currently used identity's activation + URL is used instead of the current one. */ + const iString *docUrl = url_DocumentWidget(document_App()); + const iGmIdentity *cur = identityForUrl_GmCerts(d->certs, docUrl); + iGmIdentity *dst = findIdentity_GmCerts( + d->certs, collect_Block(hexDecode_Rangecc(range_Command(cmd, "fp")))); + if (cur && dst && cur != dst) { + iString *useUrl = copy_String(findUse_GmIdentity(cur, docUrl)); + if (isEmpty_String(useUrl)) { + useUrl = copy_String(docUrl); + } + signIn_GmCerts(d->certs, dst, useUrl); + postCommand_App("idents.changed"); + postCommand_App("navigate.reload"); + delete_String(useUrl); + } + return iTrue; + } else if (equal_Command(cmd, "idents.changed")) { saveIdentities_GmCerts(d->certs); return iFalse; diff --git a/src/gmcerts.c b/src/gmcerts.c index f95fea7d..345c36e0 100644 --- a/src/gmcerts.c +++ b/src/gmcerts.c @@ -201,6 +201,15 @@ void clearUse_GmIdentity(iGmIdentity *d) { clear_StringSet(d->useUrls); } +const iString *findUse_GmIdentity(const iGmIdentity *d, const iString *url) { + iConstForEach(StringSet, using, d->useUrls) { + if (startsWith_String(url, cstr_String(using.value))) { + return using.value; + } + } + return NULL; +} + const iString *name_GmIdentity(const iGmIdentity *d) { iString *name = collect_String(subject_TlsCertificate(d->cert)); if (startsWith_String(name, "CN = ")) { diff --git a/src/gmcerts.h b/src/gmcerts.h index 02a41c14..6ece1954 100644 --- a/src/gmcerts.h +++ b/src/gmcerts.h @@ -48,8 +48,9 @@ iBool isUsed_GmIdentity (const iGmIdentity *); iBool isUsedOn_GmIdentity (const iGmIdentity *, const iString *url); iBool isUsedOnDomain_GmIdentity (const iGmIdentity *, const iRangecc domain); -void setUse_GmIdentity (iGmIdentity *, const iString *url, iBool use); -void clearUse_GmIdentity (iGmIdentity *); +void setUse_GmIdentity (iGmIdentity *, const iString *url, iBool use); +void clearUse_GmIdentity (iGmIdentity *); +const iString *findUse_GmIdentity (const iGmIdentity *, const iString *url); const iString *name_GmIdentity(const iGmIdentity *); diff --git a/src/sitespec.c b/src/sitespec.c index 6f4546f0..f8b77c86 100644 --- a/src/sitespec.c +++ b/src/sitespec.c @@ -25,6 +25,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include #include #include +#include #include iDeclareClass(SiteParams) @@ -35,6 +36,7 @@ struct Impl_SiteParams { uint16_t titanPort; iString titanIdentity; /* fingerprint */ int dismissWarnings; + iStringArray usedIdentities; /* fingerprints; latest ones at the end */ /* TODO: theme seed, style settings */ }; @@ -42,12 +44,23 @@ void init_SiteParams(iSiteParams *d) { d->titanPort = 0; /* undefined */ init_String(&d->titanIdentity); d->dismissWarnings = 0; + init_StringArray(&d->usedIdentities); } void deinit_SiteParams(iSiteParams *d) { + deinit_StringArray(&d->usedIdentities); deinit_String(&d->titanIdentity); } +static size_t findUsedIdentity_SiteParams_(const iSiteParams *d, const iString *fingerprint) { + iConstForEach(StringArray, i, &d->usedIdentities) { + if (equal_String(i.value, fingerprint)) { + return index_StringArrayConstIterator(&i); + } + } + return iInvalidPos; +} + iDefineClass(SiteParams) iDefineObjectConstruction(SiteParams) @@ -130,6 +143,12 @@ static void handleIniKeyValue_SiteSpec_(void *context, const iString *table, con else if (!cmp_String(key, "dismissWarnings") && value->type == int64_TomlType) { d->loadParams->dismissWarnings = value->value.int64; } + else if (!cmp_String(key, "usedIdentities") && value->type == string_TomlType) { + iRangecc seg = iNullRange; + while (nextSplit_Rangecc(range_String(value->value.string), " ", &seg)) { + pushBack_StringArray(&d->loadParams->usedIdentities, collectNewRange_String(seg)); + } + } } static iBool load_SiteSpec_(iSiteSpec *d) { @@ -151,6 +170,7 @@ static void save_SiteSpec_(iSiteSpec *d) { if (open_File(f, writeOnly_FileMode | text_FileMode)) { iString *buf = new_String(); iConstForEach(StringHash, i, &d->sites) { + iBeginCollect(); const iBlock * key = &i.value->keyBlock; const iSiteParams *params = i.value->object; format_String(buf, "[%s]\n", cstr_Block(key)); @@ -164,8 +184,15 @@ static void save_SiteSpec_(iSiteSpec *d) { if (params->dismissWarnings) { appendFormat_String(buf, "dismissWarnings = 0x%x\n", params->dismissWarnings); } + if (!isEmpty_StringArray(¶ms->usedIdentities)) { + appendFormat_String( + buf, + "usedIdentities = \"%s\"\n", + cstrCollect_String(joinCStr_StringArray(¶ms->usedIdentities, " "))); + } appendCStr_String(buf, "\n"); write_File(f, utf8_String(buf)); + iEndCollect(); } delete_String(buf); } @@ -188,14 +215,19 @@ void deinit_SiteSpec(void) { deinit_String(&d->saveDir); } -void setValue_SiteSpec(const iString *site, enum iSiteSpecKey key, int value) { - iSiteSpec *d = &siteSpec_; +static iSiteParams *findParams_SiteSpec_(iSiteSpec *d, const iString *site) { const iString *hashKey = collect_String(lower_String(site)); iSiteParams *params = value_StringHash(&d->sites, hashKey); if (!params) { params = new_SiteParams(); insert_StringHash(&d->sites, hashKey, params); } + return params; +} + +void setValue_SiteSpec(const iString *site, enum iSiteSpecKey key, int value) { + iSiteSpec *d = &siteSpec_; + iSiteParams *params = findParams_SiteSpec_(d, site); iBool needSave = iFalse; switch (key) { case titanPort_SiteSpecKey: @@ -216,12 +248,7 @@ void setValue_SiteSpec(const iString *site, enum iSiteSpecKey key, int value) { void setValueString_SiteSpec(const iString *site, enum iSiteSpecKey key, const iString *value) { iSiteSpec *d = &siteSpec_; - const iString *hashKey = collect_String(lower_String(site)); - iSiteParams *params = value_StringHash(&d->sites, hashKey); - if (!params) { - params = new_SiteParams(); - insert_StringHash(&d->sites, hashKey, params); - } + iSiteParams *params = findParams_SiteSpec_(d, site); iBool needSave = iFalse; switch (key) { case titanIdentity_SiteSpecKey: @@ -238,6 +265,44 @@ void setValueString_SiteSpec(const iString *site, enum iSiteSpecKey key, const i } } +static void insertOrRemoveString_SiteSpec_(iSiteSpec *d, const iString *site, enum iSiteSpecKey key, + const iString *value, iBool doInsert) { + iSiteParams *params = findParams_SiteSpec_(d, site); + iBool needSave = iFalse; + switch (key) { + case usedIdentities_SiteSpecKey: { + const size_t index = findUsedIdentity_SiteParams_(params, value); + if (doInsert && index == iInvalidPos) { + pushBack_StringArray(¶ms->usedIdentities, value); + needSave = iTrue; + } + else if (!doInsert && index != iInvalidPos) { + remove_StringArray(¶ms->usedIdentities, index); + needSave = iTrue; + } + break; + } + default: + break; + } + if (needSave) { + save_SiteSpec_(d); + } +} + +void insertString_SiteSpec(const iString *site, enum iSiteSpecKey key, const iString *value) { + insertOrRemoveString_SiteSpec_(&siteSpec_, site, key, value, iTrue); +} + +void removeString_SiteSpec(const iString *site, enum iSiteSpecKey key, const iString *value) { + insertOrRemoveString_SiteSpec_(&siteSpec_, site, key, value, iFalse); +} + +const iStringArray *strings_SiteSpec(const iString *site, enum iSiteSpecKey key) { + const iSiteParams *params = findParams_SiteSpec_(&siteSpec_, site); + return ¶ms->usedIdentities; +} + int value_SiteSpec(const iString *site, enum iSiteSpecKey key) { iSiteSpec *d = &siteSpec_; const iSiteParams *params = constValue_StringHash(&d->sites, collect_String(lower_String(site))); diff --git a/src/sitespec.h b/src/sitespec.h index 5adaeb8c..11c40e3c 100644 --- a/src/sitespec.h +++ b/src/sitespec.h @@ -22,22 +22,26 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #pragma once -#include +#include iDeclareType(SiteSpec) - + enum iSiteSpecKey { - titanPort_SiteSpecKey, - titanIdentity_SiteSpecKey, - dismissWarnings_SiteSpecKey, + titanPort_SiteSpecKey, /* int */ + titanIdentity_SiteSpecKey, /* String */ + dismissWarnings_SiteSpecKey, /* int */ + usedIdentities_SiteSpecKey, /* StringArray */ }; - + void init_SiteSpec (const char *saveDir); void deinit_SiteSpec (void); /* changes saved immediately */ void setValue_SiteSpec (const iString *site, enum iSiteSpecKey key, int value); void setValueString_SiteSpec (const iString *site, enum iSiteSpecKey key, const iString *value); +void insertString_SiteSpec (const iString *site, enum iSiteSpecKey key, const iString *value); +void removeString_SiteSpec (const iString *site, enum iSiteSpecKey key, const iString *value); -int value_SiteSpec (const iString *site, enum iSiteSpecKey key); -const iString * valueString_SiteSpec (const iString *site, enum iSiteSpecKey key); +int value_SiteSpec (const iString *site, enum iSiteSpecKey key); +const iString * valueString_SiteSpec (const iString *site, enum iSiteSpecKey key); +const iStringArray *strings_SiteSpec (const iString *site, enum iSiteSpecKey key); diff --git a/src/ui/root.c b/src/ui/root.c index c2161d80..f06ae842 100644 --- a/src/ui/root.c +++ b/src/ui/root.c @@ -38,6 +38,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "../history.h" #include "../gmcerts.h" #include "../gmutil.h" +#include "../sitespec.h" #include "../visited.h" #if defined (iPlatformMsys) @@ -330,6 +331,66 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) { openMenuFlags_Widget(menu, zero_I2(), postCommands_MenuOpenFlags | center_MenuOpenFlags); return iTrue; } + else if (equal_Command(cmd, "identmenu.open")) { + iWidget *button = findWidget_Root("navbar.ident"); + iArray items; + init_Array(&items, sizeof(iMenuItem)); + /* Current identity. */ + const iString *docUrl = url_DocumentWidget(document_App()); + const iGmIdentity *ident = identityForUrl_GmCerts(certs_App(), docUrl); + const iString *fp = collect_String(hexEncode_Block(&ident->fingerprint)); + pushBackN_Array(&items, + (iMenuItem[]){ { format_CStr("///" uiHeading_ColorEscape "%s", + ident ? cstr_String(name_GmIdentity(ident)) + : "${menu.identity.notactive}") }, + { "---" } }, + 2); + /* Alternate identities. */ { + const iString *site = collectNewRange_String(urlRoot_String(docUrl)); + iBool haveAlts = iFalse; + iConstForEach(StringArray, i, strings_SiteSpec(site, usedIdentities_SiteSpecKey)) { + if (!equal_String(i.value, fp)) { + const iBlock *otherFp = collect_Block(hexDecode_Rangecc(range_String(i.value))); + const iGmIdentity *other = findIdentity_GmCerts(certs_App(), otherFp); + if (other) { + pushBack_Array( + &items, + &(iMenuItem){ + format_CStr(translateCStr_Lang("\U0001f816 ${ident.switch}"), + cstr_String(name_GmIdentity(other))), + 0, + 0, + format_CStr("ident.switch fp:%s", cstr_String(i.value)) }); + haveAlts = iTrue; + } + } + } + if (haveAlts) { + pushBack_Array(&items, &(iMenuItem){ "---" }); + } + } + iSidebarWidget *sidebar = findWidget_App("sidebar"); + pushBackN_Array( + &items, + (iMenuItem[]){ + { add_Icon " ${menu.identity.new}", newIdentity_KeyShortcut, "ident.new" }, + { "${menu.identity.import}", SDLK_i, KMOD_PRIMARY | KMOD_SHIFT, "ident.import" }, + { "---" }, + { isVisible_Widget(sidebar) && mode_SidebarWidget(sidebar) == identities_SidebarMode + ? leftHalf_Icon " ${menu.hide.identities}" + : leftHalf_Icon " ${menu.show.identities}", + 0, + 0, + deviceType_App() == phone_AppDeviceType ? "toolbar.showident" + : "sidebar.mode arg:3 toggle:1" }, + }, + 4); + iWidget *menu = + makeMenu_Widget(button, constData_Array(&items), size_Array(&items)); + openMenu_Widget(menu, topLeft_Rect(bounds_Widget(button))); + deinit_Array(&items); + return iTrue; + } else if (equal_Command(cmd, "contextclick")) { iBool showBarMenu = iFalse; if (equal_Rangecc(range_Command(cmd, "id"), "buttons")) { @@ -780,6 +841,26 @@ static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) { dismissPortraitPhoneSidebars_Root(get_Root()); updateNavBarIdentity_(navBar); updateNavDirButtons_(navBar); + /* Update site-specific used identities. */ { + const iGmIdentity *ident = + identityForUrl_GmCerts(certs_App(), url_DocumentWidget(document_App())); + if (ident) { + const iString *site = + collectNewRange_String(urlRoot_String(canonicalUrl_String(urlStr))); + const iStringArray *usedIdents = + strings_SiteSpec(site, usedIdentities_SiteSpecKey); + const iString *fingerprint = collect_String(hexEncode_Block(&ident->fingerprint)); + /* Keep this identity at the end of the list. */ + removeString_SiteSpec(site, usedIdentities_SiteSpecKey, fingerprint); + insertString_SiteSpec(site, usedIdentities_SiteSpecKey, fingerprint); + /* Keep the list short. */ + while (size_StringArray(usedIdents) > 5) { + removeString_SiteSpec(site, + usedIdentities_SiteSpecKey, + constAt_StringArray(usedIdents, 0)); + } + } + } /* Icon updates should be limited to automatically chosen icons if the user is allowed to pick their own in the future. */ if (updateBookmarkIcon_Bookmarks(bookmarks_App(), urlStr, @@ -1268,10 +1349,10 @@ void createUserInterface_Root(iRoot *d) { setId_Widget(addChild_Widget(rightEmbed, iClob(makePadding_Widget(0))), "url.embedpad"); } /* The active identity menu. */ { - iLabelWidget *idMenu = makeMenuButton_LabelWidget( - "\U0001f464", identityButtonMenuItems_, iElemCount(identityButtonMenuItems_)); - setAlignVisually_LabelWidget(idMenu, iTrue); - setId_Widget(addChildFlags_Widget(navBar, iClob(idMenu), collapse_WidgetFlag), "navbar.ident"); + iLabelWidget *idButton = new_LabelWidget(person_Icon, "identmenu.open"); +// "\U0001f464", identityButtonMenuItems_, iElemCount(identityButtonMenuItems_)); + setAlignVisually_LabelWidget(idButton, iTrue); + setId_Widget(addChildFlags_Widget(navBar, iClob(idButton), collapse_WidgetFlag), "navbar.ident"); } addChildFlags_Widget(navBar, iClob(new_Widget()), expand_WidgetFlag); setId_Widget(addChildFlags_Widget(navBar, -- cgit v1.2.3 From b9cdb34c59dc133b549deed5a4f3b9bb95197cca Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 2 Dec 2021 10:02:16 +0200 Subject: Refactored CertListWidget out of the sidebar The identity list is needed elsewhere outside of the sidebar, so moved it into a specialized ListWidget class. --- CMakeLists.txt | 2 + res/about/version.gmi | 2 +- src/ui/certlistwidget.c | 474 ++++++++++++++++++++++++++++++++++++++++++++++++ src/ui/certlistwidget.h | 40 ++++ src/ui/listwidget.c | 15 -- src/ui/listwidget.h | 20 ++ src/ui/root.c | 18 +- src/ui/sidebarwidget.c | 335 ++++------------------------------ 8 files changed, 586 insertions(+), 320 deletions(-) create mode 100644 src/ui/certlistwidget.c create mode 100644 src/ui/certlistwidget.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 5c225c9b..4200c7c5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -168,6 +168,8 @@ set (SOURCES src/ui/bindingswidget.h src/ui/certimportwidget.c src/ui/certimportwidget.h + src/ui/certlistwidget.c + src/ui/certlistwidget.h src/ui/color.c src/ui/color.h src/ui/command.c diff --git a/res/about/version.gmi b/res/about/version.gmi index 7a14e79b..506e3ae0 100644 --- a/res/about/version.gmi +++ b/res/about/version.gmi @@ -8,7 +8,7 @@ ## 1.10 New features: -* Identity toolbar menu can be used to switch between alternate identities that have been used on the site. If you have multiple accounts on a site, this makes it more convenient to switch between them. +* Identity toolbar menu can be used to switch between alternate identities. If you have used multiple identities on one site, this makes it more convenient to switch between them. ## 1.9.2 * Windows: Use the correct version number for update checks. diff --git a/src/ui/certlistwidget.c b/src/ui/certlistwidget.c new file mode 100644 index 00000000..4d939ae2 --- /dev/null +++ b/src/ui/certlistwidget.c @@ -0,0 +1,474 @@ +/* Copyright 2021 Jaakko Keränen + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + +#include "certlistwidget.h" + +#include "documentwidget.h" +#include "command.h" +#include "labelwidget.h" +#include "listwidget.h" +#include "../gmcerts.h" +#include "../app.h" + +#include + +iDeclareType(CertItem) +typedef iListItemClass iCertItemClass; + +struct Impl_CertItem { + iListItem listItem; + uint32_t id; + int indent; + iChar icon; + iBool isBold; + iString label; + iString meta; +// iString url; +}; + +void init_CertItem(iCertItem *d) { + init_ListItem(&d->listItem); + d->id = 0; + d->indent = 0; + d->icon = 0; + d->isBold = iFalse; + init_String(&d->label); + init_String(&d->meta); +// init_String(&d->url); +} + +void deinit_CertItem(iCertItem *d) { +// deinit_String(&d->url); + deinit_String(&d->meta); + deinit_String(&d->label); +} + +static void draw_CertItem_(const iCertItem *d, iPaint *p, iRect itemRect, const iListWidget *list); + +iBeginDefineSubclass(CertItem, ListItem) + .draw = (iAny *) draw_CertItem_, +iEndDefineSubclass(CertItem) + +iDefineObjectConstruction(CertItem) + +/*----------------------------------------------------------------------------------------------*/ + +struct Impl_CertListWidget { + iListWidget list; + int itemFonts[2]; + iWidget *menu; /* context menu for an item */ + iCertItem *contextItem; /* list item accessed in the context menu */ + size_t contextIndex; /* index of list item accessed in the context menu */ +}; + +iDefineObjectConstruction(CertListWidget) + +static iGmIdentity *menuIdentity_CertListWidget_(const iCertListWidget *d) { + if (d->contextItem) { + return identity_GmCerts(certs_App(), d->contextItem->id); + } + return NULL; +} + +static void updateContextMenu_CertListWidget_(iCertListWidget *d) { + iArray *items = collectNew_Array(sizeof(iMenuItem)); + pushBackN_Array(items, (iMenuItem[]){ + { person_Icon " ${ident.use}", 0, 0, "ident.use arg:1" }, + { close_Icon " ${ident.stopuse}", 0, 0, "ident.use arg:0" }, + { close_Icon " ${ident.stopuse.all}", 0, 0, "ident.use arg:0 clear:1" }, + { "---", 0, 0, NULL }, + { edit_Icon " ${menu.edit.notes}", 0, 0, "ident.edit" }, + { "${ident.fingerprint}", 0, 0, "ident.fingerprint" }, + { export_Icon " ${ident.export}", 0, 0, "ident.export" }, + { "---", 0, 0, NULL }, + { delete_Icon " " uiTextCaution_ColorEscape "${ident.delete}", 0, 0, "ident.delete confirm:1" }, + }, 9); + /* Used URLs. */ + const iGmIdentity *ident = menuIdentity_CertListWidget_(d); + if (ident) { + size_t insertPos = 3; + if (!isEmpty_StringSet(ident->useUrls)) { + insert_Array(items, insertPos++, &(iMenuItem){ "---", 0, 0, NULL }); + } + const iString *docUrl = url_DocumentWidget(document_App()); + iBool usedOnCurrentPage = iFalse; + iConstForEach(StringSet, i, ident->useUrls) { + const iString *url = i.value; + usedOnCurrentPage |= equalCase_String(docUrl, url); + iRangecc urlStr = range_String(url); + if (startsWith_Rangecc(urlStr, "gemini://")) { + urlStr.start += 9; /* omit the default scheme */ + } + insert_Array(items, + insertPos++, + &(iMenuItem){ format_CStr(globe_Icon " %s", cstr_Rangecc(urlStr)), + 0, + 0, + format_CStr("!open url:%s", cstr_String(url)) }); + } + if (!usedOnCurrentPage) { + remove_Array(items, 1); + } + } + destroy_Widget(d->menu); + d->menu = makeMenu_Widget(as_Widget(d), data_Array(items), size_Array(items)); +} + +static void itemClicked_CertListWidget_(iCertListWidget *d, iCertItem *item, size_t itemIndex) { + iWidget *w = as_Widget(d); + setFocus_Widget(NULL); + d->contextItem = item; + if (d->contextIndex != iInvalidPos) { + invalidateItem_ListWidget(&d->list, d->contextIndex); + } + d->contextIndex = itemIndex; + if (itemIndex < numItems_ListWidget(&d->list)) { + updateContextMenu_CertListWidget_(d); + arrange_Widget(d->menu); + openMenu_Widget(d->menu, + bounds_Widget(w).pos.x < mid_Rect(rect_Root(w->root)).x + ? topRight_Rect(itemRect_ListWidget(&d->list, itemIndex)) + : addX_I2(topLeft_Rect(itemRect_ListWidget(&d->list, itemIndex)), + -width_Widget(d->menu))); + } +} + +static iBool processEvent_CertListWidget_(iCertListWidget *d, const SDL_Event *ev) { + iWidget *w = as_Widget(d); + /* Handle commands. */ + if (ev->type == SDL_USEREVENT && ev->user.code == command_UserEventCode) { + const char *cmd = command_UserEvent(ev); + if (equal_Command(cmd, "idents.changed")) { + updateItems_CertListWidget(d); + } + else if (isCommand_Widget(w, ev, "list.clicked")) { + itemClicked_CertListWidget_( + d, pointerLabel_Command(cmd, "item"), argU32Label_Command(cmd, "arg")); + return iTrue; + } + else if (isCommand_Widget(w, ev, "ident.use")) { + iGmIdentity * ident = menuIdentity_CertListWidget_(d); + const iString *tabUrl = url_DocumentWidget(document_App()); + if (ident) { + if (argLabel_Command(cmd, "clear")) { + clearUse_GmIdentity(ident); + } + else if (arg_Command(cmd)) { + signIn_GmCerts(certs_App(), ident, tabUrl); + postCommand_App("navigate.reload"); + } + else { + signOut_GmCerts(certs_App(), tabUrl); + postCommand_App("navigate.reload"); + } + saveIdentities_GmCerts(certs_App()); + updateItems_CertListWidget(d); + } + return iTrue; + } + else if (isCommand_Widget(w, ev, "ident.edit")) { + const iGmIdentity *ident = menuIdentity_CertListWidget_(d); + if (ident) { + makeValueInput_Widget(get_Root()->widget, + &ident->notes, + uiHeading_ColorEscape "${heading.ident.notes}", + format_CStr(cstr_Lang("dlg.ident.notes"), cstr_String(name_GmIdentity(ident))), + uiTextAction_ColorEscape "${dlg.default}", + format_CStr("!ident.setnotes ident:%p ptr:%p", ident, d)); + } + return iTrue; + } + else if (isCommand_Widget(w, ev, "ident.fingerprint")) { + const iGmIdentity *ident = menuIdentity_CertListWidget_(d); + if (ident) { + const iString *fps = collect_String( + hexEncode_Block(collect_Block(fingerprint_TlsCertificate(ident->cert)))); + SDL_SetClipboardText(cstr_String(fps)); + } + return iTrue; + } + else if (isCommand_Widget(w, ev, "ident.export")) { + const iGmIdentity *ident = menuIdentity_CertListWidget_(d); + if (ident) { + iString *pem = collect_String(pem_TlsCertificate(ident->cert)); + append_String(pem, collect_String(privateKeyPem_TlsCertificate(ident->cert))); + iDocumentWidget *expTab = newTab_App(NULL, iTrue); + setUrlAndSource_DocumentWidget( + expTab, + collectNewFormat_String("file:%s.pem", cstr_String(name_GmIdentity(ident))), + collectNewCStr_String("text/plain"), + utf8_String(pem)); + } + return iTrue; + } + else if (isCommand_Widget(w, ev, "ident.setnotes")) { + iGmIdentity *ident = pointerLabel_Command(cmd, "ident"); + if (ident) { + setCStr_String(&ident->notes, suffixPtr_Command(cmd, "value")); + updateItems_CertListWidget(d); + } + return iTrue; + } + else if (isCommand_Widget(w, ev, "ident.pickicon")) { + return iTrue; + } + else if (isCommand_Widget(w, ev, "ident.reveal")) { + const iGmIdentity *ident = menuIdentity_CertListWidget_(d); + if (ident) { + const iString *crtPath = certificatePath_GmCerts(certs_App(), ident); + if (crtPath) { + revealPath_App(crtPath); + } + } + return iTrue; + } + else if (isCommand_Widget(w, ev, "ident.delete")) { + iCertItem *item = d->contextItem; + if (argLabel_Command(cmd, "confirm")) { + makeQuestion_Widget( + uiTextCaution_ColorEscape "${heading.ident.delete}", + format_CStr(cstr_Lang("dlg.confirm.ident.delete"), + uiTextAction_ColorEscape, + cstr_String(&item->label), + uiText_ColorEscape), + (iMenuItem[]){ { "${cancel}", 0, 0, NULL }, + { uiTextCaution_ColorEscape "${dlg.ident.delete}", + 0, + 0, + format_CStr("!ident.delete confirm:0 ptr:%p", d) } }, + 2); + return iTrue; + } + deleteIdentity_GmCerts(certs_App(), menuIdentity_CertListWidget_(d)); + postCommand_App("idents.changed"); + return iTrue; + } + } + if (ev->type == SDL_MOUSEMOTION && !isVisible_Widget(d->menu)) { + const iInt2 mouse = init_I2(ev->motion.x, ev->motion.y); + /* Update cursor. */ + if (contains_Widget(w, mouse)) { + setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW); + } + else if (d->contextIndex != iInvalidPos) { + invalidateItem_ListWidget(&d->list, d->contextIndex); + d->contextIndex = iInvalidPos; + } + } + /* Update context menu items. */ + if (ev->type == SDL_MOUSEBUTTONDOWN && ev->button.button == SDL_BUTTON_RIGHT) { + d->contextItem = NULL; + if (!isVisible_Widget(d->menu)) { + updateMouseHover_ListWidget(&d->list); + } + if (constHoverItem_ListWidget(&d->list) || isVisible_Widget(d->menu)) { + d->contextItem = hoverItem_ListWidget(&d->list); + /* Context is drawn in hover state. */ + if (d->contextIndex != iInvalidPos) { + invalidateItem_ListWidget(&d->list, d->contextIndex); + } + d->contextIndex = hoverItemIndex_ListWidget(&d->list); + updateContextMenu_CertListWidget_(d); + /* TODO: Some callback-based mechanism would be nice for updating menus right + before they open? At least move these to `updateContextMenu_ */ + const iGmIdentity *ident = constHoverIdentity_CertListWidget(d); + const iString * docUrl = url_DocumentWidget(document_App()); + iForEach(ObjectList, i, children_Widget(d->menu)) { + if (isInstance_Object(i.object, &Class_LabelWidget)) { + iLabelWidget *menuItem = i.object; + const char * cmdItem = cstr_String(command_LabelWidget(menuItem)); + if (equal_Command(cmdItem, "ident.use")) { + const iBool cmdUse = arg_Command(cmdItem) != 0; + const iBool cmdClear = argLabel_Command(cmdItem, "clear") != 0; + setFlags_Widget( + as_Widget(menuItem), + disabled_WidgetFlag, + (cmdClear && !isUsed_GmIdentity(ident)) || + (!cmdClear && cmdUse && isUsedOn_GmIdentity(ident, docUrl)) || + (!cmdClear && !cmdUse && !isUsedOn_GmIdentity(ident, docUrl))); + } + } + } + } + if (hoverItem_ListWidget(&d->list) || isVisible_Widget(d->menu)) { + processContextMenuEvent_Widget(d->menu, ev, {}); + } + } + return ((iWidgetClass *) class_Widget(w)->super)->processEvent(w, ev); +} + +static void draw_CertListWidget_(const iCertListWidget *d) { + const iWidget *w = constAs_Widget(d); + ((iWidgetClass *) class_Widget(w)->super)->draw(w); +} + +static void draw_CertItem_(const iCertItem *d, iPaint *p, iRect itemRect, + const iListWidget *list) { + const iCertListWidget *certList = (const iCertListWidget *) list; + const iBool isMenuVisible = isVisible_Widget(certList->menu); + const iBool isDragging = constDragItem_ListWidget(list) == d; + const iBool isPressing = isMouseDown_ListWidget(list) && !isDragging; + const iBool isHover = + (!isMenuVisible && + isHover_Widget(constAs_Widget(list)) && + constHoverItem_ListWidget(list) == d) || + (isMenuVisible && certList->contextItem == d) || + isDragging; + const int itemHeight = height_Rect(itemRect); + const int iconColor = isHover ? (isPressing ? uiTextPressed_ColorId : uiIconHover_ColorId) + : uiIcon_ColorId; + const int altIconColor = isPressing ? uiTextPressed_ColorId : uiTextCaution_ColorId; + const int font = certList->itemFonts[d->isBold ? 1 : 0]; + int bg = uiBackgroundSidebar_ColorId; + if (isHover) { + bg = isPressing ? uiBackgroundPressed_ColorId + : uiBackgroundFramelessHover_ColorId; + fillRect_Paint(p, itemRect, bg); + } + else if (d->listItem.isSelected) { + bg = uiBackgroundUnfocusedSelection_ColorId; + fillRect_Paint(p, itemRect, bg); + } +// iInt2 pos = itemRect.pos; + const int fg = isHover ? (isPressing ? uiTextPressed_ColorId : uiTextFramelessHover_ColorId) + : uiTextStrong_ColorId; + const iBool isUsedOnDomain = (d->indent != 0); + iString icon; + initUnicodeN_String(&icon, &d->icon, 1); + iInt2 cPos = topLeft_Rect(itemRect); + const int indent = 1.4f * lineHeight_Text(font); + addv_I2(&cPos, + init_I2(3 * gap_UI, + (itemHeight - lineHeight_Text(uiLabel_FontId) * 2 - lineHeight_Text(font)) / + 2)); + const int metaFg = isHover ? permanent_ColorId | (isPressing ? uiTextPressed_ColorId + : uiTextFramelessHover_ColorId) + : uiTextDim_ColorId; + if (!d->listItem.isSelected && !isUsedOnDomain) { + drawOutline_Text(font, cPos, metaFg, none_ColorId, range_String(&icon)); + } + drawRange_Text(font, + cPos, + d->listItem.isSelected ? iconColor + : isUsedOnDomain ? altIconColor + : uiBackgroundSidebar_ColorId, + range_String(&icon)); + deinit_String(&icon); + drawRange_Text(d->listItem.isSelected ? certList->itemFonts[1] : font, + add_I2(cPos, init_I2(indent, 0)), + fg, + range_String(&d->label)); + drawRange_Text(uiLabel_FontId, + add_I2(cPos, init_I2(indent, lineHeight_Text(font))), + metaFg, + range_String(&d->meta)); +} + +void init_CertListWidget(iCertListWidget *d) { + iWidget *w = as_Widget(d); + init_ListWidget(&d->list); + setId_Widget(w, "certlist"); + setBackgroundColor_Widget(w, none_ColorId); + d->itemFonts[0] = uiContent_FontId; + d->itemFonts[1] = uiContentBold_FontId; +#if defined (iPlatformMobile) + if (deviceType_App() == phone_AppDeviceType) { + d->itemFonts[0] = uiLabelBig_FontId; + d->itemFonts[1] = uiLabelBigBold_FontId; + } +#endif + d->menu = NULL; + d->contextItem = NULL; + d->contextIndex = iInvalidPos; +} + +void updateItemHeight_CertListWidget(iCertListWidget *d) { + setItemHeight_ListWidget(&d->list, 3.5f * lineHeight_Text(d->itemFonts[0])); +} + +iBool updateItems_CertListWidget(iCertListWidget *d) { + clear_ListWidget(&d->list); + destroy_Widget(d->menu); + d->menu = NULL; + const iString *tabUrl = url_DocumentWidget(document_App()); + const iRangecc tabHost = urlHost_String(tabUrl); + iBool haveItems = iFalse; + iConstForEach(PtrArray, i, identities_GmCerts(certs_App())) { + const iGmIdentity *ident = i.ptr; + iCertItem *item = new_CertItem(); + item->id = (uint32_t) index_PtrArrayConstIterator(&i); + item->icon = 0x1f464; /* person */ + set_String(&item->label, name_GmIdentity(ident)); + iDate until; + validUntil_TlsCertificate(ident->cert, &until); + const iBool isActive = isUsedOn_GmIdentity(ident, tabUrl); + format_String(&item->meta, + "%s", + isActive ? cstr_Lang("ident.using") + : isUsed_GmIdentity(ident) + ? formatCStrs_Lang("ident.usedonurls.n", size_StringSet(ident->useUrls)) + : cstr_Lang("ident.notused")); + const char *expiry = + ident->flags & temporary_GmIdentityFlag + ? cstr_Lang("ident.temporary") + : cstrCollect_String(format_Date(&until, cstr_Lang("ident.expiry"))); + if (isEmpty_String(&ident->notes)) { + appendFormat_String(&item->meta, "\n%s", expiry); + } + else { + appendFormat_String(&item->meta, + " \u2014 %s\n%s%s", + expiry, + escape_Color(uiHeading_ColorId), + cstr_String(&ident->notes)); + } + item->listItem.isSelected = isActive; + if (isUsedOnDomain_GmIdentity(ident, tabHost)) { + item->indent = 1; /* will be highlighted */ + } + addItem_ListWidget(&d->list, item); + haveItems = iTrue; + iRelease(item); + } + return haveItems; +} + +void deinit_CertListWidget(iCertListWidget *d) { + iUnused(d); +} + +const iGmIdentity *constHoverIdentity_CertListWidget(const iCertListWidget *d) { + const iCertItem *hoverItem = constHoverItem_ListWidget(&d->list); + if (hoverItem) { + return identity_GmCerts(certs_App(), hoverItem->id); + } + return NULL; +} + +iGmIdentity *hoverIdentity_CertListWidget(const iCertListWidget *d) { + return iConstCast(iGmIdentity *, constHoverIdentity_CertListWidget(d)); +} + +iBeginDefineSubclass(CertListWidget, ListWidget) + .processEvent = (iAny *) processEvent_CertListWidget_, + .draw = (iAny *) draw_CertListWidget_, +iEndDefineSubclass(CertListWidget) diff --git a/src/ui/certlistwidget.h b/src/ui/certlistwidget.h new file mode 100644 index 00000000..2e5f6247 --- /dev/null +++ b/src/ui/certlistwidget.h @@ -0,0 +1,40 @@ +/* Copyright 2021 Jaakko Keränen + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + +#pragma once + +#include "listwidget.h" + +iDeclareType(CertListWidget) + +typedef iListWidgetClass iCertListWidgetClass; +extern iCertListWidgetClass Class_CertListWidget; + +iDeclareObjectConstruction(CertListWidget) + +iDeclareType(GmIdentity) + +const iGmIdentity * constHoverIdentity_CertListWidget(const iCertListWidget *); +iGmIdentity * hoverIdentity_CertListWidget (const iCertListWidget *); + +iBool updateItems_CertListWidget (iCertListWidget *); /* returns False is empty */ +void updateItemHeight_CertListWidget (iCertListWidget *); diff --git a/src/ui/listwidget.c b/src/ui/listwidget.c index c2ba5581..82e4e451 100644 --- a/src/ui/listwidget.c +++ b/src/ui/listwidget.c @@ -49,21 +49,6 @@ iDefineClass(ListItem) iDefineObjectConstruction(ListWidget) -struct Impl_ListWidget { - iWidget widget; - iScrollWidget *scroll; - iSmoothScroll scrollY; - int itemHeight; - iPtrArray items; - size_t hoverItem; - size_t dragItem; - iInt2 dragOrigin; /* offset from mouse to drag item's top-left corner */ - iClick click; - iIntSet invalidItems; - iVisBuf *visBuf; - iBool noHoverWhileScrolling; -}; - static void refreshWhileScrolling_ListWidget_(iAnyObject *any) { iListWidget *d = any; updateVisible_ListWidget(d); diff --git a/src/ui/listwidget.h b/src/ui/listwidget.h index 8adf6ac3..7e6624a0 100644 --- a/src/ui/listwidget.h +++ b/src/ui/listwidget.h @@ -25,6 +25,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "scrollwidget.h" #include "paint.h" +#include #include iDeclareType(ListWidget) @@ -48,6 +49,25 @@ iDeclareObjectConstruction(ListItem) iDeclareWidgetClass(ListWidget) iDeclareObjectConstruction(ListWidget) +iDeclareType(VisBuf) + +struct Impl_ListWidget { + iWidget widget; + iScrollWidget *scroll; + iSmoothScroll scrollY; + int itemHeight; + iPtrArray items; + size_t hoverItem; + size_t dragItem; + iInt2 dragOrigin; /* offset from mouse to drag item's top-left corner */ + iClick click; + iIntSet invalidItems; + iVisBuf *visBuf; + iBool noHoverWhileScrolling; +}; + +void init_ListWidget (iListWidget *); + void setItemHeight_ListWidget (iListWidget *, int itemHeight); void invalidate_ListWidget (iListWidget *); diff --git a/src/ui/root.c b/src/ui/root.c index f06ae842..5ed6b529 100644 --- a/src/ui/root.c +++ b/src/ui/root.c @@ -332,7 +332,8 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) { return iTrue; } else if (equal_Command(cmd, "identmenu.open")) { - iWidget *button = findWidget_Root("navbar.ident"); + iWidget *toolBar = findWidget_Root("toolbar"); + iWidget *button = findWidget_Root(toolBar ? "toolbar.ident" : "navbar.ident"); iArray items; init_Array(&items, sizeof(iMenuItem)); /* Current identity. */ @@ -375,16 +376,21 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) { (iMenuItem[]){ { add_Icon " ${menu.identity.new}", newIdentity_KeyShortcut, "ident.new" }, { "${menu.identity.import}", SDLK_i, KMOD_PRIMARY | KMOD_SHIFT, "ident.import" }, - { "---" }, + { "---" } }, 3); + if (deviceType_App() == desktop_AppDeviceType) { + pushBack_Array(&items, &(iMenuItem) { isVisible_Widget(sidebar) && mode_SidebarWidget(sidebar) == identities_SidebarMode ? leftHalf_Icon " ${menu.hide.identities}" : leftHalf_Icon " ${menu.show.identities}", 0, 0, deviceType_App() == phone_AppDeviceType ? "toolbar.showident" - : "sidebar.mode arg:3 toggle:1" }, - }, - 4); + : "sidebar.mode arg:3 toggle:1" }); + } + else { + pushBack_Array(&items, &(iMenuItem){ gear_Icon " ${menu.identities}", 0, 0, + "toolbar.showident"}); + } iWidget *menu = makeMenu_Widget(button, constData_Array(&items), size_Array(&items)); openMenu_Widget(menu, topLeft_Rect(bounds_Widget(button))); @@ -1475,7 +1481,7 @@ void createUserInterface_Root(iRoot *d) { frameless_WidgetFlag), "toolbar.forward"); setId_Widget(addChildFlags_Widget(toolBar, - iClob(newLargeIcon_LabelWidget("\U0001f464", "toolbar.showident")), + iClob(newLargeIcon_LabelWidget("\U0001f464", "identmenu.open")), frameless_WidgetFlag), "toolbar.ident"); setId_Widget(addChildFlags_Widget(toolBar, diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index 9cfc507a..8440a597 100644 --- a/src/ui/sidebarwidget.c +++ b/src/ui/sidebarwidget.c @@ -25,6 +25,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "app.h" #include "defs.h" #include "bookmarks.h" +#include "certlistwidget.h" #include "command.h" #include "documentwidget.h" #include "feeds.h" @@ -96,6 +97,7 @@ struct Impl_SidebarWidget { iString cmdPrefix; iWidget * blank; iListWidget * list; + iCertListWidget * certList; iWidget * actions; /* below the list, area for buttons */ int modeScroll[max_SidebarMode]; iLabelWidget * modeButtons[max_SidebarMode]; @@ -114,6 +116,10 @@ struct Impl_SidebarWidget { iDefineObjectConstructionArgs(SidebarWidget, (enum iSidebarSide side), side) +iLocalDef iListWidget *list_SidebarWidget_(iSidebarWidget *d) { + return d->mode == identities_SidebarMode ? (iListWidget *) d->certList : d->list; +} + static iBool isResizing_SidebarWidget_(const iSidebarWidget *d) { return (flags_Widget(d->resizer) & pressed_WidgetFlag) != 0; } @@ -195,65 +201,6 @@ static iLabelWidget *addActionButton_SidebarWidget_(iSidebarWidget *d, const cha return btn; } -static iGmIdentity *menuIdentity_SidebarWidget_(const iSidebarWidget *d) { - if (d->mode == identities_SidebarMode) { - if (d->contextItem) { - return identity_GmCerts(certs_App(), d->contextItem->id); - } - } - return NULL; -} - -static void updateContextMenu_SidebarWidget_(iSidebarWidget *d) { - if (d->mode != identities_SidebarMode) { - return; - } - iArray *items = collectNew_Array(sizeof(iMenuItem)); - pushBackN_Array(items, (iMenuItem[]){ - { person_Icon " ${ident.use}", 0, 0, "ident.use arg:1" }, - { close_Icon " ${ident.stopuse}", 0, 0, "ident.use arg:0" }, - { close_Icon " ${ident.stopuse.all}", 0, 0, "ident.use arg:0 clear:1" }, - { "---", 0, 0, NULL }, - { edit_Icon " ${menu.edit.notes}", 0, 0, "ident.edit" }, - { "${ident.fingerprint}", 0, 0, "ident.fingerprint" }, - { export_Icon " ${ident.export}", 0, 0, "ident.export" }, - { "---", 0, 0, NULL }, - { delete_Icon " " uiTextCaution_ColorEscape "${ident.delete}", 0, 0, "ident.delete confirm:1" }, - }, 9); - /* Used URLs. */ - const iGmIdentity *ident = menuIdentity_SidebarWidget_(d); - if (ident) { - size_t insertPos = 3; - if (!isEmpty_StringSet(ident->useUrls)) { - insert_Array(items, insertPos++, &(iMenuItem){ "---", 0, 0, NULL }); - } - const iString *docUrl = url_DocumentWidget(document_App()); - iBool usedOnCurrentPage = iFalse; - iConstForEach(StringSet, i, ident->useUrls) { - const iString *url = i.value; - usedOnCurrentPage |= equalCase_String(docUrl, url); - iRangecc urlStr = range_String(url); - if (startsWith_Rangecc(urlStr, "gemini://")) { - urlStr.start += 9; /* omit the default scheme */ - } - if (endsWith_Rangecc(urlStr, "/")) { - urlStr.end--; /* looks cleaner */ - } - insert_Array(items, - insertPos++, - &(iMenuItem){ format_CStr(globe_Icon " %s", cstr_Rangecc(urlStr)), - 0, - 0, - format_CStr("!open url:%s", cstr_String(url)) }); - } - if (!usedOnCurrentPage) { - remove_Array(items, 1); - } - } - destroy_Widget(d->menu); - 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)) { @@ -547,46 +494,7 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct break; } case identities_SidebarMode: { - const iString *tabUrl = url_DocumentWidget(document_App()); - const iRangecc tabHost = urlHost_String(tabUrl); - isEmpty = iTrue; - iConstForEach(PtrArray, i, identities_GmCerts(certs_App())) { - const iGmIdentity *ident = i.ptr; - iSidebarItem *item = new_SidebarItem(); - item->id = (uint32_t) index_PtrArrayConstIterator(&i); - item->icon = 0x1f464; /* person */ - set_String(&item->label, name_GmIdentity(ident)); - iDate until; - validUntil_TlsCertificate(ident->cert, &until); - const iBool isActive = isUsedOn_GmIdentity(ident, tabUrl); - format_String(&item->meta, - "%s", - isActive ? cstr_Lang("ident.using") - : isUsed_GmIdentity(ident) - ? formatCStrs_Lang("ident.usedonurls.n", size_StringSet(ident->useUrls)) - : cstr_Lang("ident.notused")); - const char *expiry = - ident->flags & temporary_GmIdentityFlag - ? cstr_Lang("ident.temporary") - : cstrCollect_String(format_Date(&until, cstr_Lang("ident.expiry"))); - if (isEmpty_String(&ident->notes)) { - appendFormat_String(&item->meta, "\n%s", expiry); - } - else { - appendFormat_String(&item->meta, - " \u2014 %s\n%s%s", - expiry, - escape_Color(uiHeading_ColorId), - cstr_String(&ident->notes)); - } - item->listItem.isSelected = isActive; - if (isUsedOnDomain_GmIdentity(ident, tabHost)) { - item->indent = 1; /* will be highlighted */ - } - addItem_ListWidget(d->list, item); - iRelease(item); - isEmpty = iFalse; - } + isEmpty = !updateItems_CertListWidget(d->certList); /* Actions. */ if (!isEmpty) { addActionButton_SidebarWidget_(d, add_Icon " ${sidebar.action.ident.new}", "ident.new", 0); @@ -597,9 +505,11 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct default: break; } - scrollOffset_ListWidget(d->list, 0); - updateVisible_ListWidget(d->list); - invalidate_ListWidget(d->list); + setFlags_Widget(as_Widget(d->list), hidden_WidgetFlag, d->mode == identities_SidebarMode); + setFlags_Widget(as_Widget(d->certList), hidden_WidgetFlag, d->mode != identities_SidebarMode); + scrollOffset_ListWidget(list_SidebarWidget_(d), 0); + updateVisible_ListWidget(list_SidebarWidget_(d)); + invalidate_ListWidget(list_SidebarWidget_(d)); /* Content for a blank tab. */ if (isEmpty) { if (d->mode == feeds_SidebarMode) { @@ -649,7 +559,7 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct #endif arrange_Widget(d->actions); arrange_Widget(as_Widget(d)); - updateMouseHover_ListWidget(d->list); + updateMouseHover_ListWidget(list_SidebarWidget_(d)); } static void updateItems_SidebarWidget_(iSidebarWidget *d) { @@ -668,10 +578,11 @@ static size_t findItem_SidebarWidget_(const iSidebarWidget *d, int id) { } static void updateItemHeight_SidebarWidget_(iSidebarWidget *d) { + const float heights[max_SidebarMode] = { 1.333f, 2.333f, 1.333f, 3.5f, 1.2f }; if (d->list) { - const float heights[max_SidebarMode] = { 1.333f, 2.333f, 1.333f, 3.5f, 1.2f }; setItemHeight_ListWidget(d->list, heights[d->mode] * lineHeight_Text(d->itemFonts[0])); } + updateItemHeight_CertListWidget(d->certList); } iBool setMode_SidebarWidget(iSidebarWidget *d, enum iSidebarMode mode) { @@ -679,18 +590,18 @@ iBool setMode_SidebarWidget(iSidebarWidget *d, enum iSidebarMode mode) { return iFalse; } if (d->mode >= 0 && d->mode < max_SidebarMode) { - d->modeScroll[d->mode] = scrollPos_ListWidget(d->list); /* saved for later */ + d->modeScroll[d->mode] = scrollPos_ListWidget(list_SidebarWidget_(d)); /* saved for later */ } d->mode = mode; for (enum iSidebarMode i = 0; i < max_SidebarMode; i++) { setFlags_Widget(as_Widget(d->modeButtons[i]), selected_WidgetFlag, i == d->mode); } - setBackgroundColor_Widget(as_Widget(d->list), + setBackgroundColor_Widget(as_Widget(list_SidebarWidget_(d)), d->mode == documentOutline_SidebarMode ? tmBannerBackground_ColorId : uiBackgroundSidebar_ColorId); updateItemHeight_SidebarWidget_(d); /* Restore previous scroll position. */ - setScrollPos_ListWidget(d->list, d->modeScroll[mode]); + setScrollPos_ListWidget(list_SidebarWidget_(d), d->modeScroll[mode]); return iTrue; } @@ -787,6 +698,7 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) { iZap(d->modeButtons); d->resizer = NULL; d->list = NULL; + d->certList = NULL; d->actions = NULL; d->closedFolders = new_IntSet(); /* On a phone, the right sidebar is used exclusively for Identities. */ @@ -829,10 +741,16 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) { setFlags_Widget(content, resizeChildren_WidgetFlag, iTrue); iWidget *listAndActions = makeVDiv_Widget(); addChild_Widget(content, iClob(listAndActions)); - d->list = new_ListWidget(); + iWidget *listArea = new_Widget(); + setFlags_Widget(listArea, resizeChildren_WidgetFlag, iTrue); + d->list = new_ListWidget(); setPadding_Widget(as_Widget(d->list), 0, gap_UI, 0, gap_UI); + addChild_Widget(listArea, iClob(d->list)); + d->certList = new_CertListWidget(); + setPadding_Widget(as_Widget(d->certList), 0, gap_UI, 0, gap_UI); + addChild_Widget(listArea, iClob(d->certList)); addChildFlags_Widget(listAndActions, - iClob(d->list), + iClob(listArea), expand_WidgetFlag); // | drawBackgroundToHorizontalSafeArea_WidgetFlag); setId_Widget(addChildPosFlags_Widget(listAndActions, iClob(d->actions = new_Widget()), @@ -892,16 +810,16 @@ iBool setButtonFont_SidebarWidget(iSidebarWidget *d, int font) { static const iGmIdentity *constHoverIdentity_SidebarWidget_(const iSidebarWidget *d) { if (d->mode == identities_SidebarMode) { - const iSidebarItem *hoverItem = constHoverItem_ListWidget(d->list); - if (hoverItem) { - return identity_GmCerts(certs_App(), hoverItem->id); - } + return constHoverIdentity_CertListWidget(d->certList); } return NULL; } static iGmIdentity *hoverIdentity_SidebarWidget_(const iSidebarWidget *d) { - return iConstCast(iGmIdentity *, constHoverIdentity_SidebarWidget_(d)); + if (d->mode == identities_SidebarMode) { + return hoverIdentity_CertListWidget(d->certList); + } + return NULL; } static void itemClicked_SidebarWidget_(iSidebarWidget *d, iSidebarItem *item, size_t itemIndex) { @@ -944,23 +862,6 @@ static void itemClicked_SidebarWidget_(iSidebarWidget *d, iSidebarItem *item, si } break; } - case identities_SidebarMode: { - d->contextItem = item; - if (d->contextIndex != iInvalidPos) { - invalidateItem_ListWidget(d->list, d->contextIndex); - } - d->contextIndex = itemIndex; - if (itemIndex < numItems_ListWidget(d->list)) { - updateContextMenu_SidebarWidget_(d); - arrange_Widget(d->menu); - openMenu_Widget(d->menu, - d->side == left_SidebarSide - ? topRight_Rect(itemRect_ListWidget(d->list, itemIndex)) - : addX_I2(topLeft_Rect(itemRect_ListWidget(d->list, itemIndex)), - -width_Widget(d->menu))); - } - break; - } default: break; } @@ -1245,9 +1146,6 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) } } } - else if (equal_Command(cmd, "idents.changed") && d->mode == identities_SidebarMode) { - updateItems_SidebarWidget_(d); - } else if (deviceType_App() == tablet_AppDeviceType && equal_Command(cmd, "toolbar.showident")) { postCommandf_App("sidebar.mode arg:%d toggle:1", identities_SidebarMode); return iTrue; @@ -1316,9 +1214,6 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) } return iTrue; } -// else if (isCommand_Widget(w, ev, "menu.closed")) { - // invalidateItem_ListWidget(d->list, d->contextIndex); -// } else if (isCommand_Widget(w, ev, "bookmark.open")) { const iSidebarItem *item = d->contextItem; if (d->mode == bookmarks_SidebarMode && item) { @@ -1548,103 +1443,6 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) } } } - else if (isCommand_Widget(w, ev, "ident.use")) { - iGmIdentity * ident = menuIdentity_SidebarWidget_(d); - const iString *tabUrl = url_DocumentWidget(document_App()); - if (ident) { - if (argLabel_Command(cmd, "clear")) { - clearUse_GmIdentity(ident); - } - else if (arg_Command(cmd)) { - signIn_GmCerts(certs_App(), ident, tabUrl); - postCommand_App("navigate.reload"); - } - else { - signOut_GmCerts(certs_App(), tabUrl); - postCommand_App("navigate.reload"); - } - saveIdentities_GmCerts(certs_App()); - updateItems_SidebarWidget_(d); - } - return iTrue; - } - else if (isCommand_Widget(w, ev, "ident.edit")) { - const iGmIdentity *ident = menuIdentity_SidebarWidget_(d); - if (ident) { - makeValueInput_Widget(get_Root()->widget, - &ident->notes, - uiHeading_ColorEscape "${heading.ident.notes}", - format_CStr(cstr_Lang("dlg.ident.notes"), cstr_String(name_GmIdentity(ident))), - uiTextAction_ColorEscape "${dlg.default}", - format_CStr("!ident.setnotes ident:%p ptr:%p", ident, d)); - } - return iTrue; - } - else if (isCommand_Widget(w, ev, "ident.fingerprint")) { - const iGmIdentity *ident = menuIdentity_SidebarWidget_(d); - if (ident) { - const iString *fps = collect_String( - hexEncode_Block(collect_Block(fingerprint_TlsCertificate(ident->cert)))); - SDL_SetClipboardText(cstr_String(fps)); - } - return iTrue; - } - else if (isCommand_Widget(w, ev, "ident.export")) { - const iGmIdentity *ident = menuIdentity_SidebarWidget_(d); - if (ident) { - iString *pem = collect_String(pem_TlsCertificate(ident->cert)); - append_String(pem, collect_String(privateKeyPem_TlsCertificate(ident->cert))); - iDocumentWidget *expTab = newTab_App(NULL, iTrue); - setUrlAndSource_DocumentWidget( - expTab, - collectNewFormat_String("file:%s.pem", cstr_String(name_GmIdentity(ident))), - collectNewCStr_String("text/plain"), - utf8_String(pem)); - } - return iTrue; - } - else if (isCommand_Widget(w, ev, "ident.setnotes")) { - iGmIdentity *ident = pointerLabel_Command(cmd, "ident"); - if (ident) { - setCStr_String(&ident->notes, suffixPtr_Command(cmd, "value")); - updateItems_SidebarWidget_(d); - } - return iTrue; - } - else if (isCommand_Widget(w, ev, "ident.pickicon")) { - return iTrue; - } - else if (isCommand_Widget(w, ev, "ident.reveal")) { - const iGmIdentity *ident = menuIdentity_SidebarWidget_(d); - if (ident) { - const iString *crtPath = certificatePath_GmCerts(certs_App(), ident); - if (crtPath) { - revealPath_App(crtPath); - } - } - return iTrue; - } - else if (isCommand_Widget(w, ev, "ident.delete")) { - iSidebarItem *item = d->contextItem; - if (argLabel_Command(cmd, "confirm")) { - makeQuestion_Widget( - uiTextCaution_ColorEscape "${heading.ident.delete}", - format_CStr(cstr_Lang("dlg.confirm.ident.delete"), - uiTextAction_ColorEscape, - cstr_String(&item->label), - uiText_ColorEscape), - (iMenuItem[]){ { "${cancel}", 0, 0, NULL }, - { uiTextCaution_ColorEscape "${dlg.ident.delete}", - 0, - 0, - format_CStr("!ident.delete confirm:0 ptr:%p", d) } }, - 2); - return iTrue; - } - deleteIdentity_GmCerts(certs_App(), menuIdentity_SidebarWidget_(d)); - postCommand_App("idents.changed"); - return iTrue; - } else if (isCommand_Widget(w, ev, "history.delete")) { if (d->contextItem && !isEmpty_String(&d->contextItem->url)) { removeUrl_Visited(visited_App(), &d->contextItem->url); @@ -1698,14 +1496,10 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) /* Update cursor. */ else if (contains_Widget(w, mouse)) { const iSidebarItem *item = constHoverItem_ListWidget(d->list); - if (item && d->mode != identities_SidebarMode) { - setCursor_Window(get_Window(), - item->listItem.isSeparator ? SDL_SYSTEM_CURSOR_ARROW - : SDL_SYSTEM_CURSOR_HAND); - } - else { - setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW); - } + setCursor_Window(get_Window(), + item ? (item->listItem.isSeparator ? SDL_SYSTEM_CURSOR_ARROW + : SDL_SYSTEM_CURSOR_HAND) + : SDL_SYSTEM_CURSOR_ARROW); } if (d->contextIndex != iInvalidPos) { invalidateItem_ListWidget(d->list, d->contextIndex); @@ -1713,7 +1507,7 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) } } /* Update context menu items. */ - if ((d->menu || d->mode == identities_SidebarMode) && ev->type == SDL_MOUSEBUTTONDOWN) { + if (d->menu && ev->type == SDL_MOUSEBUTTONDOWN) { if (ev->button.button == SDL_BUTTON_RIGHT) { d->contextItem = NULL; if (!isVisible_Widget(d->menu)) { @@ -1726,7 +1520,6 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) invalidateItem_ListWidget(d->list, d->contextIndex); } d->contextIndex = hoverItemIndex_ListWidget(d->list); - updateContextMenu_SidebarWidget_(d); /* TODO: Some callback-based mechanism would be nice for updating menus right before they open? At least move these to `updateContextMenu_ */ if (d->mode == bookmarks_SidebarMode && d->contextItem) { @@ -1756,26 +1549,6 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) isRead ? circle_Icon " ${feeds.entry.markunread}" : circleWhite_Icon " ${feeds.entry.markread}"); } - else if (d->mode == identities_SidebarMode) { - const iGmIdentity *ident = constHoverIdentity_SidebarWidget_(d); - const iString * docUrl = url_DocumentWidget(document_App()); - iForEach(ObjectList, i, children_Widget(d->menu)) { - if (isInstance_Object(i.object, &Class_LabelWidget)) { - iLabelWidget *menuItem = i.object; - const char * cmdItem = cstr_String(command_LabelWidget(menuItem)); - if (equal_Command(cmdItem, "ident.use")) { - const iBool cmdUse = arg_Command(cmdItem) != 0; - const iBool cmdClear = argLabel_Command(cmdItem, "clear") != 0; - setFlags_Widget( - as_Widget(menuItem), - disabled_WidgetFlag, - (cmdClear && !isUsed_GmIdentity(ident)) || - (!cmdClear && cmdUse && isUsedOn_GmIdentity(ident, docUrl)) || - (!cmdClear && !cmdUse && !isUsedOn_GmIdentity(ident, docUrl))); - } - } - } - } } } } @@ -2077,40 +1850,6 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect, } iEndCollect(); } - else if (sidebar->mode == identities_SidebarMode) { - const int fg = isHover ? (isPressing ? uiTextPressed_ColorId : uiTextFramelessHover_ColorId) - : uiTextStrong_ColorId; - const iBool isUsedOnDomain = (d->indent != 0); - iString icon; - initUnicodeN_String(&icon, &d->icon, 1); - iInt2 cPos = topLeft_Rect(itemRect); - const int indent = 1.4f * lineHeight_Text(font); - addv_I2(&cPos, - init_I2(3 * gap_UI, - (itemHeight - lineHeight_Text(uiLabel_FontId) * 2 - lineHeight_Text(font)) / - 2)); - const int metaFg = isHover ? permanent_ColorId | (isPressing ? uiTextPressed_ColorId - : uiTextFramelessHover_ColorId) - : uiTextDim_ColorId; - if (!d->listItem.isSelected && !isUsedOnDomain) { - drawOutline_Text(font, cPos, metaFg, none_ColorId, range_String(&icon)); - } - drawRange_Text(font, - cPos, - d->listItem.isSelected ? iconColor - : isUsedOnDomain ? altIconColor - : uiBackgroundSidebar_ColorId, - range_String(&icon)); - deinit_String(&icon); - drawRange_Text(d->listItem.isSelected ? sidebar->itemFonts[1] : font, - add_I2(cPos, init_I2(indent, 0)), - fg, - range_String(&d->label)); - drawRange_Text(uiLabel_FontId, - add_I2(cPos, init_I2(indent, lineHeight_Text(font))), - metaFg, - range_String(&d->meta)); - } } iBeginDefineSubclass(SidebarWidget, Widget) -- cgit v1.2.3 From 28f8dcd21324c3747e9d6201a1876a48d8d86477 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 2 Dec 2021 10:23:04 +0200 Subject: Mobile: Button group appearance --- src/ui/mobile.c | 13 +++++++++---- src/ui/sidebarwidget.c | 4 +++- src/ui/util.c | 6 +++--- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/ui/mobile.c b/src/ui/mobile.c index abc91218..7b34d389 100644 --- a/src/ui/mobile.c +++ b/src/ui/mobile.c @@ -515,6 +515,7 @@ void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) { } else if (equal_Command(spec, "radio") || equal_Command(spec, "buttons")) { const iBool isRadio = equal_Command(spec, "radio"); + const iBool isHorizontal = argLabel_Command(spec, "horizontal"); addChild_Widget(panel, iClob(makePadding_Widget(lineHeight_Text(labelFont_())))); iLabelWidget *head = makeHeading_Widget(label); setAllCaps_LabelWidget(head, iTrue); @@ -522,11 +523,12 @@ void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) { addChild_Widget(panel, iClob(head)); widget = new_Widget(); setBackgroundColor_Widget(widget, uiBackgroundSidebar_ColorId); - setPadding_Widget(widget, 4 * gap_UI, 2 * gap_UI, 4 * gap_UI, 2 * gap_UI); + const int hPad = (isHorizontal ? 0 : 1); + setPadding_Widget(widget, hPad * gap_UI, 2 * gap_UI, hPad * gap_UI, 2 * gap_UI); setFlags_Widget(widget, borderTop_WidgetFlag | borderBottom_WidgetFlag | - arrangeHorizontal_WidgetFlag | + (isHorizontal ? arrangeHorizontal_WidgetFlag : arrangeVertical_WidgetFlag) | arrangeHeight_WidgetFlag | resizeToParentWidth_WidgetFlag | resizeWidthOfChildren_WidgetFlag, @@ -534,7 +536,10 @@ void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) { setId_Widget(widget, id); for (const iMenuItem *radioItem = item->data; radioItem->label; radioItem++) { const char * radId = cstr_Rangecc(range_Command(radioItem->label, "id")); - int64_t flags = noBackground_WidgetFlag; + int64_t flags = noBackground_WidgetFlag | frameless_WidgetFlag; + if (!isHorizontal) { + flags |= alignLeft_WidgetFlag; + } iLabelWidget *button; if (isRadio) { const char *radLabel = @@ -552,7 +557,7 @@ void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) { updateSize_LabelWidget(button); } setId_Widget(as_Widget(button), radId); - setFont_LabelWidget(button, uiLabelMedium_FontId); + setFont_LabelWidget(button, isHorizontal ? uiLabelMedium_FontId : uiLabelBig_FontId); addChildFlags_Widget(widget, iClob(button), flags); } } diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index 8440a597..674e2b7c 100644 --- a/src/ui/sidebarwidget.c +++ b/src/ui/sidebarwidget.c @@ -582,7 +582,9 @@ static void updateItemHeight_SidebarWidget_(iSidebarWidget *d) { if (d->list) { setItemHeight_ListWidget(d->list, heights[d->mode] * lineHeight_Text(d->itemFonts[0])); } - updateItemHeight_CertListWidget(d->certList); + if (d->certList) { + updateItemHeight_CertListWidget(d->certList); + } } iBool setMode_SidebarWidget(iSidebarWidget *d, enum iSidebarMode mode) { diff --git a/src/ui/util.c b/src/ui/util.c index 695e43aa..04483e8d 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -2380,7 +2380,7 @@ iWidget *makePreferences_Widget(void) { { "heading id:heading.prefs.pagecontent" }, { "dropdown id:prefs.doctheme.dark", 0, 0, (const void *) docThemes[0] }, { "dropdown id:prefs.doctheme.light", 0, 0, (const void *) docThemes[1] }, - { "radio id:prefs.saturation", 0, 0, (const void *) satItems }, + { "radio horizontal:1 id:prefs.saturation", 0, 0, (const void *) satItems }, { "padding" }, { "dropdown id:prefs.imagestyle", 0, 0, (const void *) imgStyles }, { NULL } @@ -2397,12 +2397,12 @@ iWidget *makePreferences_Widget(void) { { "padding" }, { "dropdown id:prefs.font.ui", 0, 0, (const void *) constData_Array(makeFontItems_("ui")) }, { "padding" }, - { "button text:" fontpack_Icon " ${menu.fonts}", 0, 0, "!open url:about:fonts" }, + { "button text:" fontpack_Icon " " uiTextAction_ColorEscape "${menu.fonts}", 0, 0, "!open url:about:fonts" }, { NULL } }; const iMenuItem stylePanelItems[] = { { "title id:heading.prefs.style" }, - { "radio id:prefs.linewidth", 0, 0, (const void *) lineWidthItems }, + { "radio horizontal:1 id:prefs.linewidth", 0, 0, (const void *) lineWidthItems }, { "padding" }, { "input id:prefs.linespacing maxlen:5" }, { "radio id:prefs.quoteicon", 0, 0, (const void *) quoteItems }, -- cgit v1.2.3 From 8269c38f074cb991e79430bd2ae9fe892d412635 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 2 Dec 2021 14:48:56 +0200 Subject: Mobile: Button group style; upload dialog tweaks Use checkmarks for selections in button groups. --- po/en.po | 6 ++++++ res/lang/cs.bin | Bin 30962 -> 31054 bytes res/lang/de.bin | Bin 29969 -> 30061 bytes res/lang/en.bin | Bin 26066 -> 26158 bytes res/lang/eo.bin | Bin 25020 -> 25112 bytes res/lang/es.bin | Bin 29793 -> 29885 bytes res/lang/es_MX.bin | Bin 27125 -> 27217 bytes res/lang/fi.bin | Bin 29626 -> 29718 bytes res/lang/fr.bin | Bin 30773 -> 30865 bytes res/lang/gl.bin | Bin 28978 -> 29070 bytes res/lang/hu.bin | Bin 30798 -> 30890 bytes res/lang/ia.bin | Bin 28125 -> 28217 bytes res/lang/ie.bin | Bin 28713 -> 28805 bytes res/lang/isv.bin | Bin 24786 -> 24878 bytes res/lang/pl.bin | Bin 29401 -> 29493 bytes res/lang/ru.bin | Bin 44161 -> 44253 bytes res/lang/sk.bin | Bin 25122 -> 25214 bytes res/lang/sr.bin | Bin 43587 -> 43679 bytes res/lang/tok.bin | Bin 26835 -> 26927 bytes res/lang/tr.bin | Bin 29019 -> 29111 bytes res/lang/uk.bin | Bin 43506 -> 43598 bytes res/lang/zh_Hans.bin | Bin 25020 -> 25112 bytes res/lang/zh_Hant.bin | Bin 25218 -> 25310 bytes src/ui/inputwidget.c | 23 +++++++++++++++++++++-- src/ui/labelwidget.c | 51 ++++++++++++++++++++++++++++++++++---------------- src/ui/labelwidget.h | 1 + src/ui/mobile.c | 20 +++++++++++++++++++- src/ui/uploadwidget.c | 6 +++--- 28 files changed, 85 insertions(+), 22 deletions(-) diff --git a/po/en.po b/po/en.po index 9a154c5c..6675f78b 100644 --- a/po/en.po +++ b/po/en.po @@ -987,6 +987,9 @@ msgstr "UPLOAD WITH TITAN" msgid "upload.id" msgstr "Identity:" +msgid "heading.upload.id" +msgstr "Identification" + msgid "dlg.upload.id.none" msgstr "None" @@ -1020,6 +1023,9 @@ msgstr "Token:" msgid "hint.upload.token" msgstr "see server's instructions" +msgid "hint.upload.token.long" +msgstr "token — see server's instructions" + msgid "dlg.upload.send" msgstr "Upload" diff --git a/res/lang/cs.bin b/res/lang/cs.bin index d3e06c73..8f8b3a6f 100644 Binary files a/res/lang/cs.bin and b/res/lang/cs.bin differ diff --git a/res/lang/de.bin b/res/lang/de.bin index b9d155a7..2df7f11f 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 5bb8299a..0b2edc4b 100644 Binary files a/res/lang/en.bin and b/res/lang/en.bin differ diff --git a/res/lang/eo.bin b/res/lang/eo.bin index ff1ddb01..6c0dc697 100644 Binary files a/res/lang/eo.bin and b/res/lang/eo.bin differ diff --git a/res/lang/es.bin b/res/lang/es.bin index ba9b2343..5d04443c 100644 Binary files a/res/lang/es.bin and b/res/lang/es.bin differ diff --git a/res/lang/es_MX.bin b/res/lang/es_MX.bin index f4ddcc72..37438b3a 100644 Binary files a/res/lang/es_MX.bin and b/res/lang/es_MX.bin differ diff --git a/res/lang/fi.bin b/res/lang/fi.bin index f73891d6..53449c30 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 0851f535..2ccda4da 100644 Binary files a/res/lang/fr.bin and b/res/lang/fr.bin differ diff --git a/res/lang/gl.bin b/res/lang/gl.bin index 80215589..9f37b384 100644 Binary files a/res/lang/gl.bin and b/res/lang/gl.bin differ diff --git a/res/lang/hu.bin b/res/lang/hu.bin index 7d7ed94e..4de98a70 100644 Binary files a/res/lang/hu.bin and b/res/lang/hu.bin differ diff --git a/res/lang/ia.bin b/res/lang/ia.bin index 572015c2..1a512597 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 bb485f4e..3324bb69 100644 Binary files a/res/lang/ie.bin and b/res/lang/ie.bin differ diff --git a/res/lang/isv.bin b/res/lang/isv.bin index f90a1e7d..58673ac5 100644 Binary files a/res/lang/isv.bin and b/res/lang/isv.bin differ diff --git a/res/lang/pl.bin b/res/lang/pl.bin index 753e595b..0aaa2fbd 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 edcebb14..82c43ffc 100644 Binary files a/res/lang/ru.bin and b/res/lang/ru.bin differ diff --git a/res/lang/sk.bin b/res/lang/sk.bin index b843a383..db1bd9e7 100644 Binary files a/res/lang/sk.bin and b/res/lang/sk.bin differ diff --git a/res/lang/sr.bin b/res/lang/sr.bin index 1ef302d9..7f058106 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 6e3c7af7..206fe02c 100644 Binary files a/res/lang/tok.bin and b/res/lang/tok.bin differ diff --git a/res/lang/tr.bin b/res/lang/tr.bin index 71b9382f..430ab02c 100644 Binary files a/res/lang/tr.bin and b/res/lang/tr.bin differ diff --git a/res/lang/uk.bin b/res/lang/uk.bin index f7040f2f..49d58f42 100644 Binary files a/res/lang/uk.bin and b/res/lang/uk.bin differ diff --git a/res/lang/zh_Hans.bin b/res/lang/zh_Hans.bin index 3ccab576..ec375dcd 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 39393417..dd63c0eb 100644 Binary files a/res/lang/zh_Hant.bin and b/res/lang/zh_Hant.bin differ diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index a1fb8cb5..bd9b63b5 100644 --- a/src/ui/inputwidget.c +++ b/src/ui/inputwidget.c @@ -2343,10 +2343,29 @@ static void draw_InputWidget_(const iInputWidget *d) { /* If buffered, just draw the buffered copy. */ if (d->buffered && !isFocused) { /* Most input widgets will use this, since only one is focused at a time. */ - draw_TextBuf(d->buffered, addY_I2(drawPos, visLineOffsetY), white_ColorId); + if (flags_Widget(w) & alignRight_WidgetFlag) { + draw_TextBuf( + d->buffered, + addY_I2(init_I2(right_Rect(contentBounds) - d->buffered->size.x, drawPos.y), + visLineOffsetY), + white_ColorId); + } + else { + draw_TextBuf(d->buffered, addY_I2(drawPos, visLineOffsetY), white_ColorId); + } } else if (isHint) { - drawRange_Text(d->font, drawPos, uiAnnotation_ColorId, range_String(&d->hint)); + if (flags_Widget(w) & alignRight_WidgetFlag) { + drawAlign_Text(d->font, + init_I2(right_Rect(contentBounds), drawPos.y), + uiAnnotation_ColorId, + right_Alignment, + "%s", + cstr_String(&d->hint)); + } + else { + drawRange_Text(d->font, drawPos, uiAnnotation_ColorId, range_String(&d->hint)); + } } else { iAssert(~d->inFlags & isSensitive_InputWidgetFlag || size_Range(&visLines) == 1); diff --git a/src/ui/labelwidget.c b/src/ui/labelwidget.c index f6ddfd43..9ef62262 100644 --- a/src/ui/labelwidget.c +++ b/src/ui/labelwidget.c @@ -44,14 +44,15 @@ struct Impl_LabelWidget { iString command; iClick click; struct { - uint8_t alignVisual : 1; /* align according to visible bounds, not font metrics */ - uint8_t noAutoMinHeight : 1; /* minimum height is not set automatically */ - uint8_t drawAsOutline : 1; /* draw as outline, filled with background color */ - uint8_t noTopFrame : 1; - uint8_t wrap : 1; - uint8_t allCaps : 1; - uint8_t removeTrailingColon : 1; - uint8_t chevron : 1; + uint16_t alignVisual : 1; /* align according to visible bounds, not font metrics */ + uint16_t noAutoMinHeight : 1; /* minimum height is not set automatically */ + uint16_t drawAsOutline : 1; /* draw as outline, filled with background color */ + uint16_t noTopFrame : 1; + uint16_t wrap : 1; + uint16_t allCaps : 1; + uint16_t removeTrailingColon : 1; + uint16_t chevron : 1; + uint16_t checkMark : 1; } flags; }; @@ -204,6 +205,9 @@ static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int *bg = isButton && ~flags & noBackground_WidgetFlag ? (d->widget.bgColor != none_ColorId ? d->widget.bgColor : uiBackground_ColorId) : none_ColorId; + if (d->flags.checkMark) { + *bg = none_ColorId; + } *fg = uiText_ColorId; *frame1 = isButton ? uiEmboss1_ColorId : d->widget.frameColor; *frame2 = isButton ? uiEmboss2_ColorId : *frame1; @@ -215,13 +219,15 @@ static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int *meta = uiTextDisabled_ColorId; } if (isSel) { - *bg = uiBackgroundSelected_ColorId; + if (!d->flags.checkMark) { + *bg = uiBackgroundSelected_ColorId; // if (!isKeyRoot) { // *bg = uiEmbossSelected1_ColorId; //uiBackgroundUnfocusedSelection_ColorId; // } - if (!isKeyRoot) { - *bg = isDark_ColorTheme(colorTheme_App()) ? uiBackgroundUnfocusedSelection_ColorId - : uiMarked_ColorId ; + if (!isKeyRoot) { + *bg = isDark_ColorTheme(colorTheme_App()) ? uiBackgroundUnfocusedSelection_ColorId + : uiMarked_ColorId; + } } *fg = uiTextSelected_ColorId; if (isButton) { @@ -429,14 +435,23 @@ static void draw_LabelWidget_(const iLabelWidget *d) { "%s", cstr_String(&d->label)); } - if (d->flags.chevron) { + if (d->flags.chevron || (flags & selected_WidgetFlag && d->flags.checkMark)) { const iRect chRect = rect; const int chSize = lineHeight_Text(d->font); + int offset = 0; + if (d->flags.chevron) { + offset = -iconPad; + } + else { + offset = -10 * gap_UI; + } drawCentered_Text(d->font, - (iRect){ addX_I2(topRight_Rect(chRect), -iconPad), + (iRect){ addX_I2(topRight_Rect(chRect), offset), init_I2(chSize, height_Rect(chRect)) }, - iTrue, iconColor, rightAngle_Icon); - } + iTrue, + iconColor, + d->flags.chevron ? rightAngle_Icon : check_Icon); + } unsetClip_Paint(&p); drawChildren_Widget(w); } @@ -580,6 +595,10 @@ void setChevron_LabelWidget(iLabelWidget *d, iBool chevron) { d->flags.chevron = chevron; } +void setCheckMark_LabelWidget(iLabelWidget *d, iBool checkMark) { + d->flags.checkMark = checkMark; +} + void setWrap_LabelWidget(iLabelWidget *d, iBool wrap) { d->flags.wrap = wrap; } diff --git a/src/ui/labelwidget.h b/src/ui/labelwidget.h index 6542ae12..6f1b7902 100644 --- a/src/ui/labelwidget.h +++ b/src/ui/labelwidget.h @@ -33,6 +33,7 @@ void setAlignVisually_LabelWidget(iLabelWidget *, iBool alignVisual); void setNoAutoMinHeight_LabelWidget (iLabelWidget *, iBool noAutoMinHeight); void setNoTopFrame_LabelWidget (iLabelWidget *, iBool noTopFrame); void setChevron_LabelWidget (iLabelWidget *, iBool chevron); +void setCheckMark_LabelWidget (iLabelWidget *, iBool checkMark); void setWrap_LabelWidget (iLabelWidget *, iBool wrap); void setOutline_LabelWidget (iLabelWidget *, iBool drawAsOutline); void setAllCaps_LabelWidget (iLabelWidget *, iBool allCaps); diff --git a/src/ui/mobile.c b/src/ui/mobile.c index 7b34d389..c7b75d3f 100644 --- a/src/ui/mobile.c +++ b/src/ui/mobile.c @@ -511,6 +511,7 @@ void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) { setId_Widget(as_Widget(drop), id); widget = makeValuePaddingWithHeading_(heading = makeHeading_Widget(label), as_Widget(drop)); setCommandHandler_Widget(widget, dropdownHeadingHandler_); + widget->padding[2] = gap_UI; setUserData_Object(widget, drop); } else if (equal_Command(spec, "radio") || equal_Command(spec, "buttons")) { @@ -534,7 +535,20 @@ void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) { resizeWidthOfChildren_WidgetFlag, iTrue); setId_Widget(widget, id); + iBool isFirst = iTrue; for (const iMenuItem *radioItem = item->data; radioItem->label; radioItem++) { + if (!isHorizontal && !isFirst) { + /* The separator is padded from the left so we need two. */ + iWidget *sep = new_Widget(); + iWidget *sep2 = new_Widget(); + addChildFlags_Widget(sep, iClob(sep2), 0); + setFlags_Widget(sep, arrangeHeight_WidgetFlag | resizeWidthOfChildren_WidgetFlag, iTrue); + setBackgroundColor_Widget(sep2, uiSeparator_ColorId); + setFixedSize_Widget(sep2, init_I2(-1, gap_UI / 4)); + setPadding_Widget(sep, 5 * gap_UI, 0, 0, 0); + addChildFlags_Widget(widget, iClob(sep), 0); + } + isFirst = iFalse; const char * radId = cstr_Rangecc(range_Command(radioItem->label, "id")); int64_t flags = noBackground_WidgetFlag | frameless_WidgetFlag; if (!isHorizontal) { @@ -554,10 +568,13 @@ void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) { button = (iLabelWidget *) makeToggle_Widget(radId); setTextCStr_LabelWidget(button, format_CStr("${%s}", radId)); setFlags_Widget(as_Widget(button), fixedWidth_WidgetFlag, iFalse); - updateSize_LabelWidget(button); } setId_Widget(as_Widget(button), radId); setFont_LabelWidget(button, isHorizontal ? uiLabelMedium_FontId : uiLabelBig_FontId); + setCheckMark_LabelWidget(button, !isHorizontal); + setPadding_Widget(as_Widget(button), gap_UI, 1 * gap_UI, 0, 1 * gap_UI); + updateSize_LabelWidget(button); + setPadding_Widget(widget, 0, 0, 0, 0); addChildFlags_Widget(widget, iClob(button), flags); } } @@ -575,6 +592,7 @@ void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) { setFlags_Widget(widget, expand_WidgetFlag, iTrue); } else { + setFlags_Widget(as_Widget(input), alignRight_WidgetFlag, iTrue); setContentPadding_InputWidget(input, 3 * gap_UI, 0); if (hasLabel_Command(spec, "unit")) { iWidget *unit = addChildFlags_Widget( diff --git a/src/ui/uploadwidget.c b/src/ui/uploadwidget.c index bad00071..c22bfe86 100644 --- a/src/ui/uploadwidget.c +++ b/src/ui/uploadwidget.c @@ -185,9 +185,9 @@ void init_UploadWidget(iUploadWidget *d) { { "label id:upload.info" }, { "panel id:dlg.upload.text icon:0x1f5b9 noscroll:1", 0, 0, (const void *) textItems }, { "panel id:dlg.upload.file icon:0x1f4c1", 0, 0, (const void *) fileItems }, - { "padding" }, - { "dropdown id:upload.id icon:0x1f464", 0, 0, constData_Array(makeIdentityItems_UploadWidget_(d)) }, - { "input id:upload.token hint:hint.upload.token icon:0x1f511" }, + { "heading text:${heading.upload.id}" }, + { "dropdown id:upload.id icon:0x1f464 text:", 0, 0, constData_Array(makeIdentityItems_UploadWidget_(d)) }, + { "input id:upload.token hint:hint.upload.token.long icon:0x1f516 text:" }, { NULL } }, actions, iElemCount(actions)); d->info = findChild_Widget(w, "upload.info"); -- cgit v1.2.3 From 50c5abbbeab07e3ccd67da5caafe7325467f1396 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 2 Dec 2021 15:25:22 +0200 Subject: Avoid capitalizing headings in .po files All-caps should be applied at runtime when appropriate. --- po/en.po | 24 ++++++++++++------------ res/lang/en.bin | Bin 26158 -> 26158 bytes res/lang/eo.bin | Bin 25112 -> 25112 bytes res/lang/es_MX.bin | Bin 27217 -> 27217 bytes res/lang/ia.bin | Bin 28217 -> 28217 bytes res/lang/isv.bin | Bin 24878 -> 24878 bytes res/lang/pl.bin | Bin 29493 -> 29493 bytes res/lang/sk.bin | Bin 25214 -> 25214 bytes res/lang/zh_Hans.bin | Bin 25112 -> 25112 bytes res/lang/zh_Hant.bin | Bin 25310 -> 25310 bytes src/ui/certlistwidget.c | 1 + src/ui/mobile.c | 15 ++++++++++++++- src/ui/root.c | 5 +++-- src/ui/util.c | 13 ++++++++----- 14 files changed, 38 insertions(+), 20 deletions(-) diff --git a/po/en.po b/po/en.po index 6675f78b..c6fa98be 100644 --- a/po/en.po +++ b/po/en.po @@ -982,7 +982,7 @@ msgid "menu.page.upload" msgstr "Upload Page with Titan…" msgid "heading.upload" -msgstr "UPLOAD WITH TITAN" +msgstr "Upload with Titan" msgid "upload.id" msgstr "Identity:" @@ -1239,7 +1239,7 @@ msgid "dlg.bookmark.icon" msgstr "Icon:" msgid "heading.bookmark.tags" -msgstr "SPECIAL TAGS" +msgstr "Special Tags" msgid "heading.addfolder" msgstr "ADD FOLDER" @@ -1254,14 +1254,14 @@ msgid "dlg.addfolder" msgstr "Add Folder" msgid "heading.prefs" -msgstr "PREFERENCES" +msgstr "Preferences" # used on mobile msgid "heading.settings" -msgstr "SETTINGS" +msgstr "Settings" msgid "heading.prefs.certs" -msgstr "CERTIFICATES" +msgstr "Certificates" # tab button msgid "heading.prefs.colors" @@ -1287,25 +1287,25 @@ msgid "heading.prefs.network" msgstr "Network" msgid "heading.prefs.paragraph" -msgstr "PARAGRAPH" +msgstr "Paragraph" msgid "heading.prefs.uitheme" -msgstr "UI COLORS" +msgstr "UI Colors" msgid "heading.prefs.pagecontent" -msgstr "PAGE COLORS" +msgstr "Page Colors" msgid "heading.prefs.proxies" -msgstr "PROXIES" +msgstr "Proxies" msgid "heading.prefs.scrolling" -msgstr "SCROLLING" +msgstr "Scrolling" msgid "heading.prefs.sizing" -msgstr "SIZING" +msgstr "Sizing" msgid "heading.prefs.widelayout" -msgstr "WIDE LAYOUT" +msgstr "Wide Layout" # tab button msgid "heading.prefs.style" diff --git a/res/lang/en.bin b/res/lang/en.bin index 0b2edc4b..0129d019 100644 Binary files a/res/lang/en.bin and b/res/lang/en.bin differ diff --git a/res/lang/eo.bin b/res/lang/eo.bin index 6c0dc697..4acb5f16 100644 Binary files a/res/lang/eo.bin and b/res/lang/eo.bin differ diff --git a/res/lang/es_MX.bin b/res/lang/es_MX.bin index 37438b3a..3fcecd44 100644 Binary files a/res/lang/es_MX.bin and b/res/lang/es_MX.bin differ diff --git a/res/lang/ia.bin b/res/lang/ia.bin index 1a512597..f9e134fc 100644 Binary files a/res/lang/ia.bin and b/res/lang/ia.bin differ diff --git a/res/lang/isv.bin b/res/lang/isv.bin index 58673ac5..6b98e1f8 100644 Binary files a/res/lang/isv.bin and b/res/lang/isv.bin differ diff --git a/res/lang/pl.bin b/res/lang/pl.bin index 0aaa2fbd..33ed890b 100644 Binary files a/res/lang/pl.bin and b/res/lang/pl.bin differ diff --git a/res/lang/sk.bin b/res/lang/sk.bin index db1bd9e7..6dbeb129 100644 Binary files a/res/lang/sk.bin and b/res/lang/sk.bin differ diff --git a/res/lang/zh_Hans.bin b/res/lang/zh_Hans.bin index ec375dcd..cfa284be 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 dd63c0eb..f0f6fe97 100644 Binary files a/res/lang/zh_Hant.bin and b/res/lang/zh_Hant.bin differ diff --git a/src/ui/certlistwidget.c b/src/ui/certlistwidget.c index 4d939ae2..c67203e3 100644 --- a/src/ui/certlistwidget.c +++ b/src/ui/certlistwidget.c @@ -388,6 +388,7 @@ void init_CertListWidget(iCertListWidget *d) { init_ListWidget(&d->list); setId_Widget(w, "certlist"); setBackgroundColor_Widget(w, none_ColorId); + setItemHeight_ListWidget(&d->list, 3.5f * lineHeight_Text(default_FontId)); d->itemFonts[0] = uiContent_FontId; d->itemFonts[1] = uiContentBold_FontId; #if defined (iPlatformMobile) diff --git a/src/ui/mobile.c b/src/ui/mobile.c index c7b75d3f..9e4fef84 100644 --- a/src/ui/mobile.c +++ b/src/ui/mobile.c @@ -23,6 +23,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "mobile.h" #include "app.h" +#include "certlistwidget.h" #include "command.h" #include "defs.h" #include "inputwidget.h" @@ -196,6 +197,8 @@ static iBool topPanelHandler_(iWidget *topPanel, const char *cmd) { setText_LabelWidget(detailTitle, text_LabelWidget((iLabelWidget *) findTitleLabel_(panel))); setFlags_Widget(button, selected_WidgetFlag, iTrue); postCommand_Widget(topPanel, "panel.changed arg:%d", panelIndex); + //printTree_Widget(findDetailStack_(topPanel)); +// updateVisible_ListWidget(findChild_Widget(findDetailStack_(topPanel), "certlist")); return iTrue; } if (equal_Command(cmd, "swipe.back")) { @@ -482,7 +485,7 @@ void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) { collapse_WidgetFlag); setFont_LabelWidget(title, uiLabelLargeBold_FontId); setTextColor_LabelWidget(title, uiHeading_ColorId); - setAllCaps_LabelWidget(title, iTrue); +// setAllCaps_LabelWidget(title, iTrue); setId_Widget(as_Widget(title), id); } else if (equal_Command(spec, "heading")) { @@ -609,6 +612,16 @@ void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) { setUserData_Object(widget, input); } } + else if (equal_Command(spec, "certlist")) { + iCertListWidget *certList = new_CertListWidget(); + iListWidget *list = (iListWidget *) certList; + setBackgroundColor_Widget(as_Widget(list), uiBackgroundSidebar_ColorId); + widget = as_Widget(certList); + updateItems_CertListWidget(certList); + invalidate_ListWidget(list); + setFixedSize_Widget(widget, + init_I2(-1, numItems_ListWidget(list) * itemHeight_ListWidget(list))); + } else if (equal_Command(spec, "button")) { widget = as_Widget(heading = makePanelButton_(label, item->command)); setFlags_Widget(widget, selected_WidgetFlag, argLabel_Command(spec, "selected") != 0); diff --git a/src/ui/root.c b/src/ui/root.c index 5ed6b529..b7a2e5c8 100644 --- a/src/ui/root.c +++ b/src/ui/root.c @@ -339,14 +339,15 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) { /* Current identity. */ const iString *docUrl = url_DocumentWidget(document_App()); const iGmIdentity *ident = identityForUrl_GmCerts(certs_App(), docUrl); - const iString *fp = collect_String(hexEncode_Block(&ident->fingerprint)); + const iString *fp = ident ? collect_String(hexEncode_Block(&ident->fingerprint)) : NULL; pushBackN_Array(&items, (iMenuItem[]){ { format_CStr("///" uiHeading_ColorEscape "%s", ident ? cstr_String(name_GmIdentity(ident)) : "${menu.identity.notactive}") }, { "---" } }, 2); - /* Alternate identities. */ { + /* Alternate identities. */ + if (ident) { const iString *site = collectNewRange_String(urlRoot_String(docUrl)); iBool haveAlts = iFalse; iConstForEach(StringArray, i, strings_SiteSpec(site, usedIdentities_SiteSpecKey)) { diff --git a/src/ui/util.c b/src/ui/util.c index 04483e8d..91b67e06 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -1995,9 +1995,11 @@ iWidget *appendTwoColumnTabPage_Widget(iWidget *tabs, const char *title, int sho } static void makeTwoColumnHeading_(const char *title, iWidget *headings, iWidget *values) { - addChildFlags_Widget(headings, - iClob(makeHeading_Widget(format_CStr(uiHeading_ColorEscape "%s", title))), - ignoreForParentWidth_WidgetFlag); + setFont_LabelWidget(addChildFlags_Widget(headings, + iClob(makeHeading_Widget( + format_CStr(uiHeading_ColorEscape "%s", title))), + ignoreForParentWidth_WidgetFlag), + uiLabelBold_FontId); addChild_Widget(values, iClob(makeHeading_Widget(""))); } @@ -2431,6 +2433,7 @@ iWidget *makePreferences_Widget(void) { }; const iMenuItem identityPanelItems[] = { { "title id:sidebar.identities" }, + { "certlist" }, { NULL } }; iString *aboutText = collectNew_String(); { @@ -2471,9 +2474,9 @@ iWidget *makePreferences_Widget(void) { return dlg; } iWidget *dlg = makeSheet_Widget("prefs"); - addChildFlags_Widget(dlg, + setAllCaps_LabelWidget(addChildFlags_Widget(dlg, iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.prefs}", NULL)), - frameless_WidgetFlag); + frameless_WidgetFlag), iTrue); iWidget *tabs = makeTabs_Widget(dlg); setBackgroundColor_Widget(findChild_Widget(tabs, "tabs.buttons"), uiBackgroundSidebar_ColorId); setId_Widget(tabs, "prefs.tabs"); -- cgit v1.2.3 From 8e7c651faa2e7525cc2e4251516585f89b822ab8 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 2 Dec 2021 19:33:57 +0200 Subject: Widget: Widget rearrangement issues There were issues where popup menus would only get wider and never narrower. Arrangements should not be dependent on the results of a previous one. --- src/ui/labelwidget.c | 4 ++-- src/ui/util.c | 50 +++++++++++++++++++++++++++++++------------------- src/ui/widget.c | 16 ++++++++++++++++ 3 files changed, 49 insertions(+), 21 deletions(-) diff --git a/src/ui/labelwidget.c b/src/ui/labelwidget.c index 9ef62262..947d5daa 100644 --- a/src/ui/labelwidget.c +++ b/src/ui/labelwidget.c @@ -491,9 +491,9 @@ int font_LabelWidget(const iLabelWidget *d) { } void updateSize_LabelWidget(iLabelWidget *d) { - iWidget *w = as_Widget(d); + iWidget *w = as_Widget(d); const int64_t flags = flags_Widget(w); - const iInt2 size = defaultSize_LabelWidget(d); + const iInt2 size = defaultSize_LabelWidget(d); if (!d->flags.noAutoMinHeight) { w->minSize.y = size.y; /* vertically text must remain visible */ } diff --git a/src/ui/util.c b/src/ui/util.c index 91b67e06..61d3e9bb 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -861,12 +861,8 @@ iWidget *makeMenu_Widget(iWidget *parent, const iMenuItem *items, size_t n) { setFlags_Widget(menu, keepOnTop_WidgetFlag | collapse_WidgetFlag | hidden_WidgetFlag | arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag | - resizeChildrenToWidestChild_WidgetFlag | overflowScrollable_WidgetFlag | - (isPortraitPhone_App() ? drawBackgroundToVerticalSafeArea_WidgetFlag : 0), + resizeChildrenToWidestChild_WidgetFlag | overflowScrollable_WidgetFlag, iTrue); - if (!isPortraitPhone_App()) { - setFrameColor_Widget(menu, uiBackgroundSelected_ColorId); - } makeMenuItems_Widget(menu, items, n); addChild_Widget(parent, menu); iRelease(menu); /* owned by parent now */ @@ -884,6 +880,7 @@ void openMenu_Widget(iWidget *d, iInt2 windowCoord) { static void updateMenuItemFonts_Widget_(iWidget *d) { const iBool isPortraitPhone = (deviceType_App() == phone_AppDeviceType && isPortrait_App()); + const iBool isMobile = (deviceType_App() != desktop_AppDeviceType); const iBool isSlidePanel = (flags_Widget(d) & horizontalOffset_WidgetFlag) != 0; iForEach(ObjectList, i, children_Widget(d)) { if (isInstance_Object(i.object, &Class_LabelWidget)) { @@ -895,14 +892,14 @@ static void updateMenuItemFonts_Widget_(iWidget *d) { if (deviceType_App() == desktop_AppDeviceType) { setFont_LabelWidget(label, isCaution ? uiLabelBold_FontId : uiLabel_FontId); } - else if (isPortraitPhone) { - if (!isSlidePanel) { - setFont_LabelWidget(label, isCaution ? uiLabelBigBold_FontId : uiLabelBig_FontId); - } - } - else { - setFont_LabelWidget(label, isCaution ? uiContentBold_FontId : uiContent_FontId); + else { //if (isPortraitPhone) { + //if (!isSlidePanel) { + setFont_LabelWidget(label, isCaution ? uiLabelBigBold_FontId : uiLabelBig_FontId); + // } } +// else { +// setFont_LabelWidget(label, isCaution ? uiContentBold_FontId : uiContent_FontId); +// } } else if (childCount_Widget(i.object)) { updateMenuItemFonts_Widget_(i.object); @@ -1034,6 +1031,12 @@ void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, int menuOpenFlags) { setFlags_Widget(d, hidden_WidgetFlag, iFalse); setFlags_Widget(d, commandOnMouseMiss_WidgetFlag, iTrue); setFlags_Widget(findChild_Widget(d, "menu.cancel"), disabled_WidgetFlag, iFalse); + if (!isPortraitPhone) { + setFrameColor_Widget(d, uiBackgroundSelected_ColorId); + } + else { + setFrameColor_Widget(d, none_ColorId); + } arrange_Widget(d); /* need to know the height */ iBool allowOverflow = iFalse; /* A vertical offset determined by a possible selected label in the menu. */ @@ -1104,13 +1107,22 @@ void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, int menuOpenFlags) { } #endif raise_Widget(d); - if (isPortraitPhone) { - setFlags_Widget(d, arrangeWidth_WidgetFlag | resizeChildrenToWidestChild_WidgetFlag, iFalse); - setFlags_Widget(d, resizeWidthOfChildren_WidgetFlag | drawBackgroundToBottom_WidgetFlag, iTrue); - if (!isSlidePanel) { - setFlags_Widget(d, borderTop_WidgetFlag, iTrue); + if (deviceType_App() != desktop_AppDeviceType) { + setFlags_Widget(d, arrangeWidth_WidgetFlag | resizeChildrenToWidestChild_WidgetFlag, + !isPortraitPhone); + setFlags_Widget(d, + resizeWidthOfChildren_WidgetFlag | drawBackgroundToBottom_WidgetFlag | + drawBackgroundToVerticalSafeArea_WidgetFlag, + isPortraitPhone); + if (isPortraitPhone) { + if (!isSlidePanel) { + setFlags_Widget(d, borderTop_WidgetFlag, iTrue); + } + d->rect.size.x = rootSize.x; + } + else { + d->rect.size.x = 0; } - d->rect.size.x = rootSize.x; } updateMenuItemFonts_Widget_(d); arrange_Widget(d); @@ -2458,7 +2470,7 @@ iWidget *makePreferences_Widget(void) { { "title id:heading.settings" }, { "panel text:" gear_Icon " ${heading.prefs.general}", 0, 0, (const void *) generalPanelItems }, { "panel icon:0x1f5a7 id:heading.prefs.network", 0, 0, (const void *) networkPanelItems }, - { "panel text:" person_Icon " ${sidebar.identities}", 0, 0, (const void *) identityPanelItems }, + { "panel noscroll:1 text:" person_Icon " ${sidebar.identities}", 0, 0, (const void *) identityPanelItems }, { "padding" }, { "panel icon:0x1f4f1 id:heading.prefs.interface", 0, 0, (const void *) uiPanelItems }, { "panel icon:0x1f3a8 id:heading.prefs.colors", 0, 0, (const void *) colorPanelItems }, diff --git a/src/ui/widget.c b/src/ui/widget.c index cedda461..254c2590 100644 --- a/src/ui/widget.c +++ b/src/ui/widget.c @@ -31,6 +31,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "util.h" #include "window.h" +#include "labelwidget.h" + #include #include #include @@ -836,6 +838,12 @@ static void arrange_Widget_(iWidget *d) { } static void resetArrangement_Widget_(iWidget *d) { + if (d->flags & resizeToParentWidth_WidgetFlag) { + d->rect.size.x = 0; + } + if (d->flags & resizeToParentHeight_WidgetFlag) { + d->rect.size.y = 0; + } iForEach(ObjectList, i, children_Widget(d)) { iWidget *child = as_Widget(i.object); resetArrangement_Widget_(child); @@ -847,6 +855,14 @@ static void resetArrangement_Widget_(iWidget *d) { ~child->flags & fixedWidth_WidgetFlag) { child->rect.size.x = 0; } + if (d->flags & resizeChildrenToWidestChild_WidgetFlag) { + if (isInstance_Object(child, &Class_LabelWidget)) { + updateSize_LabelWidget((iLabelWidget *) child); + } + else { + child->rect.size.x = 0; + } + } if (d->flags & arrangeVertical_WidgetFlag) { child->rect.pos.y = 0; } -- cgit v1.2.3 From aa7d9295e56c87802f2bfd3b6b69a56f351aabeb Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 2 Dec 2021 19:34:30 +0200 Subject: VisBuf: Respect the Paint origin --- src/ui/paint.c | 7 ++++++- src/ui/paint.h | 1 + src/ui/visbuf.c | 3 +++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/ui/paint.c b/src/ui/paint.c index b92be27e..439bdd37 100644 --- a/src/ui/paint.c +++ b/src/ui/paint.c @@ -41,6 +41,7 @@ void init_Paint(iPaint *d) { d->dst = get_Window(); d->setTarget = NULL; d->oldTarget = NULL; + d->oldOrigin = zero_I2(); d->alpha = 255; } @@ -48,6 +49,8 @@ void beginTarget_Paint(iPaint *d, SDL_Texture *target) { SDL_Renderer *rend = renderer_Paint_(d); if (!d->setTarget) { d->oldTarget = SDL_GetRenderTarget(rend); + d->oldOrigin = origin_Paint; + origin_Paint = zero_I2(); SDL_SetRenderTarget(rend, target); d->setTarget = target; } @@ -59,8 +62,10 @@ void beginTarget_Paint(iPaint *d, SDL_Texture *target) { void endTarget_Paint(iPaint *d) { if (d->setTarget) { SDL_SetRenderTarget(renderer_Paint_(d), d->oldTarget); + origin_Paint = d->oldOrigin; + d->oldOrigin = zero_I2(); d->oldTarget = NULL; - d->setTarget = NULL; + d->setTarget = NULL; } } diff --git a/src/ui/paint.h b/src/ui/paint.h index e894b62f..dfc9260d 100644 --- a/src/ui/paint.h +++ b/src/ui/paint.h @@ -33,6 +33,7 @@ struct Impl_Paint { iWindow * dst; SDL_Texture *setTarget; SDL_Texture *oldTarget; + iInt2 oldOrigin; uint8_t alpha; }; diff --git a/src/ui/visbuf.c b/src/ui/visbuf.c index 8f7a4c46..0097b12a 100644 --- a/src/ui/visbuf.c +++ b/src/ui/visbuf.c @@ -21,6 +21,7 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "visbuf.h" +#include "paint.h" #include "window.h" #include "util.h" @@ -224,6 +225,8 @@ void draw_VisBuf(const iVisBuf *d, const iInt2 topLeft, const iRangei yClipBound continue; /* Outside the clipping area. */ #endif } + dst.x += origin_Paint.x; + dst.y += origin_Paint.y; #if defined (DEBUG_SCALE) dst.w *= DEBUG_SCALE; dst.h *= DEBUG_SCALE; -- cgit v1.2.3 From ba90c9f0b61419b4d8878276be898cc0012a37a0 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 2 Dec 2021 19:34:42 +0200 Subject: Mobile: Layout of the Identities list --- src/ui/mobile.c | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/ui/mobile.c b/src/ui/mobile.c index 9e4fef84..5382cbce 100644 --- a/src/ui/mobile.c +++ b/src/ui/mobile.c @@ -100,6 +100,16 @@ static iWidget *findTitleLabel_(iWidget *panel) { return NULL; } +static void updateCertListHeight_(iWidget *detailStack) { + iWidget *certList = findChild_Widget(detailStack, "certlist"); + if (certList) { + setFixedSize_Widget(certList, + init_I2(-1, + -1 * gap_UI + bottom_Rect(safeRect_Root(certList->root)) - + top_Rect(boundsWithoutVisualOffset_Widget(certList)))); + } +} + static iBool mainDetailSplitHandler_(iWidget *mainDetailSplit, const char *cmd) { if (equal_Command(cmd, "window.resized")) { const iBool isPortrait = (deviceType_App() == phone_AppDeviceType && isPortrait_App()); @@ -147,6 +157,7 @@ static iBool mainDetailSplitHandler_(iWidget *mainDetailSplit, const char *cmd) setPadding_Widget(panel, pad, 0, pad, pad); } arrange_Widget(mainDetailSplit); + updateCertListHeight_(detailStack); } else if (equal_Command(cmd, "mouse.clicked") && arg_Command(cmd)) { if (focus_Widget() && class_Widget(focus_Widget()) == &Class_InputWidget) { @@ -199,6 +210,7 @@ static iBool topPanelHandler_(iWidget *topPanel, const char *cmd) { postCommand_Widget(topPanel, "panel.changed arg:%d", panelIndex); //printTree_Widget(findDetailStack_(topPanel)); // updateVisible_ListWidget(findChild_Widget(findDetailStack_(topPanel), "certlist")); + updateCertListHeight_(findDetailStack_(topPanel)); return iTrue; } if (equal_Command(cmd, "swipe.back")) { @@ -619,8 +631,6 @@ void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) { widget = as_Widget(certList); updateItems_CertListWidget(certList); invalidate_ListWidget(list); - setFixedSize_Widget(widget, - init_I2(-1, numItems_ListWidget(list) * itemHeight_ListWidget(list))); } else if (equal_Command(spec, "button")) { widget = as_Widget(heading = makePanelButton_(label, item->command)); @@ -885,18 +895,21 @@ void setupMenuTransition_Mobile(iWidget *sheet, iBool isIncoming) { if (!isUsingPanelLayout_Mobile()) { return; } - const iBool isSlidePanel = (flags_Widget(sheet) & horizontalOffset_WidgetFlag) != 0; - if (isSlidePanel && isLandscape_App()) { + const iBool isHorizPanel = (flags_Widget(sheet) & horizontalOffset_WidgetFlag) != 0; + if (isHorizPanel && isLandscape_App()) { return; } + const int maxOffset = isHorizPanel ? width_Widget(sheet) + : isPortraitPhone_App() ? height_Widget(sheet) + : (12 * gap_UI); if (isIncoming) { - setVisualOffset_Widget(sheet, isSlidePanel ? width_Widget(sheet) : height_Widget(sheet), 0, 0); + setVisualOffset_Widget(sheet, maxOffset, 0, 0); setVisualOffset_Widget(sheet, 0, 330, easeOut_AnimFlag | softer_AnimFlag); } else { const iBool wasDragged = iAbs(value_Anim(&sheet->visualOffset) - 0) > 1; setVisualOffset_Widget(sheet, - isSlidePanel ? width_Widget(sheet) : height_Widget(sheet), + maxOffset, wasDragged ? 100 : 200, wasDragged ? 0 : easeIn_AnimFlag | softer_AnimFlag); } -- cgit v1.2.3 From 6e917280380316eba77b1dfa983daf488510e70f Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 3 Dec 2021 07:46:16 +0200 Subject: Mobile: Manage Identities via Settings --- po/en.po | 5 ++- res/lang/cs.bin | Bin 31054 -> 31087 bytes res/lang/de.bin | Bin 30061 -> 30094 bytes res/lang/en.bin | Bin 26158 -> 26191 bytes res/lang/eo.bin | Bin 25112 -> 25145 bytes res/lang/es.bin | Bin 29885 -> 29918 bytes res/lang/es_MX.bin | Bin 27217 -> 27250 bytes res/lang/fi.bin | Bin 29718 -> 29751 bytes res/lang/fr.bin | Bin 30865 -> 30898 bytes res/lang/gl.bin | Bin 29070 -> 29103 bytes res/lang/hu.bin | Bin 30890 -> 30923 bytes res/lang/ia.bin | Bin 28217 -> 28250 bytes res/lang/ie.bin | Bin 28805 -> 28838 bytes res/lang/isv.bin | Bin 24878 -> 24911 bytes res/lang/pl.bin | Bin 29493 -> 29526 bytes res/lang/ru.bin | Bin 44253 -> 44286 bytes res/lang/sk.bin | Bin 25214 -> 25247 bytes res/lang/sr.bin | Bin 43679 -> 43712 bytes res/lang/tok.bin | Bin 26927 -> 26960 bytes res/lang/tr.bin | Bin 29111 -> 29144 bytes res/lang/uk.bin | Bin 43598 -> 43631 bytes res/lang/zh_Hans.bin | Bin 25112 -> 25145 bytes res/lang/zh_Hant.bin | Bin 25310 -> 25343 bytes src/app.c | 8 +++++ src/ui/labelwidget.c | 17 ++++++---- src/ui/labelwidget.h | 1 + src/ui/mobile.c | 11 ++++--- src/ui/mobile.h | 1 + src/ui/root.c | 88 +++++++++++++++++++++++++++++++++++-------------- src/ui/sidebarwidget.c | 20 ++++++----- src/ui/uploadwidget.c | 7 +++- src/ui/util.c | 9 +++++ src/ui/util.h | 3 +- src/ui/widget.c | 10 +++--- 34 files changed, 129 insertions(+), 51 deletions(-) diff --git a/po/en.po b/po/en.po index c6fa98be..4e013941 100644 --- a/po/en.po +++ b/po/en.po @@ -395,6 +395,9 @@ msgstr "New Identity…" msgid "menu.identity.import" msgstr "Import…" +msgid "menu.identities" +msgstr "Manage Identities" + msgid "menu.identity.notactive" msgstr "No Active Identity" @@ -988,7 +991,7 @@ msgid "upload.id" msgstr "Identity:" msgid "heading.upload.id" -msgstr "Identification" +msgstr "Authorization" msgid "dlg.upload.id.none" msgstr "None" diff --git a/res/lang/cs.bin b/res/lang/cs.bin index 8f8b3a6f..7c08bcfe 100644 Binary files a/res/lang/cs.bin and b/res/lang/cs.bin differ diff --git a/res/lang/de.bin b/res/lang/de.bin index 2df7f11f..e3f45a34 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 0129d019..0c65ffd3 100644 Binary files a/res/lang/en.bin and b/res/lang/en.bin differ diff --git a/res/lang/eo.bin b/res/lang/eo.bin index 4acb5f16..9edecafe 100644 Binary files a/res/lang/eo.bin and b/res/lang/eo.bin differ diff --git a/res/lang/es.bin b/res/lang/es.bin index 5d04443c..9e7d0585 100644 Binary files a/res/lang/es.bin and b/res/lang/es.bin differ diff --git a/res/lang/es_MX.bin b/res/lang/es_MX.bin index 3fcecd44..1dd95a4e 100644 Binary files a/res/lang/es_MX.bin and b/res/lang/es_MX.bin differ diff --git a/res/lang/fi.bin b/res/lang/fi.bin index 53449c30..1d5e4624 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 2ccda4da..c0202489 100644 Binary files a/res/lang/fr.bin and b/res/lang/fr.bin differ diff --git a/res/lang/gl.bin b/res/lang/gl.bin index 9f37b384..34d16341 100644 Binary files a/res/lang/gl.bin and b/res/lang/gl.bin differ diff --git a/res/lang/hu.bin b/res/lang/hu.bin index 4de98a70..2ab5484b 100644 Binary files a/res/lang/hu.bin and b/res/lang/hu.bin differ diff --git a/res/lang/ia.bin b/res/lang/ia.bin index f9e134fc..c43aec10 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 3324bb69..8a1d71e4 100644 Binary files a/res/lang/ie.bin and b/res/lang/ie.bin differ diff --git a/res/lang/isv.bin b/res/lang/isv.bin index 6b98e1f8..e9978815 100644 Binary files a/res/lang/isv.bin and b/res/lang/isv.bin differ diff --git a/res/lang/pl.bin b/res/lang/pl.bin index 33ed890b..b6de49f1 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 82c43ffc..7f153025 100644 Binary files a/res/lang/ru.bin and b/res/lang/ru.bin differ diff --git a/res/lang/sk.bin b/res/lang/sk.bin index 6dbeb129..35fdb2b1 100644 Binary files a/res/lang/sk.bin and b/res/lang/sk.bin differ diff --git a/res/lang/sr.bin b/res/lang/sr.bin index 7f058106..bd9ca450 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 206fe02c..cb34ada6 100644 Binary files a/res/lang/tok.bin and b/res/lang/tok.bin differ diff --git a/res/lang/tr.bin b/res/lang/tr.bin index 430ab02c..99dc6431 100644 Binary files a/res/lang/tr.bin and b/res/lang/tr.bin differ diff --git a/res/lang/uk.bin b/res/lang/uk.bin index 49d58f42..edcafb7e 100644 Binary files a/res/lang/uk.bin and b/res/lang/uk.bin differ diff --git a/res/lang/zh_Hans.bin b/res/lang/zh_Hans.bin index cfa284be..488cc64f 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 f0f6fe97..edab200b 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 540c46d8..5b2e88bf 100644 --- a/src/app.c +++ b/src/app.c @@ -2667,6 +2667,9 @@ iBool handleCommand_App(const char *cmd) { if (!urlArg) { return iTrue; /* invalid command */ } + if (findWidget_App("prefs")) { + postCommand_App("prefs.dismiss"); + } iString *url = collectNewCStr_String(urlArg); const iBool noProxy = argLabel_Command(cmd, "noproxy") != 0; const iBool fromSidebar = argLabel_Command(cmd, "fromsidebar") != 0; @@ -2979,6 +2982,11 @@ iBool handleCommand_App(const char *cmd) { showTabPage_Widget(tabs, tabPage_Widget(tabs, d->prefs.dialogTab)); } setCommandHandler_Widget(dlg, handlePrefsCommands_); + if (argLabel_Command(cmd, "idents") && deviceType_App() != desktop_AppDeviceType) { + iWidget *idPanel = panel_Mobile(dlg, 2); + iWidget *button = findUserData_Widget(findChild_Widget(dlg, "panel.top"), idPanel); + postCommand_Widget(button, "panel.open"); + } } else if (equal_Command(cmd, "navigate.home")) { /* Look for bookmarks tagged "homepage". */ diff --git a/src/ui/labelwidget.c b/src/ui/labelwidget.c index 947d5daa..d00733e1 100644 --- a/src/ui/labelwidget.c +++ b/src/ui/labelwidget.c @@ -36,6 +36,7 @@ struct Impl_LabelWidget { iWidget widget; iString srcLabel; iString label; + iInt2 labelOffset; int font; int key; int kmods; @@ -362,10 +363,6 @@ static void draw_LabelWidget_(const iLabelWidget *d) { } setClip_Paint(&p, rect); const int iconPad = iconPadding_LabelWidget_(d); -// const int iconColor = isCaution ? uiTextCaution_ColorId -// : flags & (disabled_WidgetFlag | pressed_WidgetFlag) ? fg -// : isHover ? uiIconHover_ColorId -// : uiIcon_ColorId; if (d->icon && d->icon != 0x20) { /* no need to draw an empty icon */ iString str; initUnicodeN_String(&str, &d->icon, 1); @@ -427,8 +424,11 @@ static void draw_LabelWidget_(const iLabelWidget *d) { else { drawCenteredOutline_Text( d->font, - adjusted_Rect(bounds, init_I2(iconPad * (flags & tight_WidgetFlag ? 1.0f : 1.5f), 0), - init_I2(-iconPad * (flags & tight_WidgetFlag ? 0.5f : 1.0f), 0)), + moved_Rect( + adjusted_Rect(bounds, + init_I2(iconPad * (flags & tight_WidgetFlag ? 1.0f : 1.5f), 0), + init_I2(-iconPad * (flags & tight_WidgetFlag ? 0.5f : 1.0f), 0)), + d->labelOffset), d->flags.alignVisual, d->flags.drawAsOutline ? fg : none_ColorId, d->flags.drawAsOutline ? d->widget.bgColor : fg, @@ -523,6 +523,7 @@ void init_LabelWidget(iLabelWidget *d, const char *label, const char *cmd) { d->font = uiLabel_FontId; d->forceFg = none_ColorId; d->icon = 0; + d->labelOffset = zero_I2(); initCStr_String(&d->srcLabel, label); initCopy_String(&d->label, &d->srcLabel); replaceVariables_LabelWidget_(d); @@ -623,6 +624,10 @@ void setRemoveTrailingColon_LabelWidget(iLabelWidget *d, iBool removeTrailingCol } } +void setTextOffset_LabelWidget(iLabelWidget *d, iInt2 offset) { + d->labelOffset = offset; +} + void updateText_LabelWidget(iLabelWidget *d, const iString *text) { set_String(&d->label, text); set_String(&d->srcLabel, text); diff --git a/src/ui/labelwidget.h b/src/ui/labelwidget.h index 6f1b7902..4f605d6b 100644 --- a/src/ui/labelwidget.h +++ b/src/ui/labelwidget.h @@ -38,6 +38,7 @@ void setWrap_LabelWidget (iLabelWidget *, iBool wrap); void setOutline_LabelWidget (iLabelWidget *, iBool drawAsOutline); void setAllCaps_LabelWidget (iLabelWidget *, iBool allCaps); void setRemoveTrailingColon_LabelWidget (iLabelWidget *, iBool removeTrailingColon); +void setTextOffset_LabelWidget (iLabelWidget *, iInt2 offset); void setFont_LabelWidget (iLabelWidget *, int fontId); void setTextColor_LabelWidget (iLabelWidget *, int color); void setText_LabelWidget (iLabelWidget *, const iString *text); /* resizes widget */ diff --git a/src/ui/mobile.c b/src/ui/mobile.c index 5382cbce..ab282a86 100644 --- a/src/ui/mobile.c +++ b/src/ui/mobile.c @@ -180,13 +180,16 @@ size_t currentPanelIndex_Mobile(const iWidget *panels) { return iInvalidPos; } +iWidget *panel_Mobile(const iWidget *panels, size_t index) { + return child_Widget(findChild_Widget(panels, "detailstack"), index); +} + static iBool topPanelHandler_(iWidget *topPanel, const char *cmd) { const iBool isPortrait = !isSideBySideLayout_(); if (equal_Command(cmd, "panel.open")) { - iWidget *button = pointer_Command(cmd); + /* This command is sent by the button that opens the panel. */ + iWidget *button = pointer_Command(cmd); iWidget *panel = userData_Object(button); -// openMenu_Widget(panel, innerToWindow_Widget(panel, zero_I2())); -// setFlags_Widget(panel, hidden_WidgetFlag, iFalse); unselectAllPanelButtons_(topPanel); int panelIndex = -1; size_t childIndex = 0; @@ -208,8 +211,6 @@ static iBool topPanelHandler_(iWidget *topPanel, const char *cmd) { setText_LabelWidget(detailTitle, text_LabelWidget((iLabelWidget *) findTitleLabel_(panel))); setFlags_Widget(button, selected_WidgetFlag, iTrue); postCommand_Widget(topPanel, "panel.changed arg:%d", panelIndex); - //printTree_Widget(findDetailStack_(topPanel)); -// updateVisible_ListWidget(findChild_Widget(findDetailStack_(topPanel), "certlist")); updateCertListHeight_(findDetailStack_(topPanel)); return iTrue; } diff --git a/src/ui/mobile.h b/src/ui/mobile.h index 9d7ac8e4..06955945 100644 --- a/src/ui/mobile.h +++ b/src/ui/mobile.h @@ -39,6 +39,7 @@ void initPanels_Mobile (iWidget *panels, iWidget *parentWidget, const iMenuItem *itemsNullTerminated, const iMenuItem *actions, size_t numActions); +iWidget * panel_Mobile (const iWidget *panels, size_t index); size_t currentPanelIndex_Mobile (const iWidget *panels); enum iTransitionFlags { diff --git a/src/ui/root.c b/src/ui/root.c index b7a2e5c8..5df4b36f 100644 --- a/src/ui/root.c +++ b/src/ui/root.c @@ -333,19 +333,28 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) { } else if (equal_Command(cmd, "identmenu.open")) { iWidget *toolBar = findWidget_Root("toolbar"); - iWidget *button = findWidget_Root(toolBar ? "toolbar.ident" : "navbar.ident"); + iWidget *button = findWidget_Root(toolBar && isPortraitPhone_App() ? "toolbar.ident" : "navbar.ident"); iArray items; init_Array(&items, sizeof(iMenuItem)); /* Current identity. */ const iString *docUrl = url_DocumentWidget(document_App()); const iGmIdentity *ident = identityForUrl_GmCerts(certs_App(), docUrl); const iString *fp = ident ? collect_String(hexEncode_Block(&ident->fingerprint)) : NULL; - pushBackN_Array(&items, - (iMenuItem[]){ { format_CStr("///" uiHeading_ColorEscape "%s", - ident ? cstr_String(name_GmIdentity(ident)) - : "${menu.identity.notactive}") }, - { "---" } }, - 2); + iString *str = NULL; + if (ident) { + str = copy_String(name_GmIdentity(ident)); + if (!isEmpty_String(&ident->notes)) { + appendFormat_String(str, "\n%s%s", escape_Color(uiAnnotation_ColorId), + cstr_String(&ident->notes)); + } + } + pushBackN_Array( + &items, + (iMenuItem[]){ { format_CStr("///" uiHeading_ColorEscape "%s", + str ? cstr_String(str) : "${menu.identity.notactive}") }, + { "---" } }, + 2); + delete_String(str); /* Alternate identities. */ if (ident) { const iString *site = collectNewRange_String(urlRoot_String(docUrl)); @@ -385,8 +394,8 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) { : leftHalf_Icon " ${menu.show.identities}", 0, 0, - deviceType_App() == phone_AppDeviceType ? "toolbar.showident" - : "sidebar.mode arg:3 toggle:1" }); + //deviceType_App() == phone_AppDeviceType ? "toolbar.showident" + "sidebar.mode arg:3 toggle:1" }); } else { pushBack_Array(&items, &(iMenuItem){ gear_Icon " ${menu.identities}", 0, 0, @@ -517,9 +526,21 @@ static void updateNavBarIdentity_(iWidget *navBar) { iLabelWidget *toolName = findWidget_App("toolbar.name"); if (toolName) { setOutline_LabelWidget(toolButton, ident == NULL); - updateTextCStr_LabelWidget(toolName, subjectName ? cstr_String(subjectName) : ""); + /* Fit the name in the widget. */ + if (subjectName) { + const char *endPos; + tryAdvanceNoWrap_Text(uiLabelTiny_FontId, range_String(subjectName), width_Widget(toolName), + &endPos); + updateText_LabelWidget( + toolName, + collectNewRange_String((iRangecc){ constBegin_String(subjectName), endPos })); + } + else { + updateTextCStr_LabelWidget(toolName, ""); + } setFont_LabelWidget(toolButton, subjectName ? uiLabelMedium_FontId : uiLabelLarge_FontId); - arrange_Widget(parent_Widget(toolButton)); + setTextOffset_LabelWidget(toolButton, init_I2(0, subjectName ? -1.5f * gap_UI : 0)); + arrange_Widget(parent_Widget(toolButton)); } } @@ -604,11 +625,13 @@ void updateToolbarColors_Root(iRoot *d) { tmBannerBackground_ColorId; setBackgroundColor_Widget(toolBar, bg); iForEach(ObjectList, i, children_Widget(toolBar)) { - iLabelWidget *btn = i.object; +// iLabelWidget *btn = i.object; setTextColor_LabelWidget(i.object, isSidebarVisible ? uiTextDim_ColorId : tmBannerIcon_ColorId); setBackgroundColor_Widget(i.object, bg); /* using noBackground, but ident has outline */ } + setTextColor_LabelWidget(findChild_Widget(toolBar, "toolbar.name"), + isSidebarVisible ? uiTextDim_ColorId : tmBannerIcon_ColorId); } #else iUnused(d); @@ -1014,8 +1037,13 @@ static iBool handleToolBarCommands_(iWidget *toolBar, const char *cmd) { return iTrue; } else if (equal_Command(cmd, "toolbar.showident")) { - /* TODO: Clean this up. */ iWidget *sidebar = findWidget_App("sidebar"); + if (isVisible_Widget(sidebar)) { + postCommandf_App("sidebar.toggle"); + } + postCommand_App("preferences idents:1"); +#if 0 + /* TODO: Clean this up. */ iWidget *sidebar2 = findWidget_App("sidebar2"); //dismissSidebar_(sidebar, "toolbar.view"); if (isVisible_Widget(sidebar)) { @@ -1036,6 +1064,7 @@ static iBool handleToolBarCommands_(iWidget *toolBar, const char *cmd) { setVisualOffset_Widget(sidebar2, offset, 0, 0); setVisualOffset_Widget(sidebar2, 0, 400, easeOut_AnimFlag | softer_AnimFlag); } +#endif return iTrue; } else if (equal_Command(cmd, "sidebar.mode.changed")) { @@ -1099,15 +1128,16 @@ void updateMetrics_Root(iRoot *d) { const iWidget *toolBar = findChild_Widget(d->widget, "toolbar"); const iWidget *viewButton = findChild_Widget(d->widget, "toolbar.view"); const iWidget *idButton = findChild_Widget(toolBar, "toolbar.ident"); - const int font = uiLabelTiny_FontId; - setFont_LabelWidget(idName, font); - setPos_Widget(as_Widget(idName), +// const int font = uiLabelTiny_FontId; + setFixedSize_Widget(as_Widget(idName), init_I2(-1, 2 * gap_UI + lineHeight_Text(uiLabelTiny_FontId))); +// setFont_LabelWidget(idName, font); + /*setPos_Widget(as_Widget(idName), windowToLocal_Widget(as_Widget(idName), init_I2(left_Rect(bounds_Widget(idButton)), bottom_Rect(bounds_Widget(viewButton)) - lineHeight_Text(font) - gap_UI / 2))); setFixedSize_Widget(as_Widget(idName), init_I2(width_Widget(idButton), - lineHeight_Text(font))); + lineHeight_Text(font)));*/ } } postRefresh_App(); @@ -1413,15 +1443,17 @@ void createUserInterface_Root(iRoot *d) { iWidget *content = findChild_Widget(root, "tabs.content"); iSidebarWidget *sidebar1 = new_SidebarWidget(left_SidebarSide); addChildPos_Widget(content, iClob(sidebar1), front_WidgetAddPos); - iSidebarWidget *sidebar2 = new_SidebarWidget(right_SidebarSide); if (deviceType_App() != phone_AppDeviceType) { + iSidebarWidget *sidebar2 = new_SidebarWidget(right_SidebarSide); addChildPos_Widget(content, iClob(sidebar2), back_WidgetAddPos); } +#if 0 else { /* The identities sidebar is always in the main area. */ addChild_Widget(findChild_Widget(root, "stack"), iClob(sidebar2)); setFlags_Widget(as_Widget(sidebar2), hidden_WidgetFlag, iTrue); } +#endif } /* Lookup results. */ { iLookupWidget *lookup = new_LookupWidget(); @@ -1481,23 +1513,29 @@ void createUserInterface_Root(iRoot *d) { iClob(newLargeIcon_LabelWidget(forwardArrow_Icon, "navigate.forward")), frameless_WidgetFlag), "toolbar.forward"); - setId_Widget(addChildFlags_Widget(toolBar, - iClob(newLargeIcon_LabelWidget("\U0001f464", "identmenu.open")), - frameless_WidgetFlag), + iWidget *identButton; + setId_Widget(identButton = addChildFlags_Widget( + toolBar, + iClob(newLargeIcon_LabelWidget("\U0001f464", "identmenu.open")), + frameless_WidgetFlag | fixedHeight_WidgetFlag), "toolbar.ident"); setId_Widget(addChildFlags_Widget(toolBar, iClob(newLargeIcon_LabelWidget(book_Icon, "toolbar.showview arg:-1")), frameless_WidgetFlag | commandOnClick_WidgetFlag), "toolbar.view"); - setId_Widget(addChildFlags_Widget(toolBar, - iClob(new_LabelWidget("", "toolbar.showident")), + iLabelWidget *idName; + setId_Widget(addChildFlags_Widget(identButton, + iClob(idName = new_LabelWidget("", NULL)), frameless_WidgetFlag | noBackground_WidgetFlag | - fixedPosition_WidgetFlag | + moveToParentBottomEdge_WidgetFlag | + resizeToParentWidth_WidgetFlag + /*fixedPosition_WidgetFlag | fixedSize_WidgetFlag | ignoreForParentWidth_WidgetFlag | - ignoreForParentHeight_WidgetFlag), + ignoreForParentHeight_WidgetFlag*/), "toolbar.name"); + setFont_LabelWidget(idName, uiLabelTiny_FontId); iLabelWidget *menuButton = makeMenuButton_LabelWidget(menu_Icon, phoneNavMenuItems_, iElemCount(phoneNavMenuItems_)); setFont_LabelWidget(menuButton, uiLabelLarge_FontId); diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index 674e2b7c..9b94f4d9 100644 --- a/src/ui/sidebarwidget.c +++ b/src/ui/sidebarwidget.c @@ -608,8 +608,10 @@ iBool setMode_SidebarWidget(iSidebarWidget *d, enum iSidebarMode mode) { } void setClosedFolders_SidebarWidget(iSidebarWidget *d, const iIntSet *closedFolders) { - delete_IntSet(d->closedFolders); - d->closedFolders = copy_IntSet(closedFolders); + if (d) { + delete_IntSet(d->closedFolders); + d->closedFolders = copy_IntSet(closedFolders); + } } enum iSidebarMode mode_SidebarWidget(const iSidebarWidget *d) { @@ -625,7 +627,7 @@ float width_SidebarWidget(const iSidebarWidget *d) { } const iIntSet *closedFolders_SidebarWidget(const iSidebarWidget *d) { - return d->closedFolders; + return d ? d->closedFolders : collect_IntSet(new_IntSet()); } static const char *normalModeLabels_[max_SidebarMode] = { @@ -710,7 +712,8 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) { setId_Widget(buttons, "buttons"); setDrawBufferEnabled_Widget(buttons, iTrue); for (int i = 0; i < max_SidebarMode; i++) { - if (deviceType_App() == phone_AppDeviceType && i == identities_SidebarMode) { + if (i == identities_SidebarMode && deviceType_App() != desktop_AppDeviceType) { + /* On mobile, identities are managed via Settings. */ continue; } d->modeButtons[i] = addChildFlags_Widget( @@ -911,6 +914,7 @@ static void checkModeButtonLayout_SidebarWidget_(iSidebarWidget *d) { } void setWidth_SidebarWidget(iSidebarWidget *d, float widthAsGaps) { + if (!d) return; iWidget *w = as_Widget(d); const iBool isFixedWidth = deviceType_App() == phone_AppDeviceType; int width = widthAsGaps * gap_UI; /* in pixels */ @@ -1148,10 +1152,10 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) } } } - else if (deviceType_App() == tablet_AppDeviceType && equal_Command(cmd, "toolbar.showident")) { - postCommandf_App("sidebar.mode arg:%d toggle:1", identities_SidebarMode); - return iTrue; - } +// else if (deviceType_App() == tablet_AppDeviceType && equal_Command(cmd, "toolbar.showident")) { +// postCommandf_App("sidebar.mode arg:%d toggle:1", identities_SidebarMode); +// return iTrue; +// } else if (isPortraitPhone_App() && isVisible_Widget(w) && d->side == left_SidebarSide && equal_Command(cmd, "swipe.forward")) { postCommand_App("sidebar.toggle"); diff --git a/src/ui/uploadwidget.c b/src/ui/uploadwidget.c index c22bfe86..89376633 100644 --- a/src/ui/uploadwidget.c +++ b/src/ui/uploadwidget.c @@ -123,9 +123,14 @@ static const iArray *makeIdentityItems_UploadWidget_(const iUploadWidget *d) { pushBack_Array(items, &(iMenuItem){ "---" }); iConstForEach(PtrArray, i, listIdentities_GmCerts(certs_App(), NULL, NULL)) { const iGmIdentity *id = i.ptr; + iString *str = collect_String(copy_String(name_GmIdentity(id))); + if (!isEmpty_String(&id->notes)) { + appendFormat_String( + str, "\n%s%s", escape_Color(uiAnnotation_ColorId), cstr_String(&id->notes)); + } pushBack_Array( items, - &(iMenuItem){ cstr_String(name_GmIdentity(id)), 0, 0, + &(iMenuItem){ cstr_String(str), 0, 0, format_CStr("upload.setid fp:%s", cstrCollect_String(hexEncode_Block(&id->fingerprint))) }); } diff --git a/src/ui/util.c b/src/ui/util.c index 61d3e9bb..de838769 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -1215,6 +1215,15 @@ iLabelWidget *findMenuItem_Widget(iWidget *menu, const char *command) { return NULL; } +iWidget *findUserData_Widget(iWidget *d, void *userData) { + iForEach(ObjectList, i, children_Widget(d)) { + if (userData_Object(i.object) == userData) { + return i.object; + } + } + return NULL; +} + void setMenuItemDisabled_Widget(iWidget *menu, const char *command, iBool disable) { if (flags_Widget(menu) & nativeMenu_WidgetFlag) { setDisabled_NativeMenuItem(findNativeMenuItem_Widget(menu, command), disable); diff --git a/src/ui/util.h b/src/ui/util.h index 81fb1cbd..7ee94f1d 100644 --- a/src/ui/util.h +++ b/src/ui/util.h @@ -249,7 +249,8 @@ void setMenuItemDisabled_Widget (iWidget *menu, const char *comm void setMenuItemDisabledByIndex_Widget(iWidget *menu, size_t index, iBool disable); void setMenuItemLabel_Widget (iWidget *menu, const char *command, const char *newLabel); void setMenuItemLabelByIndex_Widget (iWidget *menu, size_t index, const char *newLabel); -void setNativeMenuItems_Widget (iWidget *, const iMenuItem *items, size_t n); +void setNativeMenuItems_Widget (iWidget *menu, const iMenuItem *items, size_t n); +iWidget * findUserData_Widget (iWidget *, void *userData); int checkContextMenu_Widget (iWidget *, const SDL_Event *ev); /* see macro below */ diff --git a/src/ui/widget.c b/src/ui/widget.c index 254c2590..210fe899 100644 --- a/src/ui/widget.c +++ b/src/ui/widget.c @@ -271,10 +271,12 @@ void setMinSize_Widget(iWidget *d, iInt2 minSize) { } void setPadding_Widget(iWidget *d, int left, int top, int right, int bottom) { - d->padding[0] = left; - d->padding[1] = top; - d->padding[2] = right; - d->padding[3] = bottom; + if (d) { + d->padding[0] = left; + d->padding[1] = top; + d->padding[2] = right; + d->padding[3] = bottom; + } } iWidget *root_Widget(const iWidget *d) { -- cgit v1.2.3 From ad86fbe2cffa2bea07d004782e711932c5c91a79 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 3 Dec 2021 10:41:15 +0200 Subject: Improving identity use The URL checks for determining which identity was in use were a bit too page-specific. Now the URL prefix usage is applied more consistently. The identity toolbar menu is now more useful in that it allows both switching identities and stopping the use of the current identity. --- po/en.po | 2 +- res/lang/cs.bin | Bin 31087 -> 31081 bytes res/lang/de.bin | Bin 30094 -> 30088 bytes res/lang/en.bin | Bin 26191 -> 26185 bytes res/lang/eo.bin | Bin 25145 -> 25139 bytes res/lang/es.bin | Bin 29918 -> 29912 bytes res/lang/es_MX.bin | Bin 27250 -> 27244 bytes res/lang/fi.bin | Bin 29751 -> 29745 bytes res/lang/fr.bin | Bin 30898 -> 30892 bytes res/lang/gl.bin | Bin 29103 -> 29097 bytes res/lang/hu.bin | Bin 30923 -> 30917 bytes res/lang/ia.bin | Bin 28250 -> 28244 bytes res/lang/ie.bin | Bin 28838 -> 28832 bytes res/lang/isv.bin | Bin 24911 -> 24905 bytes res/lang/pl.bin | Bin 29526 -> 29520 bytes res/lang/ru.bin | Bin 44286 -> 44280 bytes res/lang/sk.bin | Bin 25247 -> 25241 bytes res/lang/sr.bin | Bin 43712 -> 43706 bytes res/lang/tok.bin | Bin 26960 -> 26954 bytes res/lang/tr.bin | Bin 29144 -> 29138 bytes res/lang/uk.bin | Bin 43631 -> 43625 bytes res/lang/zh_Hans.bin | Bin 25145 -> 25139 bytes res/lang/zh_Hant.bin | Bin 25343 -> 25337 bytes src/app.c | 4 ++- src/gmcerts.c | 16 +++++++++- src/ui/certlistwidget.c | 13 ++++---- src/ui/documentwidget.c | 9 ++++-- src/ui/root.c | 78 ++++++++++++++++++++++++++---------------------- src/ui/sidebarwidget.c | 59 +++++++++++++++++++----------------- 29 files changed, 108 insertions(+), 73 deletions(-) diff --git a/po/en.po b/po/en.po index 4e013941..6b604d61 100644 --- a/po/en.po +++ b/po/en.po @@ -639,7 +639,7 @@ msgstr "Export" # The %s represents the name of an identity. #, c-format msgid "ident.switch" -msgstr "Switch to %s" +msgstr "Use %s" msgid "heading.ident.use" msgstr "IDENTITY USAGE" diff --git a/res/lang/cs.bin b/res/lang/cs.bin index 7c08bcfe..cf37c7de 100644 Binary files a/res/lang/cs.bin and b/res/lang/cs.bin differ diff --git a/res/lang/de.bin b/res/lang/de.bin index e3f45a34..a9d497d7 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 0c65ffd3..5e24ab94 100644 Binary files a/res/lang/en.bin and b/res/lang/en.bin differ diff --git a/res/lang/eo.bin b/res/lang/eo.bin index 9edecafe..789c01c2 100644 Binary files a/res/lang/eo.bin and b/res/lang/eo.bin differ diff --git a/res/lang/es.bin b/res/lang/es.bin index 9e7d0585..7ca8e297 100644 Binary files a/res/lang/es.bin and b/res/lang/es.bin differ diff --git a/res/lang/es_MX.bin b/res/lang/es_MX.bin index 1dd95a4e..43a5bb77 100644 Binary files a/res/lang/es_MX.bin and b/res/lang/es_MX.bin differ diff --git a/res/lang/fi.bin b/res/lang/fi.bin index 1d5e4624..5a0ec586 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 c0202489..329cae2b 100644 Binary files a/res/lang/fr.bin and b/res/lang/fr.bin differ diff --git a/res/lang/gl.bin b/res/lang/gl.bin index 34d16341..527de83b 100644 Binary files a/res/lang/gl.bin and b/res/lang/gl.bin differ diff --git a/res/lang/hu.bin b/res/lang/hu.bin index 2ab5484b..82257541 100644 Binary files a/res/lang/hu.bin and b/res/lang/hu.bin differ diff --git a/res/lang/ia.bin b/res/lang/ia.bin index c43aec10..5f1d7ab2 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 8a1d71e4..0d866d47 100644 Binary files a/res/lang/ie.bin and b/res/lang/ie.bin differ diff --git a/res/lang/isv.bin b/res/lang/isv.bin index e9978815..d30dcf4f 100644 Binary files a/res/lang/isv.bin and b/res/lang/isv.bin differ diff --git a/res/lang/pl.bin b/res/lang/pl.bin index b6de49f1..e9fd8460 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 7f153025..f8a69f51 100644 Binary files a/res/lang/ru.bin and b/res/lang/ru.bin differ diff --git a/res/lang/sk.bin b/res/lang/sk.bin index 35fdb2b1..9d0e9f04 100644 Binary files a/res/lang/sk.bin and b/res/lang/sk.bin differ diff --git a/res/lang/sr.bin b/res/lang/sr.bin index bd9ca450..bbaba1e9 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 cb34ada6..517194d1 100644 Binary files a/res/lang/tok.bin and b/res/lang/tok.bin differ diff --git a/res/lang/tr.bin b/res/lang/tr.bin index 99dc6431..e0aef190 100644 Binary files a/res/lang/tr.bin and b/res/lang/tr.bin differ diff --git a/res/lang/uk.bin b/res/lang/uk.bin index edcafb7e..0c261c00 100644 Binary files a/res/lang/uk.bin and b/res/lang/uk.bin differ diff --git a/res/lang/zh_Hans.bin b/res/lang/zh_Hans.bin index 488cc64f..d31b280b 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 edab200b..e06f4339 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 5b2e88bf..587efd00 100644 --- a/src/app.c +++ b/src/app.c @@ -3142,6 +3142,7 @@ iBool handleCommand_App(const char *cmd) { d->certs, findIdentity_GmCerts(d->certs, collect_Block(hexDecode_Rangecc(range_Command(cmd, "ident")))), url); + postCommand_App("navigate.reload"); postCommand_App("idents.changed"); return iTrue; } @@ -3154,6 +3155,7 @@ iBool handleCommand_App(const char *cmd) { else { setUse_GmIdentity(ident, collect_String(suffix_Command(cmd, "url")), iFalse); } + postCommand_App("navigate.reload"); postCommand_App("idents.changed"); return iTrue; } @@ -3164,7 +3166,7 @@ iBool handleCommand_App(const char *cmd) { const iGmIdentity *cur = identityForUrl_GmCerts(d->certs, docUrl); iGmIdentity *dst = findIdentity_GmCerts( d->certs, collect_Block(hexDecode_Rangecc(range_Command(cmd, "fp")))); - if (cur && dst && cur != dst) { + if (dst && cur != dst) { iString *useUrl = copy_String(findUse_GmIdentity(cur, docUrl)); if (isEmpty_String(useUrl)) { useUrl = copy_String(docUrl); diff --git a/src/gmcerts.c b/src/gmcerts.c index 345c36e0..ed4759be 100644 --- a/src/gmcerts.c +++ b/src/gmcerts.c @@ -146,6 +146,7 @@ iBool isUsed_GmIdentity(const iGmIdentity *d) { } iBool isUsedOn_GmIdentity(const iGmIdentity *d, const iString *url) { +#if 0 size_t pos = iInvalidPos; locate_StringSet(d->useUrls, url, &pos); if (pos < size_StringSet(d->useUrls)) { @@ -159,6 +160,12 @@ iBool isUsedOn_GmIdentity(const iGmIdentity *d, const iString *url) { return iTrue; } } +#endif + iConstForEach(StringSet, i, d->useUrls) { + if (startsWithCase_String(url, cstr_String(i.value))) { + return iTrue; + } + } return iFalse; } @@ -193,7 +200,13 @@ void setUse_GmIdentity(iGmIdentity *d, const iString *url, iBool use) { iAssert(wasInserted); } else { - remove_StringSet(d->useUrls, url); + iForEach(Array, i, &d->useUrls->strings.values) { + iString *used = i.value; + if (startsWithCase_String(url, cstr_String(used))) { + deinit_String(used); + remove_ArrayIterator(&i); + } + } } } @@ -202,6 +215,7 @@ void clearUse_GmIdentity(iGmIdentity *d) { } const iString *findUse_GmIdentity(const iGmIdentity *d, const iString *url) { + if (!d) return NULL; iConstForEach(StringSet, using, d->useUrls) { if (startsWith_String(url, cstr_String(using.value))) { return using.value; diff --git a/src/ui/certlistwidget.c b/src/ui/certlistwidget.c index c67203e3..7c140c19 100644 --- a/src/ui/certlistwidget.c +++ b/src/ui/certlistwidget.c @@ -113,7 +113,7 @@ static void updateContextMenu_CertListWidget_(iCertListWidget *d) { iBool usedOnCurrentPage = iFalse; iConstForEach(StringSet, i, ident->useUrls) { const iString *url = i.value; - usedOnCurrentPage |= equalCase_String(docUrl, url); + usedOnCurrentPage |= startsWithCase_String(docUrl, cstr_String(url)); iRangecc urlStr = range_String(url); if (startsWith_Rangecc(urlStr, "gemini://")) { urlStr.start += 9; /* omit the default scheme */ @@ -128,6 +128,9 @@ static void updateContextMenu_CertListWidget_(iCertListWidget *d) { if (!usedOnCurrentPage) { remove_Array(items, 1); } + else { + remove_Array(items, 0); + } } destroy_Widget(d->menu); d->menu = makeMenu_Widget(as_Widget(d), data_Array(items), size_Array(items)); @@ -166,8 +169,8 @@ static iBool processEvent_CertListWidget_(iCertListWidget *d, const SDL_Event *e return iTrue; } else if (isCommand_Widget(w, ev, "ident.use")) { - iGmIdentity * ident = menuIdentity_CertListWidget_(d); - const iString *tabUrl = url_DocumentWidget(document_App()); + iGmIdentity *ident = menuIdentity_CertListWidget_(d); + const iString *tabUrl = urlQueryStripped_String(url_DocumentWidget(document_App())); if (ident) { if (argLabel_Command(cmd, "clear")) { clearUse_GmIdentity(ident); @@ -388,7 +391,6 @@ void init_CertListWidget(iCertListWidget *d) { init_ListWidget(&d->list); setId_Widget(w, "certlist"); setBackgroundColor_Widget(w, none_ColorId); - setItemHeight_ListWidget(&d->list, 3.5f * lineHeight_Text(default_FontId)); d->itemFonts[0] = uiContent_FontId; d->itemFonts[1] = uiContentBold_FontId; #if defined (iPlatformMobile) @@ -397,6 +399,7 @@ void init_CertListWidget(iCertListWidget *d) { d->itemFonts[1] = uiLabelBigBold_FontId; } #endif + updateItemHeight_CertListWidget(d); d->menu = NULL; d->contextItem = NULL; d->contextIndex = iInvalidPos; @@ -443,7 +446,7 @@ iBool updateItems_CertListWidget(iCertListWidget *d) { cstr_String(&ident->notes)); } item->listItem.isSelected = isActive; - if (isUsedOnDomain_GmIdentity(ident, tabHost)) { + if (!isActive && isUsedOnDomain_GmIdentity(ident, tabHost)) { item->indent = 1; /* will be highlighted */ } addItem_ListWidget(&d->list, item); diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 3fb8498b..95286566 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -1289,8 +1289,13 @@ static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode if (category_GmStatusCode(code) == categoryClientCertificate_GmStatus) { makeFooterButtons_DocumentWidget_( d, - (iMenuItem[]){ { leftHalf_Icon " ${menu.show.identities}", '4', KMOD_PRIMARY, "sidebar.mode arg:3 show:1" }, - { person_Icon " ${menu.identity.new}", newIdentity_KeyShortcut, "ident.new" } }, + (iMenuItem[]){ + { leftHalf_Icon " ${menu.show.identities}", + '4', + KMOD_PRIMARY, + deviceType_App() == desktop_AppDeviceType ? "sidebar.mode arg:3 show:1" + : "preferences idents:1" }, + { person_Icon " ${menu.identity.new}", newIdentity_KeyShortcut, "ident.new" } }, 2); } /* Make a new document for the error page.*/ diff --git a/src/ui/root.c b/src/ui/root.c index 5df4b36f..f722df94 100644 --- a/src/ui/root.c +++ b/src/ui/root.c @@ -344,41 +344,47 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) { if (ident) { str = copy_String(name_GmIdentity(ident)); if (!isEmpty_String(&ident->notes)) { - appendFormat_String(str, "\n%s%s", escape_Color(uiAnnotation_ColorId), - cstr_String(&ident->notes)); + appendFormat_String(str, "\n\x1b[0m" uiHeading_ColorEscape "%s", cstr_String(&ident->notes)); } } - pushBackN_Array( + pushBack_Array( &items, - (iMenuItem[]){ { format_CStr("///" uiHeading_ColorEscape "%s", - str ? cstr_String(str) : "${menu.identity.notactive}") }, - { "---" } }, - 2); + &(iMenuItem){ format_CStr("```" uiHeading_ColorEscape "\x1b[1m%s", + str ? cstr_String(str) : "${menu.identity.notactive}") }); + if (ident && isUsedOn_GmIdentity(ident, docUrl)) { + pushBack_Array(&items, + &(iMenuItem){ close_Icon " ${ident.stopuse}", + 0, + 0, + format_CStr("ident.signout ident:%s url:%s", + cstr_String(fp), + cstr_String(docUrl)) }); + } + pushBack_Array(&items, &(iMenuItem){ "---" }); delete_String(str); /* Alternate identities. */ - if (ident) { - const iString *site = collectNewRange_String(urlRoot_String(docUrl)); - iBool haveAlts = iFalse; - iConstForEach(StringArray, i, strings_SiteSpec(site, usedIdentities_SiteSpecKey)) { - if (!equal_String(i.value, fp)) { - const iBlock *otherFp = collect_Block(hexDecode_Rangecc(range_String(i.value))); - const iGmIdentity *other = findIdentity_GmCerts(certs_App(), otherFp); - if (other) { - pushBack_Array( - &items, - &(iMenuItem){ - format_CStr(translateCStr_Lang("\U0001f816 ${ident.switch}"), - cstr_String(name_GmIdentity(other))), - 0, - 0, - format_CStr("ident.switch fp:%s", cstr_String(i.value)) }); - haveAlts = iTrue; - } + const iString *site = collectNewRange_String(urlRoot_String(docUrl)); + iBool haveAlts = iFalse; + iConstForEach(StringArray, i, strings_SiteSpec(site, usedIdentities_SiteSpecKey)) { + if (!fp || !equal_String(i.value, fp)) { + const iBlock *otherFp = collect_Block(hexDecode_Rangecc(range_String(i.value))); + const iGmIdentity *other = findIdentity_GmCerts(certs_App(), otherFp); + if (other && other != ident) { + pushBack_Array( + &items, + &(iMenuItem){ + format_CStr(translateCStr_Lang("\U0001f816 ${ident.switch}"), + format_CStr("\x1b[1m%s", + cstr_String(name_GmIdentity(other)))), + 0, + 0, + format_CStr("ident.switch fp:%s", cstr_String(i.value)) }); + haveAlts = iTrue; } } - if (haveAlts) { - pushBack_Array(&items, &(iMenuItem){ "---" }); - } + } + if (haveAlts) { + pushBack_Array(&items, &(iMenuItem){ "---" }); } iSidebarWidget *sidebar = findWidget_App("sidebar"); pushBackN_Array( @@ -388,14 +394,14 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) { { "${menu.identity.import}", SDLK_i, KMOD_PRIMARY | KMOD_SHIFT, "ident.import" }, { "---" } }, 3); if (deviceType_App() == desktop_AppDeviceType) { - pushBack_Array(&items, &(iMenuItem) - { isVisible_Widget(sidebar) && mode_SidebarWidget(sidebar) == identities_SidebarMode - ? leftHalf_Icon " ${menu.hide.identities}" - : leftHalf_Icon " ${menu.show.identities}", - 0, - 0, - //deviceType_App() == phone_AppDeviceType ? "toolbar.showident" - "sidebar.mode arg:3 toggle:1" }); + pushBack_Array(&items, + &(iMenuItem){ isVisible_Widget(sidebar) && mode_SidebarWidget(sidebar) == + identities_SidebarMode + ? leftHalf_Icon " ${menu.hide.identities}" + : leftHalf_Icon " ${menu.show.identities}", + 0, + 0, + "sidebar.mode arg:3 toggle:1" }); } else { pushBack_Array(&items, &(iMenuItem){ gear_Icon " ${menu.identities}", 0, 0, diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index 9b94f4d9..13fc33b1 100644 --- a/src/ui/sidebarwidget.c +++ b/src/ui/sidebarwidget.c @@ -591,6 +591,9 @@ iBool setMode_SidebarWidget(iSidebarWidget *d, enum iSidebarMode mode) { if (d->mode == mode) { return iFalse; } + if (mode == identities_SidebarMode && deviceType_App() != desktop_AppDeviceType) { + return iFalse; /* Identities are in Settings. */ + } if (d->mode >= 0 && d->mode < max_SidebarMode) { d->modeScroll[d->mode] = scrollPos_ListWidget(list_SidebarWidget_(d)); /* saved for later */ } @@ -705,33 +708,34 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) { d->certList = NULL; d->actions = NULL; d->closedFolders = new_IntSet(); - /* On a phone, the right sidebar is used exclusively for Identities. */ + /* On a phone, the right sidebar is not used. */ const iBool isPhone = deviceType_App() == phone_AppDeviceType; - if (!isPhone || d->side == left_SidebarSide) { - iWidget *buttons = new_Widget(); - setId_Widget(buttons, "buttons"); - setDrawBufferEnabled_Widget(buttons, iTrue); - for (int i = 0; i < max_SidebarMode; i++) { - if (i == identities_SidebarMode && deviceType_App() != desktop_AppDeviceType) { - /* On mobile, identities are managed via Settings. */ - continue; - } - d->modeButtons[i] = addChildFlags_Widget( - buttons, - iClob(new_LabelWidget( - tightModeLabels_[i], - format_CStr("%s.mode arg:%d", cstr_String(id_Widget(w)), i))), - frameless_WidgetFlag | noBackground_WidgetFlag); - } - setButtonFont_SidebarWidget(d, isPhone ? uiLabelBig_FontId : uiLabel_FontId); - addChildFlags_Widget(vdiv, - iClob(buttons), - arrangeHorizontal_WidgetFlag | - resizeWidthOfChildren_WidgetFlag | - arrangeHeight_WidgetFlag | resizeToParentWidth_WidgetFlag); // | -// drawBackgroundToHorizontalSafeArea_WidgetFlag); - setBackgroundColor_Widget(buttons, uiBackgroundSidebar_ColorId); + //if (!isPhone || d->side == left_SidebarSide) { + iWidget *buttons = new_Widget(); + setId_Widget(buttons, "buttons"); + setDrawBufferEnabled_Widget(buttons, iTrue); + for (int i = 0; i < max_SidebarMode; i++) { + if (i == identities_SidebarMode && deviceType_App() != desktop_AppDeviceType) { + /* On mobile, identities are managed via Settings. */ + continue; + } + d->modeButtons[i] = addChildFlags_Widget( + buttons, + iClob(new_LabelWidget( + tightModeLabels_[i], + format_CStr("%s.mode arg:%d", cstr_String(id_Widget(w)), i))), + frameless_WidgetFlag | noBackground_WidgetFlag); } + setButtonFont_SidebarWidget(d, isPhone ? uiLabelBig_FontId : uiLabel_FontId); + addChildFlags_Widget(vdiv, + iClob(buttons), + arrangeHorizontal_WidgetFlag | + resizeWidthOfChildren_WidgetFlag | + arrangeHeight_WidgetFlag | resizeToParentWidth_WidgetFlag); // | +// drawBackgroundToHorizontalSafeArea_WidgetFlag); + setBackgroundColor_Widget(buttons, uiBackgroundSidebar_ColorId); +// } +#if 0 else { iLabelWidget *heading = new_LabelWidget(person_Icon " ${sidebar.identities}", NULL); checkIcon_LabelWidget(heading); @@ -742,6 +746,7 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) { drawBackgroundToHorizontalSafeArea_WidgetFlag), uiLabelLargeBold_FontId); } +#endif iWidget *content = new_Widget(); setFlags_Widget(content, resizeChildren_WidgetFlag, iTrue); iWidget *listAndActions = makeVDiv_Widget(); @@ -771,8 +776,8 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) { addChildFlags_Widget(content, iClob(d->blank), resizeChildren_WidgetFlag); addChildFlags_Widget(vdiv, iClob(content), expand_WidgetFlag); setMode_SidebarWidget(d, - deviceType_App() == phone_AppDeviceType && d->side == right_SidebarSide ? - identities_SidebarMode : bookmarks_SidebarMode); + /*deviceType_App() == phone_AppDeviceType && d->side == right_SidebarSide ? + identities_SidebarMode :*/ bookmarks_SidebarMode); d->resizer = addChildFlags_Widget(w, iClob(new_Widget()), -- cgit v1.2.3 From 723fcbf263bd2f5ff6c259a47091ebf685b50723 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 4 Dec 2021 07:40:56 +0200 Subject: Mobile: Sidebar is now a vertically sliding panel Switched the phone sidebar to use the iOS half/full-height sliding sheet design. This is better for finger reachability and for retaining access to the current page. --- po/en.po | 3 + res/lang/cs.bin | Bin 31081 -> 31100 bytes res/lang/de.bin | Bin 30088 -> 30107 bytes res/lang/en.bin | Bin 26185 -> 26204 bytes res/lang/eo.bin | Bin 25139 -> 25158 bytes res/lang/es.bin | Bin 29912 -> 29931 bytes res/lang/es_MX.bin | Bin 27244 -> 27263 bytes res/lang/fi.bin | Bin 29745 -> 29764 bytes res/lang/fr.bin | Bin 30892 -> 30911 bytes res/lang/gl.bin | Bin 29097 -> 29116 bytes res/lang/hu.bin | Bin 30917 -> 30936 bytes res/lang/ia.bin | Bin 28244 -> 28263 bytes res/lang/ie.bin | Bin 28832 -> 28851 bytes res/lang/isv.bin | Bin 24905 -> 24924 bytes res/lang/pl.bin | Bin 29520 -> 29539 bytes res/lang/ru.bin | Bin 44280 -> 44299 bytes res/lang/sk.bin | Bin 25241 -> 25260 bytes res/lang/sr.bin | Bin 43706 -> 43725 bytes res/lang/tok.bin | Bin 26954 -> 26973 bytes res/lang/tr.bin | Bin 29138 -> 29157 bytes res/lang/uk.bin | Bin 43625 -> 43644 bytes res/lang/zh_Hans.bin | Bin 25139 -> 25158 bytes res/lang/zh_Hant.bin | Bin 25337 -> 25356 bytes src/ui/documentwidget.c | 6 +- src/ui/listwidget.c | 35 ++++++- src/ui/listwidget.h | 8 ++ src/ui/root.c | 106 ++++++++----------- src/ui/sidebarwidget.c | 271 ++++++++++++++++++++++++++++++++++++++---------- src/ui/sidebarwidget.h | 1 + src/ui/touch.c | 37 +++++-- src/ui/touch.h | 1 + src/ui/widget.c | 1 + src/ui/widget.h | 5 + 33 files changed, 343 insertions(+), 131 deletions(-) diff --git a/po/en.po b/po/en.po index 6b604d61..6996d28c 100644 --- a/po/en.po +++ b/po/en.po @@ -401,6 +401,9 @@ msgstr "Manage Identities" msgid "menu.identity.notactive" msgstr "No Active Identity" +msgid "sidebar.close" +msgstr "Done" + msgid "sidebar.bookmarks" msgstr "Bookmarks" diff --git a/res/lang/cs.bin b/res/lang/cs.bin index cf37c7de..a8981bd4 100644 Binary files a/res/lang/cs.bin and b/res/lang/cs.bin differ diff --git a/res/lang/de.bin b/res/lang/de.bin index a9d497d7..6d17ff3b 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 5e24ab94..4749f358 100644 Binary files a/res/lang/en.bin and b/res/lang/en.bin differ diff --git a/res/lang/eo.bin b/res/lang/eo.bin index 789c01c2..b3e06797 100644 Binary files a/res/lang/eo.bin and b/res/lang/eo.bin differ diff --git a/res/lang/es.bin b/res/lang/es.bin index 7ca8e297..2265ddf1 100644 Binary files a/res/lang/es.bin and b/res/lang/es.bin differ diff --git a/res/lang/es_MX.bin b/res/lang/es_MX.bin index 43a5bb77..810bc543 100644 Binary files a/res/lang/es_MX.bin and b/res/lang/es_MX.bin differ diff --git a/res/lang/fi.bin b/res/lang/fi.bin index 5a0ec586..ccc61bd4 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 329cae2b..355073bd 100644 Binary files a/res/lang/fr.bin and b/res/lang/fr.bin differ diff --git a/res/lang/gl.bin b/res/lang/gl.bin index 527de83b..30c5fb5f 100644 Binary files a/res/lang/gl.bin and b/res/lang/gl.bin differ diff --git a/res/lang/hu.bin b/res/lang/hu.bin index 82257541..90f32f47 100644 Binary files a/res/lang/hu.bin and b/res/lang/hu.bin differ diff --git a/res/lang/ia.bin b/res/lang/ia.bin index 5f1d7ab2..a9672e07 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 0d866d47..058ab7ff 100644 Binary files a/res/lang/ie.bin and b/res/lang/ie.bin differ diff --git a/res/lang/isv.bin b/res/lang/isv.bin index d30dcf4f..ccf1f206 100644 Binary files a/res/lang/isv.bin and b/res/lang/isv.bin differ diff --git a/res/lang/pl.bin b/res/lang/pl.bin index e9fd8460..d49b1fdd 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 f8a69f51..cb2baebc 100644 Binary files a/res/lang/ru.bin and b/res/lang/ru.bin differ diff --git a/res/lang/sk.bin b/res/lang/sk.bin index 9d0e9f04..50ee11ce 100644 Binary files a/res/lang/sk.bin and b/res/lang/sk.bin differ diff --git a/res/lang/sr.bin b/res/lang/sr.bin index bbaba1e9..d27c1c48 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 517194d1..89e9526c 100644 Binary files a/res/lang/tok.bin and b/res/lang/tok.bin differ diff --git a/res/lang/tr.bin b/res/lang/tr.bin index e0aef190..ef7f8f61 100644 Binary files a/res/lang/tr.bin and b/res/lang/tr.bin differ diff --git a/res/lang/uk.bin b/res/lang/uk.bin index 0c261c00..cff8fb7d 100644 Binary files a/res/lang/uk.bin and b/res/lang/uk.bin differ diff --git a/res/lang/zh_Hans.bin b/res/lang/zh_Hans.bin index d31b280b..e8dafce4 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 e06f4339..898bbddc 100644 Binary files a/res/lang/zh_Hant.bin and b/res/lang/zh_Hant.bin differ diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 95286566..0ef80690 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -2620,9 +2620,9 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { /* The temporary "swipeIn" will display the previous page until the finger is lifted. */ iDocumentWidget *swipeIn = findChild_Widget(swipeParent, "swipein"); if (!swipeIn) { - const iBool sidebarSwipe = (isPortraitPhone_App() && + const iBool sidebarSwipe = iFalse; /* && (isPortraitPhone_App() && d->flags & openedFromSidebar_DocumentWidgetFlag && - !isVisible_Widget(findWidget_App("sidebar"))); + !isVisible_Widget(findWidget_App("sidebar"))); */ swipeIn = new_DocumentWidget(); setId_Widget(as_Widget(swipeIn), "swipein"); setFlags_Widget(as_Widget(swipeIn), @@ -3235,6 +3235,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) return iTrue; } else if (equal_Command(cmd, "navigate.back") && document_App() == d) { +#if 0 if (isPortraitPhone_App()) { if (d->flags & openedFromSidebar_DocumentWidgetFlag && !isVisible_Widget(findWidget_App("sidebar"))) { @@ -3247,6 +3248,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) } d->flags &= ~openedFromSidebar_DocumentWidgetFlag; } +#endif if (d->request) { postCommandf_Root(w->root, "document.request.cancelled doc:%p url:%s", d, cstr_String(d->mod.url)); diff --git a/src/ui/listwidget.c b/src/ui/listwidget.c index 82e4e451..d12afc4c 100644 --- a/src/ui/listwidget.c +++ b/src/ui/listwidget.c @@ -81,6 +81,7 @@ void init_ListWidget(iListWidget *d) { setThumb_ScrollWidget(d->scroll, 0, 0); init_SmoothScroll(&d->scrollY, w, scrollBegan_ListWidget_); d->itemHeight = 0; + d->scrollMode = normal_ScrollMode; d->noHoverWhileScrolling = iFalse; init_PtrArray(&d->items); d->hoverItem = iInvalidPos; @@ -187,6 +188,10 @@ void setScrollPos_ListWidget(iListWidget *d, int pos) { refresh_Widget(as_Widget(d)); } +void setScrollMode_ListWidget(iListWidget *d, enum iScrollMode mode) { + d->scrollMode = mode; +} + void scrollOffset_ListWidget(iListWidget *d, int offset) { moveSpan_SmoothScroll(&d->scrollY, offset, 0); } @@ -366,12 +371,28 @@ static iBool endDrag_ListWidget_(iListWidget *d, iInt2 endPos) { return iTrue; } +static iBool isScrollDisabled_ListWidget_(const iListWidget *d, const SDL_Event *ev) { + int dir = 0; + if (ev->type == SDL_MOUSEWHEEL) { + dir = iSign(ev->wheel.y); + } + switch (d->scrollMode) { + case disabledAtTopBothDirections_ScrollMode: + return scrollPos_ListWidget(d) <= 0; + case disabledAtTopUpwards_ScrollMode: + return scrollPos_ListWidget(d) <= 0 && dir > 0; + default: + break; + } + return iFalse; +} + static iBool processEvent_ListWidget_(iListWidget *d, const SDL_Event *ev) { iWidget *w = as_Widget(d); if (isMetricsChange_UserEvent(ev)) { invalidate_ListWidget(d); } - else if (processEvent_SmoothScroll(&d->scrollY, ev)) { + else if (!isScrollDisabled_ListWidget_(d, ev) && processEvent_SmoothScroll(&d->scrollY, ev)) { return iTrue; } else if (isCommand_SDLEvent(ev)) { @@ -420,6 +441,18 @@ static iBool processEvent_ListWidget_(iListWidget *d, const SDL_Event *ev) { } } if (ev->type == SDL_MOUSEWHEEL && isHover_Widget(w)) { + if (isScrollDisabled_ListWidget_(d, ev)) { + if (ev->wheel.which == SDL_TOUCH_MOUSEID) { + /* TODO: Could generalize this selection of the scrollable parent. */ + extern iWidgetClass Class_SidebarWidget; + iWidget *sidebar = findParentClass_Widget(w, &Class_SidebarWidget); + if (sidebar) { + transferAffinity_Touch(w, sidebar); + d->noHoverWhileScrolling = iTrue; + } + } + return iFalse; + } int amount = -ev->wheel.y; if (isPerPixel_MouseWheelEvent(&ev->wheel)) { stop_Anim(&d->scrollY.pos); diff --git a/src/ui/listwidget.h b/src/ui/listwidget.h index 7e6624a0..081109e8 100644 --- a/src/ui/listwidget.h +++ b/src/ui/listwidget.h @@ -51,6 +51,12 @@ iDeclareObjectConstruction(ListWidget) iDeclareType(VisBuf) +enum iScrollMode { + normal_ScrollMode, + disabledAtTopBothDirections_ScrollMode, + disabledAtTopUpwards_ScrollMode, +}; + struct Impl_ListWidget { iWidget widget; iScrollWidget *scroll; @@ -63,6 +69,7 @@ struct Impl_ListWidget { iClick click; iIntSet invalidItems; iVisBuf *visBuf; + enum iScrollMode scrollMode; iBool noHoverWhileScrolling; }; @@ -82,6 +89,7 @@ int itemHeight_ListWidget (const iListWidget *); int scrollPos_ListWidget (const iListWidget *); void setScrollPos_ListWidget (iListWidget *, int pos); +void setScrollMode_ListWidget (iListWidget *, enum iScrollMode mode); void scrollToItem_ListWidget (iListWidget *, size_t index, uint32_t span); void scrollOffset_ListWidget (iListWidget *, int offset); void scrollOffsetSpan_ListWidget (iListWidget *, int offset, uint32_t span); diff --git a/src/ui/root.c b/src/ui/root.c index f722df94..65fc11d1 100644 --- a/src/ui/root.c +++ b/src/ui/root.c @@ -487,11 +487,23 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) { else if (deviceType_App() == phone_AppDeviceType && equal_Command(cmd, "window.resized")) { /* Place the sidebar next to or under doctabs depending on orientation. */ iSidebarWidget *sidebar = findChild_Widget(root, "sidebar"); - iSidebarWidget *sidebar2 = findChild_Widget(root, "sidebar2"); removeChild_Widget(parent_Widget(sidebar), sidebar); - // setBackgroundColor_Widget(findChild_Widget(as_Widget(sidebar), "buttons"), - // isPortrait_App() ? uiBackgroundUnfocusedSelection_ColorId - // : uiBackgroundSidebar_ColorId); + if (isLandscape_App()) { + addChildPos_Widget(findChild_Widget(root, "tabs.content"), iClob(sidebar), front_WidgetAddPos); + setWidth_SidebarWidget(sidebar, 73.0f); + setFlags_Widget(as_Widget(sidebar), fixedHeight_WidgetFlag, iFalse); + } + else { + addChild_Widget(root, iClob(sidebar)); + setWidth_SidebarWidget(sidebar, (float) width_Widget(root) / (float) gap_UI); + const int midHeight = height_Widget(root) / 2;// + lineHeight_Text(uiLabelLarge_FontId); + setMidHeight_SidebarWidget(sidebar, midHeight); + setFixedSize_Widget(as_Widget(sidebar), init_I2(-1, midHeight)); + setPos_Widget(as_Widget(sidebar), init_I2(0, height_Widget(root) - midHeight)); + } +#if 0 + iSidebarWidget *sidebar = findChild_Widget(root, "sidebar"); + iSidebarWidget *sidebar2 = findChild_Widget(root, "sidebar2"); setFlags_Widget(findChild_Widget(as_Widget(sidebar), "buttons"), borderTop_WidgetFlag, isPortrait_App()); @@ -507,6 +519,7 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) { setWidth_SidebarWidget(sidebar, (float) width_Widget(root) / (float) gap_UI); setWidth_SidebarWidget(sidebar2, (float) width_Widget(root) / (float) gap_UI); } +#endif return iFalse; } else if (handleCommand_App(cmd)) { @@ -609,14 +622,14 @@ void updatePadding_Root(iRoot *d) { } } #endif - if (toolBar) { +// if (toolBar) { /* TODO: get this from toolBar height, but it's buggy for some reason */ - const int sidebarBottomPad = isPortrait_App() ? 11 * gap_UI + bottom : 0; - setPadding_Widget(findChild_Widget(d->widget, "sidebar"), 0, 0, 0, sidebarBottomPad); - setPadding_Widget(findChild_Widget(d->widget, "sidebar2"), 0, 0, 0, sidebarBottomPad); +// const int sidebarBottomPad = isPortrait_App() ? 11 * gap_UI + bottom : 0; +// setPadding_Widget(findChild_Widget(d->widget, "sidebar"), 0, 0, 0, sidebarBottomPad); + //setPadding_Widget(findChild_Widget(d->widget, "sidebar2"), 0, 0, 0, sidebarBottomPad); /* TODO: There seems to be unrelated layout glitch in the sidebar where its children are not arranged correctly until it's hidden and reshown. */ - } +// } /* Note that `handleNavBarCommands_` also adjusts padding and spacing. */ } @@ -1015,30 +1028,17 @@ static iBool handleToolBarCommands_(iWidget *toolBar, const char *cmd) { } else if (equal_Command(cmd, "toolbar.showview")) { /* TODO: Clean this up. */ - iWidget *sidebar = findWidget_App("sidebar"); - iWidget *sidebar2 = findWidget_App("sidebar2"); - dismissSidebar_(sidebar2, "toolbar.ident"); - const iBool isVisible = isVisible_Widget(sidebar); - // setFlags_Widget(findChild_Widget(toolBar, "toolbar.view"), noBackground_WidgetFlag, - // isVisible); +// iWidget *sidebar = findWidget_App("sidebar"); +// iWidget *sidebar2 = findWidget_App("sidebar2"); +// dismissSidebar_(sidebar2, "toolbar.ident"); +// const iBool isVisible = isVisible_Widget(sidebar); /* If a sidebar hasn't been shown yet, it's height is zero. */ - const int viewHeight = size_Root(get_Root()).y; +// const int viewHeight = size_Root(get_Root()).y; if (arg_Command(cmd) >= 0) { postCommandf_App("sidebar.mode arg:%d show:1", arg_Command(cmd)); -// if (!isVisible) { -// setVisualOffset_Widget(sidebar, viewHeight, 0, 0); -// setVisualOffset_Widget(sidebar, 0, 400, easeOut_AnimFlag | softer_AnimFlag); -// } } else { postCommandf_App("sidebar.toggle"); -// if (isVisible) { -// setVisualOffset_Widget(sidebar, height_Widget(sidebar), 250, easeIn_AnimFlag); -// } -// else { -// setVisualOffset_Widget(sidebar, viewHeight, 0, 0); -// setVisualOffset_Widget(sidebar, 0, 400, easeOut_AnimFlag | softer_AnimFlag); -// } } return iTrue; } @@ -1110,41 +1110,22 @@ void updateMetrics_Root(iRoot *d) { setFixedSize_Widget(appClose, appMin->rect.size); setFixedSize_Widget(appIcon, init_I2(appIconSize_Root(), appMin->rect.size.y)); } - iWidget *navBar = findChild_Widget(d->widget, "navbar"); -// iWidget *lock = findChild_Widget(navBar, "navbar.lock"); - iWidget *url = findChild_Widget(d->widget, "url"); - iWidget *rightEmbed = findChild_Widget(navBar, "url.rightembed"); - iWidget *embedPad = findChild_Widget(navBar, "url.embedpad"); - iWidget *urlButtons = findChild_Widget(navBar, "url.buttons"); + iWidget *navBar = findChild_Widget(d->widget, "navbar"); + iWidget *url = findChild_Widget(d->widget, "url"); + iWidget *rightEmbed = findChild_Widget(navBar, "url.rightembed"); + iWidget *embedPad = findChild_Widget(navBar, "url.embedpad"); + iWidget *urlButtons = findChild_Widget(navBar, "url.buttons"); + iLabelWidget *idName = findChild_Widget(d->widget, "toolbar.name"); setPadding_Widget(as_Widget(url), 0, gap_UI, 0, gap_UI); navBar->rect.size.y = 0; /* recalculate height based on children (FIXME: shouldn't be needed) */ -// updateSize_LabelWidget((iLabelWidget *) lock); -// updateSize_LabelWidget((iLabelWidget *) findChild_Widget(navBar, "reload")); -// arrange_Widget(urlButtons); setFixedSize_Widget(embedPad, init_I2(width_Widget(urlButtons) + gap_UI / 2, 1)); -// setContentPadding_InputWidget((iInputWidget *) url, width_Widget(lock) * 0.75, -// width_Widget(lock) * 0.75); rightEmbed->rect.pos.y = gap_UI; updatePadding_Root(d); arrange_Widget(d->widget); updateUrlInputContentPadding_(navBar); - /* Position the toolbar identity name label manually. */ { - iLabelWidget *idName = findChild_Widget(d->widget, "toolbar.name"); - if (idName) { - const iWidget *toolBar = findChild_Widget(d->widget, "toolbar"); - const iWidget *viewButton = findChild_Widget(d->widget, "toolbar.view"); - const iWidget *idButton = findChild_Widget(toolBar, "toolbar.ident"); -// const int font = uiLabelTiny_FontId; - setFixedSize_Widget(as_Widget(idName), init_I2(-1, 2 * gap_UI + lineHeight_Text(uiLabelTiny_FontId))); -// setFont_LabelWidget(idName, font); - /*setPos_Widget(as_Widget(idName), - windowToLocal_Widget(as_Widget(idName), - init_I2(left_Rect(bounds_Widget(idButton)), - bottom_Rect(bounds_Widget(viewButton)) - - lineHeight_Text(font) - gap_UI / 2))); - setFixedSize_Widget(as_Widget(idName), init_I2(width_Widget(idButton), - lineHeight_Text(font)));*/ - } + if (idName) { + setFixedSize_Widget(as_Widget(idName), + init_I2(-1, 2 * gap_UI + lineHeight_Text(uiLabelTiny_FontId))); } postRefresh_App(); } @@ -1168,11 +1149,9 @@ void createUserInterface_Root(iRoot *d) { setFlags_Widget( root, resizeChildren_WidgetFlag | fixedSize_WidgetFlag | focusRoot_WidgetFlag, iTrue); setCommandHandler_Widget(root, handleRootCommands_); - iWidget *div = makeVDiv_Widget(); setId_Widget(div, "navdiv"); addChild_Widget(root, iClob(div)); - #if defined (LAGRANGE_ENABLE_CUSTOM_FRAME) /* Window title bar. */ if (prefs_App()->customFrame) { @@ -1446,20 +1425,19 @@ void createUserInterface_Root(iRoot *d) { "newtab"); } /* Sidebars. */ { - iWidget *content = findChild_Widget(root, "tabs.content"); iSidebarWidget *sidebar1 = new_SidebarWidget(left_SidebarSide); - addChildPos_Widget(content, iClob(sidebar1), front_WidgetAddPos); if (deviceType_App() != phone_AppDeviceType) { + /* Sidebars are next to the tab content. */ + iWidget *content = findChild_Widget(root, "tabs.content"); + addChildPos_Widget(content, iClob(sidebar1), front_WidgetAddPos); iSidebarWidget *sidebar2 = new_SidebarWidget(right_SidebarSide); addChildPos_Widget(content, iClob(sidebar2), back_WidgetAddPos); } -#if 0 else { - /* The identities sidebar is always in the main area. */ - addChild_Widget(findChild_Widget(root, "stack"), iClob(sidebar2)); - setFlags_Widget(as_Widget(sidebar2), hidden_WidgetFlag, iTrue); + /* Sidebar is a slide-over. */ + addChild_Widget(/*findChild_Widget(root, "stack")*/ root, iClob(sidebar1)); + setFlags_Widget(as_Widget(sidebar1), hidden_WidgetFlag, iTrue); } -#endif } /* Lookup results. */ { iLookupWidget *lookup = new_LookupWidget(); diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index 13fc33b1..401c5d25 100644 --- a/src/ui/sidebarwidget.c +++ b/src/ui/sidebarwidget.c @@ -39,6 +39,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "paint.h" #include "root.h" #include "scrollwidget.h" +#include "touch.h" #include "util.h" #include "visited.h" @@ -99,13 +100,15 @@ struct Impl_SidebarWidget { iListWidget * list; iCertListWidget * certList; iWidget * actions; /* below the list, area for buttons */ + int midHeight; /* on portrait phone, the height for the middle state */ + iBool isBeingDraggedVertically; /* on portrait phone, sidebar can be dragged up/down */ int modeScroll[max_SidebarMode]; iLabelWidget * modeButtons[max_SidebarMode]; int maxButtonLabelWidth; float widthAsGaps; int buttonFont; int itemFonts[2]; - size_t numUnreadEntries; + size_t numUnreadEntries; iWidget * resizer; iWidget * menu; /* context menu for an item */ iWidget * modeMenu; /* context menu for the sidebar mode (no item) */ @@ -194,9 +197,9 @@ static iLabelWidget *addActionButton_SidebarWidget_(iSidebarWidget *d, const cha //(deviceType_App() != desktop_AppDeviceType ? // extraPadding_WidgetFlag : 0) | flags); - setFont_LabelWidget(btn, deviceType_App() == phone_AppDeviceType && d->side == right_SidebarSide - ? uiLabelBig_FontId - : d->buttonFont); + setFont_LabelWidget(btn, /*deviceType_App() == phone_AppDeviceType && d->side == right_SidebarSide + ? uiLabelBig_FontId : */ + d->buttonFont); checkIcon_LabelWidget(btn); return btn; } @@ -211,7 +214,12 @@ static iBool isBookmarkFolded_SidebarWidget_(const iSidebarWidget *d, const iBoo return iFalse; } +static iBool isSlidingSheet_SidebarWidget_(const iSidebarWidget *d) { + return isPortraitPhone_App();// && scrollPos_ListWidget(d->list) <= 0; +} + static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepActions) { + const iBool isMobile = (deviceType_App() != desktop_AppDeviceType); clear_ListWidget(d->list); releaseChildren_Widget(d->blank); if (!keepActions) { @@ -299,9 +307,10 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct } /* Actions. */ if (!keepActions) { - addActionButton_SidebarWidget_( - d, check_Icon " ${sidebar.action.feeds.markallread}", "feeds.markallread", expand_WidgetFlag | - tight_WidgetFlag); + addActionButton_SidebarWidget_(d, + check_Icon " ${sidebar.action.feeds.markallread}", + "feeds.markallread", + expand_WidgetFlag | tight_WidgetFlag); updateSize_LabelWidget(addChildFlags_Widget(d->actions, iClob(new_LabelWidget("${sidebar.action.show}", NULL)), frameless_WidgetFlag | tight_WidgetFlag)); @@ -617,6 +626,10 @@ void setClosedFolders_SidebarWidget(iSidebarWidget *d, const iIntSet *closedFold } } +void setMidHeight_SidebarWidget(iSidebarWidget *d, int midHeight) { + d->midHeight = midHeight; +} + enum iSidebarMode mode_SidebarWidget(const iSidebarWidget *d) { return d ? d->mode : 0; } @@ -686,6 +699,8 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) { d->side = side; d->mode = -1; d->feedsMode = all_FeedsMode; + d->midHeight = 0; + d->isBeingDraggedVertically = iFalse; d->numUnreadEntries = 0; d->buttonFont = uiLabel_FontId; /* wiil be changed later */ d->itemFonts[0] = uiContent_FontId; @@ -703,15 +718,24 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) { iWidget *vdiv = makeVDiv_Widget(); addChildFlags_Widget(w, vdiv, resizeToParentWidth_WidgetFlag | resizeToParentHeight_WidgetFlag); iZap(d->modeButtons); - d->resizer = NULL; - d->list = NULL; - d->certList = NULL; - d->actions = NULL; + d->resizer = NULL; + d->list = NULL; + d->certList = NULL; + d->actions = NULL; d->closedFolders = new_IntSet(); /* On a phone, the right sidebar is not used. */ - const iBool isPhone = deviceType_App() == phone_AppDeviceType; - //if (!isPhone || d->side == left_SidebarSide) { - iWidget *buttons = new_Widget(); + const iBool isPhone = (deviceType_App() == phone_AppDeviceType); + if (isPhone) { + iLabelWidget *closeButton = + addChildFlags_Widget(vdiv, + iClob(new_LabelWidget("${sidebar.close}", "sidebar.toggle")), + collapse_WidgetFlag | alignRight_WidgetFlag | + extraPadding_WidgetFlag | frameless_WidgetFlag); + as_Widget(closeButton)->flags2 |= slidingSheetDraggable_WidgetFlag2; /* phone */ + setId_Widget(as_Widget(closeButton), "sidebar.close"); + setFont_LabelWidget(closeButton, uiLabelBigBold_FontId); + } + iWidget *buttons = new_Widget(); setId_Widget(buttons, "buttons"); setDrawBufferEnabled_Widget(buttons, iTrue); for (int i = 0; i < max_SidebarMode; i++) { @@ -725,28 +749,14 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) { tightModeLabels_[i], format_CStr("%s.mode arg:%d", cstr_String(id_Widget(w)), i))), frameless_WidgetFlag | noBackground_WidgetFlag); + as_Widget(d->modeButtons[i])->flags2 |= slidingSheetDraggable_WidgetFlag2; /* phone */ } setButtonFont_SidebarWidget(d, isPhone ? uiLabelBig_FontId : uiLabel_FontId); addChildFlags_Widget(vdiv, iClob(buttons), - arrangeHorizontal_WidgetFlag | - resizeWidthOfChildren_WidgetFlag | - arrangeHeight_WidgetFlag | resizeToParentWidth_WidgetFlag); // | -// drawBackgroundToHorizontalSafeArea_WidgetFlag); + arrangeHorizontal_WidgetFlag | resizeWidthOfChildren_WidgetFlag | + arrangeHeight_WidgetFlag | resizeToParentWidth_WidgetFlag); setBackgroundColor_Widget(buttons, uiBackgroundSidebar_ColorId); -// } -#if 0 - else { - iLabelWidget *heading = new_LabelWidget(person_Icon " ${sidebar.identities}", NULL); - checkIcon_LabelWidget(heading); - setBackgroundColor_Widget(as_Widget(heading), uiBackgroundSidebar_ColorId); - setTextColor_LabelWidget(heading, uiTextSelected_ColorId); - setFont_LabelWidget(addChildFlags_Widget(vdiv, iClob(heading), borderTop_WidgetFlag | - alignLeft_WidgetFlag | frameless_WidgetFlag | - drawBackgroundToHorizontalSafeArea_WidgetFlag), - uiLabelLargeBold_FontId); - } -#endif iWidget *content = new_Widget(); setFlags_Widget(content, resizeChildren_WidgetFlag, iTrue); iWidget *listAndActions = makeVDiv_Widget(); @@ -756,15 +766,17 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) { d->list = new_ListWidget(); setPadding_Widget(as_Widget(d->list), 0, gap_UI, 0, gap_UI); addChild_Widget(listArea, iClob(d->list)); - d->certList = new_CertListWidget(); - setPadding_Widget(as_Widget(d->certList), 0, gap_UI, 0, gap_UI); - addChild_Widget(listArea, iClob(d->certList)); + if (!isPhone) { + d->certList = new_CertListWidget(); + setPadding_Widget(as_Widget(d->certList), 0, gap_UI, 0, gap_UI); + addChild_Widget(listArea, iClob(d->certList)); + } addChildFlags_Widget(listAndActions, iClob(listArea), expand_WidgetFlag); // | drawBackgroundToHorizontalSafeArea_WidgetFlag); setId_Widget(addChildPosFlags_Widget(listAndActions, iClob(d->actions = new_Widget()), - isPhone ? front_WidgetAddPos : back_WidgetAddPos, + /*isPhone ? front_WidgetAddPos :*/ back_WidgetAddPos, arrangeHorizontal_WidgetFlag | arrangeHeight_WidgetFlag | resizeWidthOfChildren_WidgetFlag), // | // drawBackgroundToHorizontalSafeArea_WidgetFlag), @@ -891,7 +903,7 @@ static void checkModeButtonLayout_SidebarWidget_(iSidebarWidget *d) { // updateMetrics_SidebarWidget_(d); updateItemHeight_SidebarWidget_(d); } - setButtonFont_SidebarWidget(d, isPortrait_App() ? uiLabelBig_FontId : uiLabel_FontId); + setButtonFont_SidebarWidget(d, isPortrait_App() ? uiLabelMedium_FontId : uiLabel_FontId); } const iBool isTight = (width_Rect(bounds_Widget(as_Widget(d->modeButtons[0]))) < d->maxButtonLabelWidth); @@ -982,6 +994,52 @@ iBool handleBookmarkEditorCommands_SidebarWidget_(iWidget *editor, const char *c return iFalse; } +static void animateSlidingSheetHeight_SidebarWidget_(iAny *sidebar) { + iWidget *d = sidebar; + const int oldSize = d->rect.size.y; + const int newSize = bottom_Rect(safeRect_Root(d->root)) - top_Rect(bounds_Widget(d)); + if (oldSize != newSize) { + d->rect.size.y = newSize; + arrange_Widget(d); + } +// printf("[%p] %u: %d animating %d\n", d, window_Widget(d)->frameTime, +// (flags_Widget(sidebar) & visualOffset_WidgetFlag) != 0, +// newSize); + if (!isFinished_Anim(&d->visualOffset)) { + addTicker_App(animateSlidingSheetHeight_SidebarWidget_, sidebar); + } +} + +enum iSlidingSheetPos { + top_SlidingSheetPos, + middle_SlidingSheetPos, + bottom_SlidingSheetPos, +}; + +static void setSlidingSheetPos_SidebarWidget_(iSidebarWidget *d, enum iSlidingSheetPos slide) { + iWidget *w = as_Widget(d); + const int pos = w->rect.pos.y; + const iRect safeRect = safeRect_Root(w->root); + if (slide == top_SlidingSheetPos) { + w->rect.pos.y = top_Rect(safeRect); + w->rect.size.y = height_Rect(safeRect); + setVisualOffset_Widget(w, pos - w->rect.pos.y, 0, 0); + setVisualOffset_Widget(w, 0, 200, easeOut_AnimFlag | softer_AnimFlag); + setScrollMode_ListWidget(d->list, disabledAtTopUpwards_ScrollMode); + } + else if (slide == bottom_SlidingSheetPos) { + postCommand_Widget(w, "sidebar.toggle"); + } + else { + w->rect.size.y = d->midHeight; + w->rect.pos.y = height_Rect(safeRect) - w->rect.size.y; + setVisualOffset_Widget(w, pos - w->rect.pos.y, 0, 0); + setVisualOffset_Widget(w, 0, 200, easeOut_AnimFlag | softer_AnimFlag); + setScrollMode_ListWidget(d->list, disabledAtTopBothDirections_ScrollMode); + } + animateSlidingSheetHeight_SidebarWidget_(d); +} + static iBool handleSidebarCommand_SidebarWidget_(iSidebarWidget *d, const char *cmd) { iWidget *w = as_Widget(d); if (equal_Command(cmd, "width")) { @@ -1011,35 +1069,58 @@ static iBool handleSidebarCommand_SidebarWidget_(iSidebarWidget *d, const char * argLabel_Command(cmd, "noanim") == 0 && (d->side == left_SidebarSide || deviceType_App() != phone_AppDeviceType); int visX = 0; + int visY = 0; if (isVisible_Widget(w)) { visX = left_Rect(bounds_Widget(w)) - left_Rect(w->root->widget->rect); + visY = top_Rect(bounds_Widget(w)) - top_Rect(w->root->widget->rect); } - setFlags_Widget(w, hidden_WidgetFlag, isVisible_Widget(w)); + const iBool isHiding = isVisible_Widget(w); + setFlags_Widget(w, hidden_WidgetFlag, isHiding); /* Safe area inset for mobile. */ const int safePad = (d->side == left_SidebarSide ? left_Rect(safeRect_Root(w->root)) : 0); - if (isVisible_Widget(w)) { - setFlags_Widget(w, keepOnTop_WidgetFlag, iFalse); - w->rect.size.x = d->widthAsGaps * gap_UI; - invalidate_ListWidget(d->list); - if (isAnimated) { + const int animFlags = easeOut_AnimFlag | softer_AnimFlag; + if (!isPortraitPhone_App()) { + if (!isHiding) { + setFlags_Widget(w, keepOnTop_WidgetFlag, iFalse); + w->rect.size.x = d->widthAsGaps * gap_UI; + invalidate_ListWidget(d->list); + if (isAnimated) { + setFlags_Widget(w, horizontalOffset_WidgetFlag, iTrue); + setVisualOffset_Widget( + w, (d->side == left_SidebarSide ? -1 : 1) * (w->rect.size.x + safePad), 0, 0); + setVisualOffset_Widget(w, 0, 300, animFlags); + } + } + else if (isAnimated) { setFlags_Widget(w, horizontalOffset_WidgetFlag, iTrue); - setVisualOffset_Widget( - w, (d->side == left_SidebarSide ? -1 : 1) * (w->rect.size.x + safePad), 0, 0); - setVisualOffset_Widget(w, 0, 300, easeOut_AnimFlag | softer_AnimFlag); + if (d->side == right_SidebarSide) { + setVisualOffset_Widget(w, visX, 0, 0); + setVisualOffset_Widget( + w, visX + w->rect.size.x + safePad, 300, animFlags); + } + else { + setFlags_Widget(w, keepOnTop_WidgetFlag, iTrue); + setVisualOffset_Widget( + w, -w->rect.size.x - safePad, 300, animFlags); + } } + setScrollMode_ListWidget(d->list, normal_ScrollMode); } - else if (isAnimated) { - setFlags_Widget(w, horizontalOffset_WidgetFlag, iTrue); - if (d->side == right_SidebarSide) { - setVisualOffset_Widget(w, visX, 0, 0); - setVisualOffset_Widget( - w, visX + w->rect.size.x + safePad, 300, easeOut_AnimFlag | softer_AnimFlag); + else { + /* Portrait phone sidebar works differently: it slides up from the bottom. */ + setFlags_Widget(w, horizontalOffset_WidgetFlag, iFalse); + if (!isHiding) { + invalidate_ListWidget(d->list); + w->rect.pos.y = height_Rect(safeRect_Root(w->root)) - d->midHeight; + setVisualOffset_Widget(w, bottom_Rect(rect_Root(w->root)) - w->rect.pos.y, 0, 0); + setVisualOffset_Widget(w, 0, 300, animFlags); + animateSlidingSheetHeight_SidebarWidget_(d); + setScrollMode_ListWidget(d->list, disabledAtTopBothDirections_ScrollMode); } else { - setFlags_Widget(w, keepOnTop_WidgetFlag, iTrue); - setVisualOffset_Widget( - w, -w->rect.size.x - safePad, 300, easeOut_AnimFlag | softer_AnimFlag); + setVisualOffset_Widget(w, bottom_Rect(rect_Root(w->root)) - w->rect.pos.y, 300, animFlags); } + showToolbar_Root(w->root, isHiding); } updateToolbarColors_Root(w->root); arrange_Widget(w->parent); @@ -1097,13 +1178,32 @@ static size_t numBookmarks_(const iPtrArray *bmList) { return num; } +static iRangei SlidingSheetMiddleRegion_SidebarWidget_(const iSidebarWidget *d) { + const iWidget *w = constAs_Widget(d); + const iRect safeRect = safeRect_Root(w->root); + const int midY = bottom_Rect(safeRect) - d->midHeight; + const int topHalf = (top_Rect(safeRect) + midY) / 2; + const int bottomHalf = (bottom_Rect(safeRect) + midY * 2) / 3; + return (iRangei){ topHalf, bottomHalf }; +} + +static void gotoNearestSlidingSheetPos_SidebarWidget_(iSidebarWidget *d) { + const iRangei midRegion = SlidingSheetMiddleRegion_SidebarWidget_(d); + const int pos = top_Rect(d->widget.rect); + setSlidingSheetPos_SidebarWidget_(d, pos < midRegion.start + ? top_SlidingSheetPos + : pos > midRegion.end ? bottom_SlidingSheetPos + : middle_SlidingSheetPos); +} + static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) { iWidget *w = as_Widget(d); /* Handle commands. */ if (isResize_UserEvent(ev)) { checkModeButtonLayout_SidebarWidget_(d); - if (deviceType_App() == phone_AppDeviceType && d->side == left_SidebarSide) { - setFlags_Widget(w, rightEdgeDraggable_WidgetFlag, isPortrait_App()); + if (deviceType_App() == phone_AppDeviceType) { // && d->side == left_SidebarSide) { +// setFlags_Widget(w, rightEdgeDraggable_WidgetFlag, isPortrait_App()); + setFlags_Widget(findChild_Widget(w, "sidebar.close"), hidden_WidgetFlag, isLandscape_App()); /* In landscape, visibility of the toolbar is controlled separately. */ if (isVisible_Widget(w)) { postCommand_Widget(w, "sidebar.toggle"); @@ -1117,6 +1217,10 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) setFlags_Widget(as_Widget(d->list), drawBackgroundToHorizontalSafeArea_WidgetFlag, isLandscape_App()); + setFlags_Widget(w, + drawBackgroundToBottom_WidgetFlag, + isPortrait_App()); + setBackgroundColor_Widget(w, isPortrait_App() ? uiBackgroundSidebar_ColorId : none_ColorId); return iFalse; } } @@ -1572,6 +1676,61 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) return iTrue; } } + if (isSlidingSheet_SidebarWidget_(d)) { + if (ev->type == SDL_MOUSEWHEEL) { + enum iWidgetTouchMode touchMode = widgetMode_Touch(w); + if (touchMode == momentum_WidgetTouchMode) { + /* We don't do momentum. */ + float swipe = stopWidgetMomentum_Touch(w); +// printf("swipe: %f\n", swipe); + const iRangei midRegion = SlidingSheetMiddleRegion_SidebarWidget_(d); + const int pos = top_Rect(w->rect); + if (swipe < 500) { + gotoNearestSlidingSheetPos_SidebarWidget_(d); + } + else if (swipe > 6500 && ev->wheel.y > 0) { + /* Fast swipe down will dismiss. */ + setSlidingSheetPos_SidebarWidget_(d, bottom_SlidingSheetPos); + } + else if (ev->wheel.y < 0) { + setSlidingSheetPos_SidebarWidget_(d, top_SlidingSheetPos); + } + else if (pos < (midRegion.start + midRegion.end) / 2) { + setSlidingSheetPos_SidebarWidget_(d, middle_SlidingSheetPos); + } + else { + setSlidingSheetPos_SidebarWidget_(d, bottom_SlidingSheetPos); + } + } + else if (touchMode == touch_WidgetTouchMode) { + /* Move with the finger. */ + adjustEdges_Rect(&w->rect, ev->wheel.y, 0, 0, 0); + /* Upon reaching the top, scrolling is switched back to the list. */ + const iRect rootRect = safeRect_Root(w->root); + const int top = top_Rect(rootRect); + if (w->rect.pos.y < top) { + setScrollMode_ListWidget(d->list, disabledAtTopUpwards_ScrollMode); + setScrollPos_ListWidget(d->list, top - w->rect.pos.y); + transferAffinity_Touch(w, as_Widget(d->list)); + w->rect.pos.y = top; + w->rect.size.y = height_Rect(rootRect); + } + else { + setScrollMode_ListWidget(d->list, disabledAtTopBothDirections_ScrollMode); + } + arrange_Widget(w); + refresh_Widget(w); + } + else { + return iFalse; + } + return iTrue; + } + if (ev->type == SDL_USEREVENT && ev->user.code == widgetTouchEnds_UserEventCode) { + gotoNearestSlidingSheetPos_SidebarWidget_(d); + return iTrue; + } + } if (ev->type == SDL_MOUSEBUTTONDOWN && contains_Widget(as_Widget(d->list), init_I2(ev->button.x, ev->button.y))) { if (hoverItem_ListWidget(d->list) || isVisible_Widget(d->menu)) { diff --git a/src/ui/sidebarwidget.h b/src/ui/sidebarwidget.h index 81c6681f..2a930a60 100644 --- a/src/ui/sidebarwidget.h +++ b/src/ui/sidebarwidget.h @@ -54,6 +54,7 @@ iBool setMode_SidebarWidget (iSidebarWidget *, enum iSidebar void setWidth_SidebarWidget (iSidebarWidget *, float widthAsGaps); iBool setButtonFont_SidebarWidget (iSidebarWidget *, int font); void setClosedFolders_SidebarWidget (iSidebarWidget *, const iIntSet *closedFolders); +void setMidHeight_SidebarWidget (iSidebarWidget *, int midHeight); /* phone layout */ enum iSidebarMode mode_SidebarWidget (const iSidebarWidget *); enum iFeedsMode feedsMode_SidebarWidget (const iSidebarWidget *); diff --git a/src/ui/touch.c b/src/ui/touch.c index 195d1dff..aee5a383 100644 --- a/src/ui/touch.c +++ b/src/ui/touch.c @@ -589,9 +589,18 @@ iBool processEvent_Touch(const SDL_Event *ev) { divvf_F3(&touch->accum, 6); divfv_I2(&pixels, 6); /* Allow scrolling a scrollable widget. */ - iWidget *flow = findOverflowScrollable_Widget(touch->affinity); - if (flow) { - touch->affinity = flow; + if (touch->affinity && touch->affinity->flags2 & slidingSheetDraggable_WidgetFlag2) { + extern iWidgetClass Class_SidebarWidget; /* The only type of sliding sheet for now. */ + iWidget *slider = findParentClass_Widget(touch->affinity, &Class_SidebarWidget); + if (slider) { + touch->affinity = slider; + } + } + else { + iWidget *flow = findOverflowScrollable_Widget(touch->affinity); + if (flow) { + touch->affinity = flow; + } } } else { @@ -617,11 +626,13 @@ iBool processEvent_Touch(const SDL_Event *ev) { if (touch->axis == y_TouchAxis) { pixels.x = 0; } -// printf("%p (%s) py: %i wy: %f acc: %f edge: %d\n", -// touch->affinity, -// class_Widget(touch->affinity)->name, -// pixels.y, y_F3(amount), y_F3(touch->accum), -// touch->edge); +#if 0 + printf("%p (%s) py: %i wy: %f acc: %f edge: %d\n", + touch->affinity, + class_Widget(touch->affinity)->name, + pixels.y, y_F3(amount), y_F3(touch->accum), + touch->edge); +#endif if (pixels.x || pixels.y) { //setFocus_Widget(NULL); dispatchMotion_Touch_(touch->startPos /*pos[0]*/, 0); @@ -801,6 +812,16 @@ void widgetDestroyed_Touch(iWidget *widget) { } } +void transferAffinity_Touch(iWidget *src, iWidget *dst) { + iTouchState *d = touchState_(); + iForEach(Array, i, d->touches) { + iTouch *touch = i.value; + if (touch->affinity == src) { + touch->affinity = dst; + } + } +} + iInt2 latestPosition_Touch(void) { return touchState_()->currentTouchPos; } diff --git a/src/ui/touch.h b/src/ui/touch.h index e048224a..c9c76d86 100644 --- a/src/ui/touch.h +++ b/src/ui/touch.h @@ -39,6 +39,7 @@ void update_Touch (void); float stopWidgetMomentum_Touch (const iWidget *widget); enum iWidgetTouchMode widgetMode_Touch (const iWidget *widget); void widgetDestroyed_Touch (iWidget *widget); +void transferAffinity_Touch (iWidget *src, iWidget *dst); iInt2 latestPosition_Touch (void); /* valid during processing of current event */ iBool isHovering_Touch (void); /* stationary touch or a long-press drag ongoing */ diff --git a/src/ui/widget.c b/src/ui/widget.c index 210fe899..0d20cca9 100644 --- a/src/ui/widget.c +++ b/src/ui/widget.c @@ -124,6 +124,7 @@ void init_Widget(iWidget *d) { init_String(&d->id); d->root = get_Root(); /* never changes after this */ d->flags = 0; + d->flags2 = 0; d->rect = zero_Rect(); d->minSize = zero_I2(); d->sizeRef = NULL; diff --git a/src/ui/widget.h b/src/ui/widget.h index 4025f5c5..35be1bcb 100644 --- a/src/ui/widget.h +++ b/src/ui/widget.h @@ -123,6 +123,10 @@ enum iWidgetFlag { #define refChildrenOffset_WidgetFlag iBit64(63) /* visual offset determined by the offset of referenced children */ #define nativeMenu_WidgetFlag iBit64(64) +enum iWidgetFlag2 { + slidingSheetDraggable_WidgetFlag2 = iBit(1), +}; + enum iWidgetAddPos { back_WidgetAddPos, front_WidgetAddPos, @@ -139,6 +143,7 @@ struct Impl_Widget { iObject object; iString id; int64_t flags; + int flags2; iRect rect; iInt2 minSize; iWidget * sizeRef; -- cgit v1.2.3 From 8d255fca904c55fb8c15631c6845c8a87f1a0c6a Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 4 Dec 2021 09:49:16 +0200 Subject: Mobile: Sidebar actions for Feeds, History --- po/en.po | 3 + src/ui/sidebarwidget.c | 164 ++++++++++++++++++++++++++++++++++--------------- 2 files changed, 117 insertions(+), 50 deletions(-) diff --git a/po/en.po b/po/en.po index 6996d28c..08683659 100644 --- a/po/en.po +++ b/po/en.po @@ -438,6 +438,9 @@ msgstr "New…" msgid "sidebar.action.ident.import" msgstr "Import…" +msgid "sidebar.action.history.clear" +msgstr "Clear" + # Usage: "(count) Unread" in the sidebar tab title, referring to feed entries. msgid "sidebar.unread" msgid_plural "sidebar.unread.n" diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index 401c5d25..48802ddf 100644 --- a/src/ui/sidebarwidget.c +++ b/src/ui/sidebarwidget.c @@ -90,6 +90,22 @@ iDefineObjectConstruction(SidebarItem) /*----------------------------------------------------------------------------------------------*/ +static const char *normalModeLabels_[max_SidebarMode] = { + book_Icon " ${sidebar.bookmarks}", + star_Icon " ${sidebar.feeds}", + clock_Icon " ${sidebar.history}", + person_Icon " ${sidebar.identities}", + page_Icon " ${sidebar.outline}", +}; + +static const char *tightModeLabels_[max_SidebarMode] = { + book_Icon, + star_Icon, + clock_Icon, + person_Icon, + page_Icon, +}; + struct Impl_SidebarWidget { iWidget widget; enum iSidebarSide side; @@ -197,10 +213,14 @@ static iLabelWidget *addActionButton_SidebarWidget_(iSidebarWidget *d, const cha //(deviceType_App() != desktop_AppDeviceType ? // extraPadding_WidgetFlag : 0) | flags); - setFont_LabelWidget(btn, /*deviceType_App() == phone_AppDeviceType && d->side == right_SidebarSide + setFont_LabelWidget(btn, deviceType_App() == desktop_AppDeviceType ? /*deviceType_App() == phone_AppDeviceType && d->side == right_SidebarSide ? uiLabelBig_FontId : */ - d->buttonFont); + d->buttonFont : uiLabelBig_FontId); checkIcon_LabelWidget(btn); + if (deviceType_App() != desktop_AppDeviceType) { + setFlags_Widget(as_Widget(btn), frameless_WidgetFlag, iTrue); + setBackgroundColor_Widget(as_Widget(btn), uiBackground_ColorId); + } return btn; } @@ -306,36 +326,57 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct } } /* Actions. */ - if (!keepActions) { - addActionButton_SidebarWidget_(d, - check_Icon " ${sidebar.action.feeds.markallread}", - "feeds.markallread", - expand_WidgetFlag | tight_WidgetFlag); - updateSize_LabelWidget(addChildFlags_Widget(d->actions, - iClob(new_LabelWidget("${sidebar.action.show}", NULL)), - frameless_WidgetFlag | tight_WidgetFlag)); - const iMenuItem items[] = { - { page_Icon " ${sidebar.action.feeds.showall}", SDLK_u, KMOD_SHIFT, "feeds.mode arg:0" }, - { circle_Icon " ${sidebar.action.feeds.showunread}", SDLK_u, 0, "feeds.mode arg:1" }, - }; - iWidget *dropButton = addChild_Widget( - d->actions, - iClob(makeMenuButton_LabelWidget(items[d->feedsMode].label, items, 2))); - setId_Widget(dropButton, "feeds.modebutton"); - checkIcon_LabelWidget((iLabelWidget *) dropButton); - setFixedSize_Widget( - dropButton, - init_I2(iMaxi(20 * gap_UI, - measure_Text(default_FontId, - translateCStr_Lang( - items[findWidestLabel_MenuItem(items, 2)].label)) - .advance.x + - 13 * gap_UI), - -1)); + if (!isMobile) { + if (!keepActions) { + addActionButton_SidebarWidget_(d, + check_Icon " ${sidebar.action.feeds.markallread}", + "feeds.markallread", + expand_WidgetFlag | tight_WidgetFlag); + updateSize_LabelWidget(addChildFlags_Widget(d->actions, + iClob(new_LabelWidget("${sidebar.action.show}", NULL)), + frameless_WidgetFlag | tight_WidgetFlag)); + const iMenuItem items[] = { + { page_Icon " ${sidebar.action.feeds.showall}", SDLK_u, KMOD_SHIFT, "feeds.mode arg:0" }, + { circle_Icon " ${sidebar.action.feeds.showunread}", SDLK_u, 0, "feeds.mode arg:1" }, + }; + iWidget *dropButton = addChild_Widget( + d->actions, + iClob(makeMenuButton_LabelWidget(items[d->feedsMode].label, items, 2))); + setId_Widget(dropButton, "feeds.modebutton"); + checkIcon_LabelWidget((iLabelWidget *) dropButton); + setFixedSize_Widget( + dropButton, + init_I2(iMaxi(20 * gap_UI, + measure_Text(default_FontId, + translateCStr_Lang( + items[findWidestLabel_MenuItem(items, 2)].label)) + .advance.x + + 13 * gap_UI), + -1)); + } + else { + updateDropdownSelection_LabelWidget(findChild_Widget(d->actions, "feeds.modebutton"), + format_CStr(" arg:%d", d->feedsMode)); + } } else { - updateDropdownSelection_LabelWidget(findChild_Widget(d->actions, "feeds.modebutton"), - format_CStr(" arg:%d", d->feedsMode)); + if (!keepActions) { + iLabelWidget *readAll = addActionButton_SidebarWidget_(d, + check_Icon, + "feeds.markallread confirm:1", + 0); + setTextColor_LabelWidget(readAll, uiTextCaution_ColorId); + addActionButton_SidebarWidget_(d, + page_Icon, + "feeds.mode arg:0", + 0); + addActionButton_SidebarWidget_(d, + circle_Icon, + "feeds.mode arg:1", + 0); + } + setOutline_LabelWidget(child_Widget(d->actions, 1), d->feedsMode != all_FeedsMode); + setOutline_LabelWidget(child_Widget(d->actions, 2), d->feedsMode != unread_FeedsMode); } d->menu = makeMenu_Widget( as_Widget(d), @@ -500,6 +541,12 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct (iMenuItem[]){ { delete_Icon " " uiTextCaution_ColorEscape "${history.clear}", 0, 0, "history.clear confirm:1" }, }, 1); + if (isMobile) { + addChildFlags_Widget(d->actions, iClob(new_Widget()), expand_WidgetFlag); + iLabelWidget *btn = addActionButton_SidebarWidget_(d, "${sidebar.action.history.clear}", + "history.clear confirm:1", 0); + setFont_LabelWidget(btn, uiLabelBigBold_FontId); + } break; } case identities_SidebarMode: { @@ -616,6 +663,15 @@ iBool setMode_SidebarWidget(iSidebarWidget *d, enum iSidebarMode mode) { updateItemHeight_SidebarWidget_(d); /* Restore previous scroll position. */ setScrollPos_ListWidget(list_SidebarWidget_(d), d->modeScroll[mode]); + /* Title of the mobile sliding sheet. */ + iLabelWidget *sheetTitle = findChild_Widget(&d->widget, "sidebar.title"); + if (sheetTitle) { + iString title; + initCStr_String(&title, normalModeLabels_[d->mode]); + removeIconPrefix_String(&title); + setText_LabelWidget(sheetTitle, &title); + deinit_String(&title); + } return iTrue; } @@ -646,22 +702,6 @@ const iIntSet *closedFolders_SidebarWidget(const iSidebarWidget *d) { return d ? d->closedFolders : collect_IntSet(new_IntSet()); } -static const char *normalModeLabels_[max_SidebarMode] = { - book_Icon " ${sidebar.bookmarks}", - star_Icon " ${sidebar.feeds}", - clock_Icon " ${sidebar.history}", - person_Icon " ${sidebar.identities}", - page_Icon " ${sidebar.outline}", -}; - -static const char *tightModeLabels_[max_SidebarMode] = { - book_Icon, - star_Icon, - clock_Icon, - person_Icon, - page_Icon, -}; - const char *icon_SidebarMode(enum iSidebarMode mode) { return tightModeLabels_[mode]; } @@ -726,13 +766,21 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) { /* On a phone, the right sidebar is not used. */ const iBool isPhone = (deviceType_App() == phone_AppDeviceType); if (isPhone) { - iLabelWidget *closeButton = + iLabelWidget *sheetTitle = addChildFlags_Widget(vdiv, - iClob(new_LabelWidget("${sidebar.close}", "sidebar.toggle")), - collapse_WidgetFlag | alignRight_WidgetFlag | + iClob(new_LabelWidget("", NULL)), + collapse_WidgetFlag | extraPadding_WidgetFlag | frameless_WidgetFlag); + setBackgroundColor_Widget(as_Widget(sheetTitle), uiBackground_ColorId); + iLabelWidget *closeButton = addChildFlags_Widget(as_Widget(sheetTitle), + iClob(new_LabelWidget(uiTextAction_ColorEscape "${sidebar.close}", "sidebar.toggle")), + extraPadding_WidgetFlag | frameless_WidgetFlag | + alignRight_WidgetFlag | moveToParentRightEdge_WidgetFlag); + as_Widget(sheetTitle)->flags2 |= slidingSheetDraggable_WidgetFlag2; /* phone */ as_Widget(closeButton)->flags2 |= slidingSheetDraggable_WidgetFlag2; /* phone */ + setId_Widget(as_Widget(sheetTitle), "sidebar.title"); setId_Widget(as_Widget(closeButton), "sidebar.close"); + setFont_LabelWidget(sheetTitle, uiLabelBig_FontId); setFont_LabelWidget(closeButton, uiLabelBigBold_FontId); } iWidget *buttons = new_Widget(); @@ -781,7 +829,14 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) { resizeWidthOfChildren_WidgetFlag), // | // drawBackgroundToHorizontalSafeArea_WidgetFlag), "actions"); - setBackgroundColor_Widget(d->actions, uiBackgroundSidebar_ColorId); + if (deviceType_App() != desktop_AppDeviceType) { + setFlags_Widget(findChild_Widget(w, "sidebar.title"), borderTop_WidgetFlag, iTrue); + setFlags_Widget(d->actions, drawBackgroundToBottom_WidgetFlag, iTrue); + setBackgroundColor_Widget(d->actions, uiBackground_ColorId); + } + else { + setBackgroundColor_Widget(d->actions, uiBackgroundSidebar_ColorId); + } d->contextItem = NULL; d->contextIndex = iInvalidPos; d->blank = new_Widget(); @@ -1488,6 +1543,15 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) return iTrue; } else if (equal_Command(cmd, "feeds.markallread") && d->mode == feeds_SidebarMode) { + if (argLabel_Command(cmd, "confirm")) { + /* This is used on mobile. */ + iWidget *menu = makeMenu_Widget(w->root->widget, (iMenuItem[]){ + check_Icon " " uiTextCaution_ColorEscape "${feeds.markallread}", 0, 0, + "feeds.markallread" + }, 1); + openMenu_Widget(menu, topLeft_Rect(bounds_Widget(d->actions))); + return iTrue; + } iConstForEach(PtrArray, i, listEntries_Feeds()) { const iFeedEntry *entry = i.ptr; const iString *url = url_FeedEntry(entry); -- cgit v1.2.3 From d3fd40c8ef38ba5eeaeaa474166a66ca36f2edf7 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 4 Dec 2021 12:23:59 +0200 Subject: SidebarWidget: Mobile bookmark editing mode ListWidget can use drag handles on items. --- po/en.po | 6 ++++++ src/ui/listwidget.c | 29 ++++++++++++++++++++++++++--- src/ui/listwidget.h | 2 ++ src/ui/mobile.c | 3 +++ src/ui/sidebarwidget.c | 50 ++++++++++++++++++++++++++++++++++++++++++++++---- 5 files changed, 83 insertions(+), 7 deletions(-) diff --git a/po/en.po b/po/en.po index 08683659..0ac1dd2e 100644 --- a/po/en.po +++ b/po/en.po @@ -419,6 +419,12 @@ msgstr "Identities" msgid "sidebar.outline" msgstr "Outline" +msgid "sidebar.action.bookmarks.newfolder" +msgstr "New Folder" + +msgid "sidebar.action.bookmarks.edit" +msgstr "Edit" + # This label should be fairly short so it fits in a button in the sidebar. msgid "sidebar.action.feeds.markallread" msgstr "Read All" diff --git a/src/ui/listwidget.c b/src/ui/listwidget.c index d12afc4c..ccc5b104 100644 --- a/src/ui/listwidget.c +++ b/src/ui/listwidget.c @@ -87,6 +87,7 @@ void init_ListWidget(iListWidget *d) { d->hoverItem = iInvalidPos; d->dragItem = iInvalidPos; d->dragOrigin = zero_I2(); + d->dragHandleWidth = 0; init_Click(&d->click, d, SDL_BUTTON_LEFT); init_IntSet(&d->invalidItems); d->visBuf = new_VisBuf(); @@ -192,6 +193,13 @@ void setScrollMode_ListWidget(iListWidget *d, enum iScrollMode mode) { d->scrollMode = mode; } +void setDragHandleWidth_ListWidget(iListWidget *d, int dragHandleWidth) { + d->dragHandleWidth = dragHandleWidth; + if (dragHandleWidth == 0) { + setFlags_Widget(as_Widget(d), touchDrag_WidgetFlag, iFalse); /* mobile drag handles */ + } +} + void scrollOffset_ListWidget(iListWidget *d, int offset) { moveSpan_SmoothScroll(&d->scrollY, offset, 0); } @@ -353,6 +361,7 @@ static iBool endDrag_ListWidget_(iListWidget *d, iInt2 endPos) { if (d->dragItem == iInvalidPos) { return iFalse; } + setFlags_Widget(as_Widget(d), touchDrag_WidgetFlag, iFalse); /* mobile drag handles */ stop_Anim(&d->scrollY.pos); enum iDragDestination dstKind; const size_t index = resolveDragDestination_ListWidget_(d, endPos, &dstKind); @@ -441,6 +450,17 @@ static iBool processEvent_ListWidget_(iListWidget *d, const SDL_Event *ev) { } } if (ev->type == SDL_MOUSEWHEEL && isHover_Widget(w)) { + if (d->dragHandleWidth) { + if (d->dragItem == iInvalidPos) { + const iInt2 wpos = coord_MouseWheelEvent(&ev->wheel); + if (contains_Widget(w, wpos) && + wpos.x >= right_Rect(boundsWithoutVisualOffset_Widget(w)) - d->dragHandleWidth) { + setFlags_Widget(w, touchDrag_WidgetFlag, iTrue); + printf("[%p] touch drag started\n", d); + return iTrue; + } + } + } if (isScrollDisabled_ListWidget_(d, ev)) { if (ev->wheel.which == SDL_TOUCH_MOUSEID) { /* TODO: Could generalize this selection of the scrollable parent. */ @@ -498,7 +518,9 @@ static iBool processEvent_ListWidget_(iListWidget *d, const SDL_Event *ev) { return iTrue; } redrawHoverItem_ListWidget_(d); - if (contains_Rect(itemRect_ListWidget(d, d->hoverItem), pos_Click(&d->click)) && + if (contains_Rect(adjusted_Rect(itemRect_ListWidget(d, d->hoverItem), + zero_I2(), init_I2(-d->dragHandleWidth, 0)), + pos_Click(&d->click)) && d->hoverItem != iInvalidPos) { postCommand_Widget(w, "list.clicked arg:%zu item:%p", d->hoverItem, constHoverItem_ListWidget(d)); @@ -598,8 +620,9 @@ static void draw_ListWidget_(const iListWidget *d) { } setClip_Paint(&p, bounds_Widget(w)); draw_VisBuf(d->visBuf, addY_I2(topLeft_Rect(bounds), -scrollY), ySpan_Rect(bounds)); - const iInt2 mousePos = mouseCoord_Window(get_Window(), 0); - if (d->dragItem != iInvalidPos && contains_Rect(bounds, mousePos)) { + const iBool isMobile = (deviceType_App() != desktop_AppDeviceType); + const iInt2 mousePos = mouseCoord_Window(get_Window(), isMobile ? SDL_TOUCH_MOUSEID : 0); + if (d->dragItem != iInvalidPos && (isMobile || contains_Rect(bounds, mousePos))) { iInt2 pos = add_I2(mousePos, d->dragOrigin); const iListItem *item = constAt_PtrArray(&d->items, d->dragItem); const iRect itemRect = { init_I2(left_Rect(bounds), pos.y), diff --git a/src/ui/listwidget.h b/src/ui/listwidget.h index 081109e8..c5d412dd 100644 --- a/src/ui/listwidget.h +++ b/src/ui/listwidget.h @@ -66,6 +66,7 @@ struct Impl_ListWidget { size_t hoverItem; size_t dragItem; iInt2 dragOrigin; /* offset from mouse to drag item's top-left corner */ + int dragHandleWidth; iClick click; iIntSet invalidItems; iVisBuf *visBuf; @@ -90,6 +91,7 @@ int scrollPos_ListWidget (const iListWidget *); void setScrollPos_ListWidget (iListWidget *, int pos); void setScrollMode_ListWidget (iListWidget *, enum iScrollMode mode); +void setDragHandleWidth_ListWidget(iListWidget *, int dragHandleWidth); void scrollToItem_ListWidget (iListWidget *, size_t index, uint32_t span); void scrollOffset_ListWidget (iListWidget *, int offset); void scrollOffsetSpan_ListWidget (iListWidget *, int offset, uint32_t span); diff --git a/src/ui/mobile.c b/src/ui/mobile.c index ab282a86..633b33b8 100644 --- a/src/ui/mobile.c +++ b/src/ui/mobile.c @@ -243,6 +243,9 @@ static iBool topPanelHandler_(iWidget *topPanel, const char *cmd) { else if (findWidget_App("upload")) { postCommand_App("upload.cancel"); } + else if (findWidget_App("bmed.sidebar")) { + postCommand_App("bmed.cancel"); + } else if (findWidget_App("ident")) { postCommand_Widget(topPanel, "ident.cancel"); } diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index 48802ddf..b7e3b19a 100644 --- a/src/ui/sidebarwidget.c +++ b/src/ui/sidebarwidget.c @@ -117,7 +117,7 @@ struct Impl_SidebarWidget { iCertListWidget * certList; iWidget * actions; /* below the list, area for buttons */ int midHeight; /* on portrait phone, the height for the middle state */ - iBool isBeingDraggedVertically; /* on portrait phone, sidebar can be dragged up/down */ + iBool isEditing; /* mobile edit mode */ int modeScroll[max_SidebarMode]; iLabelWidget * modeButtons[max_SidebarMode]; int maxButtonLabelWidth; @@ -235,7 +235,18 @@ static iBool isBookmarkFolded_SidebarWidget_(const iSidebarWidget *d, const iBoo } static iBool isSlidingSheet_SidebarWidget_(const iSidebarWidget *d) { - return isPortraitPhone_App();// && scrollPos_ListWidget(d->list) <= 0; + return isPortraitPhone_App(); +} + +static void setMobileEditMode_SidebarWidget_(iSidebarWidget *d, iBool editing) { + iWidget *w = as_Widget(d); + d->isEditing = editing; + setFlags_Widget(findChild_Widget(w, "sidebar.close"), hidden_WidgetFlag, editing); + setFlags_Widget(child_Widget(d->actions, 0), hidden_WidgetFlag, !editing); + setTextCStr_LabelWidget(child_Widget(as_Widget(d->actions), 2), + editing ? "${sidebar.close}" : "${sidebar.action.bookmarks.edit}"); + setDragHandleWidth_ListWidget(d->list, editing ? itemHeight_ListWidget(d->list) * 3 / 2 : 0); + arrange_Widget(d->actions); } static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepActions) { @@ -482,6 +493,14 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct { "---", 0, 0, NULL }, { reload_Icon " ${bookmarks.reload}", 0, 0, "bookmarks.reload.remote" } }, 6); + if (isMobile) { + addActionButton_SidebarWidget_(d, "${sidebar.action.bookmarks.newfolder}", + "bookmarks.addfolder", !d->isEditing ? hidden_WidgetFlag : 0); + addChildFlags_Widget(d->actions, iClob(new_Widget()), expand_WidgetFlag); + iLabelWidget *btn = addActionButton_SidebarWidget_(d, + d->isEditing ? "${sidebar.close}" : "${sidebar.action.bookmarks.edit}", + "sidebar.bookmarks.edit", 0); + } break; } case history_SidebarMode: { @@ -545,7 +564,6 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct addChildFlags_Widget(d->actions, iClob(new_Widget()), expand_WidgetFlag); iLabelWidget *btn = addActionButton_SidebarWidget_(d, "${sidebar.action.history.clear}", "history.clear confirm:1", 0); - setFont_LabelWidget(btn, uiLabelBigBold_FontId); } break; } @@ -740,7 +758,7 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) { d->mode = -1; d->feedsMode = all_FeedsMode; d->midHeight = 0; - d->isBeingDraggedVertically = iFalse; + d->isEditing = iFalse; d->numUnreadEntries = 0; d->buttonFont = uiLabel_FontId; /* wiil be changed later */ d->itemFonts[0] = uiContent_FontId; @@ -918,6 +936,12 @@ static void itemClicked_SidebarWidget_(iSidebarWidget *d, iSidebarItem *item, si break; } case bookmarks_SidebarMode: + if (d->isEditing) { + d->contextItem = item; + d->contextIndex = itemIndex; + postCommand_Widget(d, "bookmark.edit"); + break; + } if (isEmpty_String(&item->url)) /* a folder */ { if (contains_IntSet(d->closedFolders, item->id)) { remove_IntSet(d->closedFolders, item->id); @@ -1174,6 +1198,9 @@ static iBool handleSidebarCommand_SidebarWidget_(iSidebarWidget *d, const char * } else { setVisualOffset_Widget(w, bottom_Rect(rect_Root(w->root)) - w->rect.pos.y, 300, animFlags); + if (d->isEditing) { + setMobileEditMode_SidebarWidget_(d, iFalse); + } } showToolbar_Root(w->root, isHiding); } @@ -1189,6 +1216,10 @@ static iBool handleSidebarCommand_SidebarWidget_(iSidebarWidget *d, const char * refresh_Widget(w->parent); return iTrue; } + else if (equal_Command(cmd, "bookmarks.edit")) { + setMobileEditMode_SidebarWidget_(d, !d->isEditing); + invalidate_ListWidget(d->list); + } return iFalse; } @@ -1853,6 +1884,7 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect, &Class_SidebarWidget); const iBool isMenuVisible = isVisible_Widget(sidebar->menu); const iBool isDragging = constDragItem_ListWidget(list) == d; + const iBool isEditing = sidebar->isEditing; /* only on mobile */ const iBool isPressing = isMouseDown_ListWidget(list) && !isDragging; const iBool isHover = (!isMenuVisible && @@ -2006,6 +2038,16 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect, drawRange_Text(font, textPos, fg, range_String(&d->label)); const int metaFont = uiLabel_FontId; const int metaIconWidth = 4.5f * gap_UI; + if (isEditing) { + iRect dragRect = { + addX_I2(topRight_Rect(itemRect), -itemHeight * 3 / 2), + init_I2(itemHeight * 3 / 2, itemHeight) + }; + fillRect_Paint(p, dragRect, bg); + drawVLine_Paint(p, topLeft_Rect(dragRect), height_Rect(dragRect), uiSeparator_ColorId); + drawCentered_Text(uiContent_FontId, dragRect, iTrue, uiAnnotation_ColorId, menu_Icon); + adjustEdges_Rect(&itemRect, 0, -width_Rect(dragRect), 0, 0); + } const iInt2 metaPos = init_I2(right_Rect(itemRect) - length_String(&d->meta) * -- cgit v1.2.3 From 2963641491200a6d67edd51af3b986af35faa04b Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 4 Dec 2021 13:01:33 +0200 Subject: Fixed UI glitches after sidebar changes --- src/ui/root.c | 5 +++-- src/ui/sidebarwidget.c | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ui/root.c b/src/ui/root.c index 65fc11d1..0e949246 100644 --- a/src/ui/root.c +++ b/src/ui/root.c @@ -489,9 +489,10 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) { iSidebarWidget *sidebar = findChild_Widget(root, "sidebar"); removeChild_Widget(parent_Widget(sidebar), sidebar); if (isLandscape_App()) { - addChildPos_Widget(findChild_Widget(root, "tabs.content"), iClob(sidebar), front_WidgetAddPos); + setVisualOffset_Widget(as_Widget(sidebar), 0, 0, 0); + addChildPos_Widget(findChild_Widget(root, "tabs.content"), iClob(sidebar), front_WidgetAddPos); setWidth_SidebarWidget(sidebar, 73.0f); - setFlags_Widget(as_Widget(sidebar), fixedHeight_WidgetFlag, iFalse); + setFlags_Widget(as_Widget(sidebar), fixedHeight_WidgetFlag | fixedPosition_WidgetFlag, iFalse); } else { addChild_Widget(root, iClob(sidebar)); diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index b7e3b19a..62abd58f 100644 --- a/src/ui/sidebarwidget.c +++ b/src/ui/sidebarwidget.c @@ -1289,6 +1289,7 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) checkModeButtonLayout_SidebarWidget_(d); if (deviceType_App() == phone_AppDeviceType) { // && d->side == left_SidebarSide) { // setFlags_Widget(w, rightEdgeDraggable_WidgetFlag, isPortrait_App()); + setFlags_Widget(findChild_Widget(w, "sidebar.title"), hidden_WidgetFlag, isLandscape_App()); setFlags_Widget(findChild_Widget(w, "sidebar.close"), hidden_WidgetFlag, isLandscape_App()); /* In landscape, visibility of the toolbar is controlled separately. */ if (isVisible_Widget(w)) { -- cgit v1.2.3 From f8c0c817c2ce1919f71ef333439c9f4742fc6a12 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 4 Dec 2021 14:40:37 +0200 Subject: ListWidget: Fixed touch scrolling issue --- src/ui/listwidget.c | 5 ++++- src/ui/listwidget.h | 1 + src/ui/sidebarwidget.c | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/ui/listwidget.c b/src/ui/listwidget.c index ccc5b104..1d0f1729 100644 --- a/src/ui/listwidget.c +++ b/src/ui/listwidget.c @@ -386,6 +386,8 @@ static iBool isScrollDisabled_ListWidget_(const iListWidget *d, const SDL_Event dir = iSign(ev->wheel.y); } switch (d->scrollMode) { + case disabled_ScrollMode: + return iTrue; case disabledAtTopBothDirections_ScrollMode: return scrollPos_ListWidget(d) <= 0; case disabledAtTopUpwards_ScrollMode: @@ -401,7 +403,8 @@ static iBool processEvent_ListWidget_(iListWidget *d, const SDL_Event *ev) { if (isMetricsChange_UserEvent(ev)) { invalidate_ListWidget(d); } - else if (!isScrollDisabled_ListWidget_(d, ev) && processEvent_SmoothScroll(&d->scrollY, ev)) { + else if (!isScrollDisabled_ListWidget_(d, ev) && + processEvent_SmoothScroll(&d->scrollY, ev)) { return iTrue; } else if (isCommand_SDLEvent(ev)) { diff --git a/src/ui/listwidget.h b/src/ui/listwidget.h index c5d412dd..da215b19 100644 --- a/src/ui/listwidget.h +++ b/src/ui/listwidget.h @@ -55,6 +55,7 @@ enum iScrollMode { normal_ScrollMode, disabledAtTopBothDirections_ScrollMode, disabledAtTopUpwards_ScrollMode, + disabled_ScrollMode, }; struct Impl_ListWidget { diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index 62abd58f..d91c85af 100644 --- a/src/ui/sidebarwidget.c +++ b/src/ui/sidebarwidget.c @@ -1812,7 +1812,7 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) w->rect.size.y = height_Rect(rootRect); } else { - setScrollMode_ListWidget(d->list, disabledAtTopBothDirections_ScrollMode); + setScrollMode_ListWidget(d->list, disabled_ScrollMode); } arrange_Widget(w); refresh_Widget(w); -- cgit v1.2.3 From 368eddea42200ec95faa4f4767a72b2b3cf7bdff Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 4 Dec 2021 17:55:38 +0200 Subject: iOS: Setting up a system-provided text input control --- src/ios.h | 20 ++++++++++ src/ios.m | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++- src/ui/inputwidget.c | 50 +++++++++++++++++++++-- 3 files changed, 176 insertions(+), 5 deletions(-) diff --git a/src/ios.h b/src/ios.h index 85177409..2908d833 100644 --- a/src/ios.h +++ b/src/ios.h @@ -61,3 +61,23 @@ iBool isPaused_AVFAudioPlayer (const iAVFAudioPlayer *); void clearNowPlayingInfo_iOS (void); void updateNowPlayingInfo_iOS (void); + +/*----------------------------------------------------------------------------------------------*/ + +enum iSystemTextInputFlags { + multiLine_SystemTextInputFlags = iBit(1), + returnGo_SystemTextInputFlags = iBit(2), + returnSend_SystemTextInputFlags = iBit(3), + disableAutocorrect_SystemTextInputFlag = iBit(4), + alignRight_SystemTextInputFlag = iBit(5), +}; + +iDeclareType(SystemTextInput) +iDeclareTypeConstructionArgs(SystemTextInput, int flags) + +void setRect_SystemTextInput (iSystemTextInput *, iRect rect); +void setText_SystemTextInput (iSystemTextInput *, const iString *text); +void setFont_SystemTextInput (iSystemTextInput *, int fontId); +void setTextChangedFunc_SystemTextInput (iSystemTextInput *, void (*textChangedFunc)(iSystemTextInput *, void *), void *); + +const iString * text_SystemTextInput (const iSystemTextInput *); diff --git a/src/ios.m b/src/ios.m index b46fb8dc..be1e644f 100644 --- a/src/ios.m +++ b/src/ios.m @@ -60,6 +60,8 @@ static UIViewController *viewController_(iWindow *window) { return NULL; } +static void notifyChange_SystemTextInput_(iSystemTextInput *); + /*----------------------------------------------------------------------------------------------*/ API_AVAILABLE(ios(13.0)) @@ -159,9 +161,10 @@ API_AVAILABLE(ios(13.0)) /*----------------------------------------------------------------------------------------------*/ -@interface AppState : NSObject { +@interface AppState : NSObject { iString *fileBeingSaved; iString *pickFileCommand; + iSystemTextInput *sysCtrl; } @property (nonatomic, assign) BOOL isHapticsAvailable; @property (nonatomic, strong) NSObject *haptic; @@ -175,9 +178,18 @@ static AppState *appState_; self = [super init]; fileBeingSaved = NULL; pickFileCommand = NULL; + sysCtrl = NULL; return self; } +-(void)setSystemTextInput:(iSystemTextInput *)sys { + sysCtrl = sys; +} + +-(iSystemTextInput *)systemTextInput { + return sysCtrl; +} + -(void)setPickFileCommand:(const char *)command { if (!pickFileCommand) { pickFileCommand = new_String(); @@ -256,6 +268,21 @@ didPickDocumentsAtURLs:(NSArray *)urls { -(void)keyboardOffScreen:(NSNotification *)notification { setKeyboardHeight_MainWindow(get_MainWindow(), 0); } + +- (BOOL)textFieldShouldReturn:(UITextField *)textField { + SDL_Event ev = { .type = SDL_KEYDOWN }; + ev.key.keysym.sym = SDLK_RETURN; + SDL_PushEvent(&ev); + printf("Return pressed\n"); + return NO; +} + +- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { + iSystemTextInput *sysCtrl = [appState_ systemTextInput]; + notifyChange_SystemTextInput_(sysCtrl); + return YES; +} + @end static void enableMouse_(iBool yes) { @@ -607,3 +634,85 @@ iBool isStarted_AVFAudioPlayer(const iAVFAudioPlayer *d) { iBool isPaused_AVFAudioPlayer(const iAVFAudioPlayer *d) { return d->state == paused_AVFAudioPlayerState; } + +/*----------------------------------------------------------------------------------------------*/ + +struct Impl_SystemTextInput { + void *ctrl; + void (*textChangedFunc)(iSystemTextInput *, void *); + void *textChangedContext; +}; + +iDefineTypeConstructionArgs(SystemTextInput, (int flags), flags) + +#define REF_d_ctrl (__bridge UITextField *)d->ctrl + +void init_SystemTextInput(iSystemTextInput *d, int flags) { + d->ctrl = (void *) CFBridgingRetain([[UITextField alloc] init]); + UITextField *field = REF_d_ctrl; + // TODO: Use the right font: https://developer.apple.com/documentation/uikit/text_display_and_fonts/adding_a_custom_font_to_your_app?language=objc + [[viewController_(get_Window()) view] addSubview:REF_d_ctrl]; + if (flags & returnGo_SystemTextInputFlags) { + [field setReturnKeyType:UIReturnKeyGo]; + } + if (flags & returnSend_SystemTextInputFlags) { + [field setReturnKeyType:UIReturnKeySend]; + } + if (flags & disableAutocorrect_SystemTextInputFlag) { + [field setAutocorrectionType:UITextAutocorrectionTypeNo]; + [field setAutocapitalizationType:UITextAutocapitalizationTypeNone]; + [field setSpellCheckingType:UITextSpellCheckingTypeNo]; + } + if (flags & alignRight_WidgetFlag) { + [field setTextAlignment:NSTextAlignmentRight]; + } + [field setDelegate:appState_]; + [field becomeFirstResponder]; + d->textChangedFunc = NULL; + d->textChangedContext = NULL; + [appState_ setSystemTextInput:d]; +} + +void deinit_SystemTextInput(iSystemTextInput *d) { + [appState_ setSystemTextInput:nil]; + [REF_d_ctrl removeFromSuperview]; + d->ctrl = nil; // TODO: Does this need to be released?? +} + +void setText_SystemTextInput(iSystemTextInput *d, const iString *text) { + [REF_d_ctrl setText:[NSString stringWithUTF8String:cstr_String(text)]]; + [REF_d_ctrl selectAll:nil]; +} + +void setFont_SystemTextInput(iSystemTextInput *d, int fontId) { + int height = lineHeight_Text(fontId); + UIFont *font = [UIFont systemFontOfSize:0.65f * height / get_Window()->pixelRatio]; + [REF_d_ctrl setFont:font]; +} + +const iString *text_SystemTextInput(const iSystemTextInput *d) { + return collectNewCStr_String([[REF_d_ctrl text] cStringUsingEncoding:NSUTF8StringEncoding]);; +} + +void setRect_SystemTextInput(iSystemTextInput *d, iRect rect) { + const iWindow *win = get_Window(); + CGRect frame; + frame.origin.x = rect.pos.x / win->pixelRatio; + frame.origin.y = (rect.pos.y - gap_UI + 2) / win->pixelRatio; + frame.size.width = rect.size.x / win->pixelRatio; + frame.size.height = rect.size.y / win->pixelRatio; + [REF_d_ctrl setFrame:frame]; +} + +void setTextChangedFunc_SystemTextInput(iSystemTextInput *d, + void (*textChangedFunc)(iSystemTextInput *, void *), + void *context) { + d->textChangedFunc = textChangedFunc; + d->textChangedContext = context; +} + +static void notifyChange_SystemTextInput_(iSystemTextInput *d) { + if (d && d->textChangedFunc) { + d->textChangedFunc(d, d->textChangedContext); + } +} diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index bd9b63b5..bb3851df 100644 --- a/src/ui/inputwidget.c +++ b/src/ui/inputwidget.c @@ -40,6 +40,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ # include "macos.h" #endif +#if defined (iPlatformAppleMobile) +# include "ios.h" +# define LAGRANGE_ENABLE_SYSTEM_INPUT 1 +#endif + static const int refreshInterval_InputWidget_ = 512; static const size_t maxUndo_InputWidget_ = 64; static const int unlimitedWidth_InputWidget_ = 1000000; /* TODO: WrapText disables some functionality if maxWidth==0 */ @@ -233,6 +238,7 @@ struct Impl_InputWidget { void * validatorContext; iString * backupPath; int backupTimer; + iSystemTextInput *sysCtrl; }; iDefineObjectConstructionArgs(InputWidget, (size_t maxLen), maxLen) @@ -721,10 +727,12 @@ void init_InputWidget(iInputWidget *d, size_t maxLen) { d->buffered = NULL; d->backupPath = NULL; d->backupTimer = 0; + d->sysCtrl = NULL; updateMetrics_InputWidget_(d); } void deinit_InputWidget(iInputWidget *d) { + delete_SystemTextInput(d->sysCtrl); if (d->backupTimer) { SDL_RemoveTimer(d->backupTimer); } @@ -953,6 +961,9 @@ void setText_InputWidget(iInputWidget *d, const iString *text) { iString *nfcText = collect_String(copy_String(text)); normalize_String(nfcText); splitToLines_(nfcText, &d->lines); + if (d->sysCtrl) { + setText_SystemTextInput(d->sysCtrl, nfcText); + } iAssert(!isEmpty_Array(&d->lines)); iForEach(Array, i, &d->lines) { updateLine_InputWidget_(d, i.value); /* count number of visible lines */ @@ -1008,6 +1019,16 @@ iLocalDef iBool isEditing_InputWidget_(const iInputWidget *d) { return (flags_Widget(constAs_Widget(d)) & selected_WidgetFlag) != 0; } +static void contentsWereChanged_InputWidget_(iInputWidget *); + +#if defined (LAGRANGE_ENABLE_SYSTEM_INPUT) +void systemInputChanged_InputWidget_(iSystemTextInput *sysCtrl, void *widget) { + iInputWidget *d = widget; + splitToLines_(text_SystemTextInput(sysCtrl), &d->lines); + contentsWereChanged_InputWidget_(d); +} +#endif + void begin_InputWidget(iInputWidget *d) { iWidget *w = as_Widget(d); if (isEditing_InputWidget_(d)) { @@ -1016,7 +1037,18 @@ void begin_InputWidget(iInputWidget *d) { } invalidateBuffered_InputWidget_(d); setFlags_Widget(w, hidden_WidgetFlag | disabled_WidgetFlag, iFalse); + setFlags_Widget(w, selected_WidgetFlag, iTrue); mergeLines_(&d->lines, &d->oldText); +#if defined (LAGRANGE_ENABLE_SYSTEM_INPUT) + d->sysCtrl = new_SystemTextInput((d->inFlags & isUrl_InputWidgetFlag ? disableAutocorrect_SystemTextInputFlag : 0) | + (!cmp_String(id_Widget(w), "url") ? returnGo_SystemTextInputFlags : 0) ); +// (flags_Widget(w) & alignRight_WidgetFlag ? alignRight_SystemTextInputFlag : 0)); + setFont_SystemTextInput(d->sysCtrl, d->font); + setRect_SystemTextInput(d->sysCtrl, contentBounds_InputWidget_(d)); + setText_SystemTextInput(d->sysCtrl, &d->oldText); + setTextChangedFunc_SystemTextInput(d->sysCtrl, systemInputChanged_InputWidget_, d); + return; +#endif if (d->mode == overwrite_InputMode) { d->cursor = zero_I2(); } @@ -1025,7 +1057,6 @@ void begin_InputWidget(iInputWidget *d) { d->cursor.x = iMin(d->cursor.x, cursorLine_InputWidget_(d)->range.end); } SDL_StartTextInput(); - setFlags_Widget(w, selected_WidgetFlag, iTrue); showCursor_InputWidget_(d); refresh_Widget(w); startOrStopCursorTimer_InputWidget_(d, iTrue); @@ -1048,15 +1079,22 @@ void end_InputWidget(iInputWidget *d, iBool accept) { /* Was not active. */ return; } - enableEditorKeysInMenus_(iTrue); - if (!accept) { + if (d->sysCtrl) { + if (accept) { + splitToLines_(text_SystemTextInput(d->sysCtrl), &d->lines); + } + delete_SystemTextInput(d->sysCtrl); + d->sysCtrl = NULL; + } + else if (!accept) { /* Overwrite the edited lines. */ splitToLines_(&d->oldText, &d->lines); + SDL_StopTextInput(); } + enableEditorKeysInMenus_(iTrue); d->inFlags |= needUpdateBuffer_InputWidgetFlag; d->inFlags &= ~isMarking_InputWidgetFlag; startOrStopCursorTimer_InputWidget_(d, iFalse); - SDL_StopTextInput(); setFlags_Widget(w, selected_WidgetFlag | keepOnTop_WidgetFlag | touchDrag_WidgetFlag, iFalse); const char *id = cstr_String(id_Widget(as_Widget(d))); if (!*id) id = "_"; @@ -2324,6 +2362,10 @@ static void draw_InputWidget_(const iInputWidget *d) { isFocused ? gap_UI / 4 : 1, isFocused ? uiInputFrameFocused_ColorId : isHover ? uiInputFrameHover_ColorId : uiInputFrame_ColorId); + if (d->sysCtrl) { + drawChildren_Widget(w); + return; + } setClip_Paint(&p, adjusted_Rect(bounds, init_I2(d->leftPadding, 0), init_I2(-d->rightPadding, w->flags & extraPadding_WidgetFlag ? -gap_UI / 2 : 0))); const iRect contentBounds = contentBounds_InputWidget_(d); -- cgit v1.2.3 From 63d39ae7a8f2c10bb2304edefee8dd1034b96693 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 4 Dec 2021 19:07:53 +0200 Subject: Mobile: Swiping back from bookmark editors --- src/ui/mobile.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ui/mobile.c b/src/ui/mobile.c index 633b33b8..90dfafde 100644 --- a/src/ui/mobile.c +++ b/src/ui/mobile.c @@ -243,7 +243,8 @@ static iBool topPanelHandler_(iWidget *topPanel, const char *cmd) { else if (findWidget_App("upload")) { postCommand_App("upload.cancel"); } - else if (findWidget_App("bmed.sidebar")) { + else if (findWidget_App("bmed.sidebar") || findWidget_App("bmed.create") || + findWidget_App("bmed")) { postCommand_App("bmed.cancel"); } else if (findWidget_App("ident")) { -- cgit v1.2.3 From 8c0ce575d06e10ac9f2ec60c0816443005533d53 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 4 Dec 2021 19:08:36 +0200 Subject: iOS: Position system input controls during animation --- src/app.c | 12 +++++++++++- src/ui/inputwidget.c | 12 ++++++++++++ src/ui/root.c | 8 ++++++++ src/ui/root.h | 7 +++++++ src/ui/widget.c | 1 + 5 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/app.c b/src/app.c index 587efd00..52072d39 100644 --- a/src/app.c +++ b/src/app.c @@ -1455,11 +1455,17 @@ backToMainLoop:; static void runTickers_App_(iApp *d) { const uint32_t now = SDL_GetTicks(); d->elapsedSinceLastTicker = (d->lastTickerTime ? now - d->lastTickerTime : 0); - d->lastTickerTime = now; + d->lastTickerTime = now; if (isEmpty_SortedArray(&d->tickers)) { d->lastTickerTime = 0; return; } + iForIndices(i, d->window->base.roots) { + iRoot *root = d->window->base.roots[i]; + if (root) { + root->didAnimateVisualOffsets = iFalse; + } + } /* Tickers may add themselves again, so we'll run off a copy. */ iSortedArray *pending = copy_SortedArray(&d->tickers); clear_SortedArray(&d->tickers); @@ -1476,6 +1482,10 @@ static void runTickers_App_(iApp *d) { if (isEmpty_SortedArray(&d->tickers)) { d->lastTickerTime = 0; } + iForIndices(i, d->window->base.roots) { + iRoot *root = d->window->base.roots[i]; + notifyVisualOffsetChange_Root(root); + } } static int resizeWatcher_(void *user, SDL_Event *event) { diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index bb3851df..e72ed361 100644 --- a/src/ui/inputwidget.c +++ b/src/ui/inputwidget.c @@ -756,6 +756,16 @@ void deinit_InputWidget(iInputWidget *d) { deinit_Array(&d->lines); } +#if defined (LAGRANGE_ENABLE_SYSTEM_INPUT) +static void updateAfterVisualOffsetChange_InputWidget_(iInputWidget *d, iRoot *root) { + iAssert(as_Widget(d)->root == root); + iUnused(root); + if (d->sysCtrl) { + setRect_SystemTextInput(d->sysCtrl, contentBounds_InputWidget_(d)); + } +} +#endif + void setFont_InputWidget(iInputWidget *d, int fontId) { d->font = fontId; updateMetrics_InputWidget_(d); @@ -1047,6 +1057,7 @@ void begin_InputWidget(iInputWidget *d) { setRect_SystemTextInput(d->sysCtrl, contentBounds_InputWidget_(d)); setText_SystemTextInput(d->sysCtrl, &d->oldText); setTextChangedFunc_SystemTextInput(d->sysCtrl, systemInputChanged_InputWidget_, d); + iConnect(Root, w->root, visualOffsetsChanged, d, updateAfterVisualOffsetChange_InputWidget_); return; #endif if (d->mode == overwrite_InputMode) { @@ -1080,6 +1091,7 @@ void end_InputWidget(iInputWidget *d, iBool accept) { return; } if (d->sysCtrl) { + iDisconnect(Root, w->root, visualOffsetsChanged, d, updateAfterVisualOffsetChange_InputWidget_); if (accept) { splitToLines_(text_SystemTextInput(d->sysCtrl), &d->lines); } diff --git a/src/ui/root.c b/src/ui/root.c index 0e949246..5a579eb4 100644 --- a/src/ui/root.c +++ b/src/ui/root.c @@ -241,6 +241,7 @@ static int loadAnimIndex_ = 0; static iRoot * activeRoot_ = NULL; iDefineTypeConstruction(Root) +iDefineAudienceGetter(Root, visualOffsetsChanged) void init_Root(iRoot *d) { iZap(*d); @@ -250,6 +251,7 @@ void deinit_Root(iRoot *d) { iReleasePtr(&d->widget); delete_PtrArray(d->onTop); delete_PtrSet(d->pendingDestruction); + delete_Audience(d->visualOffsetsChanged); } void setCurrent_Root(iRoot *root) { @@ -658,6 +660,12 @@ void updateToolbarColors_Root(iRoot *d) { #endif } +void notifyVisualOffsetChange_Root(iRoot *d) { + if (d && d->didAnimateVisualOffsets) { + iNotifyAudience(d, visualOffsetsChanged, RootVisualOffsetsChanged); + } +} + void dismissPortraitPhoneSidebars_Root(iRoot *d) { if (deviceType_App() == phone_AppDeviceType && isPortrait_App()) { iWidget *sidebar = findChild_Widget(d->widget, "sidebar"); diff --git a/src/ui/root.h b/src/ui/root.h index 851d927d..2419b599 100644 --- a/src/ui/root.h +++ b/src/ui/root.h @@ -2,11 +2,15 @@ #include "widget.h" #include "color.h" +#include #include #include iDeclareType(Root) +iDeclareNotifyFunc(Root, VisualOffsetsChanged) +iDeclareAudienceGetter(Root, visualOffsetsChanged) + struct Impl_Root { iWidget * widget; iWindow * window; @@ -14,6 +18,8 @@ struct Impl_Root { iPtrSet * pendingDestruction; iBool pendingArrange; int loadAnimTimer; + iBool didAnimateVisualOffsets; + iAudience *visualOffsetsChanged; /* called after running tickers */ iColor tmPalette[tmMax_ColorId]; /* theme-specific palette */ }; @@ -36,6 +42,7 @@ void updatePadding_Root (iRoot *); /* TODO: is part of m void dismissPortraitPhoneSidebars_Root (iRoot *); void showToolbar_Root (iRoot *, iBool show); void updateToolbarColors_Root (iRoot *); +void notifyVisualOffsetChange_Root (iRoot *); iInt2 size_Root (const iRoot *); iRect rect_Root (const iRoot *); diff --git a/src/ui/widget.c b/src/ui/widget.c index 0d20cca9..225c0480 100644 --- a/src/ui/widget.c +++ b/src/ui/widget.c @@ -142,6 +142,7 @@ void init_Widget(iWidget *d) { static void visualOffsetAnimation_Widget_(void *ptr) { iWidget *d = ptr; postRefresh_App(); + d->root->didAnimateVisualOffsets = iTrue; if (!isFinished_Anim(&d->visualOffset)) { addTicker_App(visualOffsetAnimation_Widget_, ptr); } -- cgit v1.2.3 From 8050511b683d89c5ef907bc461b98e9aaaa051a7 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 4 Dec 2021 19:18:41 +0200 Subject: iOS: Right-aligned input fields --- src/ios.m | 2 +- src/ui/inputwidget.c | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ios.m b/src/ios.m index be1e644f..59d02fc4 100644 --- a/src/ios.m +++ b/src/ios.m @@ -663,7 +663,7 @@ void init_SystemTextInput(iSystemTextInput *d, int flags) { [field setAutocapitalizationType:UITextAutocapitalizationTypeNone]; [field setSpellCheckingType:UITextSpellCheckingTypeNo]; } - if (flags & alignRight_WidgetFlag) { + if (flags & alignRight_SystemTextInputFlag) { [field setTextAlignment:NSTextAlignmentRight]; } [field setDelegate:appState_]; diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index e72ed361..1c6b8d9e 100644 --- a/src/ui/inputwidget.c +++ b/src/ui/inputwidget.c @@ -1051,8 +1051,8 @@ void begin_InputWidget(iInputWidget *d) { mergeLines_(&d->lines, &d->oldText); #if defined (LAGRANGE_ENABLE_SYSTEM_INPUT) d->sysCtrl = new_SystemTextInput((d->inFlags & isUrl_InputWidgetFlag ? disableAutocorrect_SystemTextInputFlag : 0) | - (!cmp_String(id_Widget(w), "url") ? returnGo_SystemTextInputFlags : 0) ); -// (flags_Widget(w) & alignRight_WidgetFlag ? alignRight_SystemTextInputFlag : 0)); + (!cmp_String(id_Widget(w), "url") ? returnGo_SystemTextInputFlags : 0) | + (flags_Widget(w) & alignRight_WidgetFlag ? alignRight_SystemTextInputFlag : 0)); setFont_SystemTextInput(d->sysCtrl, d->font); setRect_SystemTextInput(d->sysCtrl, contentBounds_InputWidget_(d)); setText_SystemTextInput(d->sysCtrl, &d->oldText); -- cgit v1.2.3 From b8656d5a2e0f540d47d2dc5ccc269f709dd0b923 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 5 Dec 2021 07:54:15 +0200 Subject: iOS: Multiline text input using UITextView --- src/ios.h | 14 +++-- src/ios.m | 158 ++++++++++++++++++++++++++++++++++++++++----------- src/ui/inputwidget.c | 16 +++++- 3 files changed, 148 insertions(+), 40 deletions(-) diff --git a/src/ios.h b/src/ios.h index 2908d833..ff07b2e8 100644 --- a/src/ios.h +++ b/src/ios.h @@ -65,15 +65,16 @@ void updateNowPlayingInfo_iOS (void); /*----------------------------------------------------------------------------------------------*/ enum iSystemTextInputFlags { - multiLine_SystemTextInputFlags = iBit(1), - returnGo_SystemTextInputFlags = iBit(2), - returnSend_SystemTextInputFlags = iBit(3), - disableAutocorrect_SystemTextInputFlag = iBit(4), - alignRight_SystemTextInputFlag = iBit(5), + selectAll_SystemTextInputFlags = iBit(1), + multiLine_SystemTextInputFlags = iBit(2), + returnGo_SystemTextInputFlags = iBit(3), + returnSend_SystemTextInputFlags = iBit(4), + disableAutocorrect_SystemTextInputFlag = iBit(5), + alignRight_SystemTextInputFlag = iBit(6), }; iDeclareType(SystemTextInput) -iDeclareTypeConstructionArgs(SystemTextInput, int flags) +iDeclareTypeConstructionArgs(SystemTextInput, iRect rect, int flags) void setRect_SystemTextInput (iSystemTextInput *, iRect rect); void setText_SystemTextInput (iSystemTextInput *, const iString *text); @@ -81,3 +82,4 @@ void setFont_SystemTextInput (iSystemTextInput *, int fontId); void setTextChangedFunc_SystemTextInput (iSystemTextInput *, void (*textChangedFunc)(iSystemTextInput *, void *), void *); const iString * text_SystemTextInput (const iSystemTextInput *); +int preferredHeight_SystemTextInput (const iSystemTextInput *); diff --git a/src/ios.m b/src/ios.m index 59d02fc4..02659c55 100644 --- a/src/ios.m +++ b/src/ios.m @@ -161,7 +161,7 @@ API_AVAILABLE(ios(13.0)) /*----------------------------------------------------------------------------------------------*/ -@interface AppState : NSObject { +@interface AppState : NSObject { iString *fileBeingSaved; iString *pickFileCommand; iSystemTextInput *sysCtrl; @@ -273,16 +273,21 @@ didPickDocumentsAtURLs:(NSArray *)urls { SDL_Event ev = { .type = SDL_KEYDOWN }; ev.key.keysym.sym = SDLK_RETURN; SDL_PushEvent(&ev); - printf("Return pressed\n"); return NO; } -- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { +- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range +replacementString:(NSString *)string { iSystemTextInput *sysCtrl = [appState_ systemTextInput]; notifyChange_SystemTextInput_(sysCtrl); return YES; } +- (void)textViewDidChange:(UITextView *)textView { + iSystemTextInput *sysCtrl = [appState_ systemTextInput]; + notifyChange_SystemTextInput_(sysCtrl); +} + @end static void enableMouse_(iBool yes) { @@ -538,6 +543,10 @@ void init_AVFAudioPlayer(iAVFAudioPlayer *d) { void deinit_AVFAudioPlayer(iAVFAudioPlayer *d) { setInput_AVFAudioPlayer(d, NULL, NULL); + if (d->player) { + CFBridgingRelease(d->player); + d->player = nil; + } } static const char *cacheDir_ = "~/Library/Caches/Audio"; @@ -638,70 +647,155 @@ iBool isPaused_AVFAudioPlayer(const iAVFAudioPlayer *d) { /*----------------------------------------------------------------------------------------------*/ struct Impl_SystemTextInput { - void *ctrl; + int flags; + void *field; /* single-line text field */ + void *view; /* multi-line text view */ void (*textChangedFunc)(iSystemTextInput *, void *); void *textChangedContext; }; -iDefineTypeConstructionArgs(SystemTextInput, (int flags), flags) +iDefineTypeConstructionArgs(SystemTextInput, (iRect rect, int flags), rect, flags) -#define REF_d_ctrl (__bridge UITextField *)d->ctrl +#define REF_d_field (__bridge UITextField *)d->field +#define REF_d_view (__bridge UITextView *)d->view -void init_SystemTextInput(iSystemTextInput *d, int flags) { - d->ctrl = (void *) CFBridgingRetain([[UITextField alloc] init]); - UITextField *field = REF_d_ctrl; +static CGRect convertToCGRect_(const iRect *rect) { + const iWindow *win = get_Window(); + CGRect frame; + // TODO: Convert coordinates properly! + frame.origin.x = rect->pos.x / win->pixelRatio; + frame.origin.y = (rect->pos.y - gap_UI + 2) / win->pixelRatio; + frame.size.width = rect->size.x / win->pixelRatio; + frame.size.height = rect->size.y / win->pixelRatio; + return frame; +} + +static UIColor *makeUIColor_(enum iColorId colorId) { + iColor color = get_Color(colorId); + return [UIColor colorWithRed:color.r / 255.0 + green:color.g / 255.0 + blue:color.b / 255.0 + alpha:color.a / 255.0]; +} + +void init_SystemTextInput(iSystemTextInput *d, iRect rect, int flags) { + d->flags = flags; + d->field = NULL; + d->view = NULL; + CGRect frame = convertToCGRect_(&rect); + if (flags & multiLine_SystemTextInputFlags) { + d->view = (void *) CFBridgingRetain([[UITextView alloc] initWithFrame:frame textContainer:nil]); + [[viewController_(get_Window()) view] addSubview:REF_d_view]; + } + else { + d->field = (void *) CFBridgingRetain([[UITextField alloc] initWithFrame:frame]); + [[viewController_(get_Window()) view] addSubview:REF_d_field]; + } + UIControl *traits = (UIControl *) (d->view ? REF_d_view : REF_d_field); // TODO: Use the right font: https://developer.apple.com/documentation/uikit/text_display_and_fonts/adding_a_custom_font_to_your_app?language=objc - [[viewController_(get_Window()) view] addSubview:REF_d_ctrl]; if (flags & returnGo_SystemTextInputFlags) { - [field setReturnKeyType:UIReturnKeyGo]; + [traits setReturnKeyType:UIReturnKeyGo]; } if (flags & returnSend_SystemTextInputFlags) { - [field setReturnKeyType:UIReturnKeySend]; + [traits setReturnKeyType:UIReturnKeySend]; } if (flags & disableAutocorrect_SystemTextInputFlag) { - [field setAutocorrectionType:UITextAutocorrectionTypeNo]; - [field setAutocapitalizationType:UITextAutocapitalizationTypeNone]; - [field setSpellCheckingType:UITextSpellCheckingTypeNo]; + [traits setAutocorrectionType:UITextAutocorrectionTypeNo]; + [traits setAutocapitalizationType:UITextAutocapitalizationTypeNone]; + [traits setSpellCheckingType:UITextSpellCheckingTypeNo]; } if (flags & alignRight_SystemTextInputFlag) { - [field setTextAlignment:NSTextAlignmentRight]; + if (d->field) { + [REF_d_field setTextAlignment:NSTextAlignmentRight]; + } + } + UIColor *textColor = makeUIColor_(uiInputTextFocused_ColorId); + UIColor *backgroundColor = makeUIColor_(uiInputBackgroundFocused_ColorId); + [appState_ setSystemTextInput:d]; + if (d->field) { + [REF_d_field setTextColor:textColor]; + [REF_d_field setDelegate:appState_]; + [REF_d_field becomeFirstResponder]; + } + else { + [REF_d_view setTextColor:textColor]; + [REF_d_view setBackgroundColor:backgroundColor]; + [REF_d_view setEditable:YES]; +// [REF_d_view setSelectable:YES]; + [REF_d_view setDelegate:appState_]; + [REF_d_view becomeFirstResponder]; } - [field setDelegate:appState_]; - [field becomeFirstResponder]; d->textChangedFunc = NULL; d->textChangedContext = NULL; - [appState_ setSystemTextInput:d]; } void deinit_SystemTextInput(iSystemTextInput *d) { [appState_ setSystemTextInput:nil]; - [REF_d_ctrl removeFromSuperview]; - d->ctrl = nil; // TODO: Does this need to be released?? + if (d->field) { + [REF_d_field removeFromSuperview]; + CFBridgingRelease(d->field); + d->field = nil; + } + if (d->view) { + [REF_d_view removeFromSuperview]; + CFBridgingRelease(d->view); + d->view = nil; + } } void setText_SystemTextInput(iSystemTextInput *d, const iString *text) { - [REF_d_ctrl setText:[NSString stringWithUTF8String:cstr_String(text)]]; - [REF_d_ctrl selectAll:nil]; + NSString *str = [NSString stringWithUTF8String:cstr_String(text)]; + if (d->field) { + [REF_d_field setText:str]; + if (d->flags & selectAll_SystemTextInputFlags) { + [REF_d_field selectAll:nil]; + } + } + else { + [REF_d_view setText:str]; + if (d->flags & selectAll_SystemTextInputFlags) { + [REF_d_view selectAll:nil]; + } + } +} + +int preferredHeight_SystemTextInput(const iSystemTextInput *d) { + if (d->view) { + CGRect usedRect = [[REF_d_view layoutManager] usedRectForTextContainer:[REF_d_view textContainer]]; + return usedRect.size.height * get_Window()->pixelRatio; + } + return 0; } void setFont_SystemTextInput(iSystemTextInput *d, int fontId) { int height = lineHeight_Text(fontId); UIFont *font = [UIFont systemFontOfSize:0.65f * height / get_Window()->pixelRatio]; - [REF_d_ctrl setFont:font]; + if (d->field) { + [REF_d_field setFont:font]; + } + if (d->view) { + [REF_d_view setFont:font]; + } } const iString *text_SystemTextInput(const iSystemTextInput *d) { - return collectNewCStr_String([[REF_d_ctrl text] cStringUsingEncoding:NSUTF8StringEncoding]);; + if (d->field) { + return collectNewCStr_String([[REF_d_field text] cStringUsingEncoding:NSUTF8StringEncoding]); + } + if (d->view) { + return collectNewCStr_String([[REF_d_view text] cStringUsingEncoding:NSUTF8StringEncoding]); + } + return NULL; } void setRect_SystemTextInput(iSystemTextInput *d, iRect rect) { - const iWindow *win = get_Window(); - CGRect frame; - frame.origin.x = rect.pos.x / win->pixelRatio; - frame.origin.y = (rect.pos.y - gap_UI + 2) / win->pixelRatio; - frame.size.width = rect.size.x / win->pixelRatio; - frame.size.height = rect.size.y / win->pixelRatio; - [REF_d_ctrl setFrame:frame]; + CGRect frame = convertToCGRect_(&rect); + if (d->field) { + [REF_d_field setFrame:frame]; + } + else { + [REF_d_view setFrame:frame]; + } } void setTextChangedFunc_SystemTextInput(iSystemTextInput *d, diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index 1c6b8d9e..ea7ed523 100644 --- a/src/ui/inputwidget.c +++ b/src/ui/inputwidget.c @@ -596,10 +596,19 @@ static size_t length_InputWidget_(const iInputWidget *d) { } static int contentHeight_InputWidget_(const iInputWidget *d) { + if (d->sysCtrl) { + const int preferred = preferredHeight_SystemTextInput(d->sysCtrl); + return iClamp(preferred, + d->minWrapLines * lineHeight_Text(d->font), + d->maxWrapLines * lineHeight_Text(d->font)); + } return size_Range(&d->visWrapLines) * lineHeight_Text(d->font); } static void updateTextInputRect_InputWidget_(const iInputWidget *d) { + if (d->sysCtrl) { + setRect_SystemTextInput(d->sysCtrl, contentBounds_InputWidget_(d)); + } #if !defined (iPlatformAppleMobile) const iRect bounds = bounds_Widget(constAs_Widget(d)); SDL_SetTextInputRect(&(SDL_Rect){ bounds.pos.x, bounds.pos.y, bounds.size.x, bounds.size.y }); @@ -1036,6 +1045,7 @@ void systemInputChanged_InputWidget_(iSystemTextInput *sysCtrl, void *widget) { iInputWidget *d = widget; splitToLines_(text_SystemTextInput(sysCtrl), &d->lines); contentsWereChanged_InputWidget_(d); + updateMetrics_InputWidget_(d); } #endif @@ -1050,14 +1060,16 @@ void begin_InputWidget(iInputWidget *d) { setFlags_Widget(w, selected_WidgetFlag, iTrue); mergeLines_(&d->lines, &d->oldText); #if defined (LAGRANGE_ENABLE_SYSTEM_INPUT) - d->sysCtrl = new_SystemTextInput((d->inFlags & isUrl_InputWidgetFlag ? disableAutocorrect_SystemTextInputFlag : 0) | + d->sysCtrl = new_SystemTextInput(contentBounds_InputWidget_(d), + (d->maxWrapLines > 1 ? multiLine_SystemTextInputFlags : 0) | + (d->inFlags & isUrl_InputWidgetFlag ? disableAutocorrect_SystemTextInputFlag : 0) | (!cmp_String(id_Widget(w), "url") ? returnGo_SystemTextInputFlags : 0) | (flags_Widget(w) & alignRight_WidgetFlag ? alignRight_SystemTextInputFlag : 0)); setFont_SystemTextInput(d->sysCtrl, d->font); - setRect_SystemTextInput(d->sysCtrl, contentBounds_InputWidget_(d)); setText_SystemTextInput(d->sysCtrl, &d->oldText); setTextChangedFunc_SystemTextInput(d->sysCtrl, systemInputChanged_InputWidget_, d); iConnect(Root, w->root, visualOffsetsChanged, d, updateAfterVisualOffsetChange_InputWidget_); + updateMetrics_InputWidget_(d); return; #endif if (d->mode == overwrite_InputMode) { -- cgit v1.2.3 From 6b4d15c83232d3af564157afdc32dfb5573c2aef Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 5 Dec 2021 08:10:41 +0200 Subject: iOS: Monospace text view font --- src/ios.m | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/ios.m b/src/ios.m index 02659c55..290d4804 100644 --- a/src/ios.m +++ b/src/ios.m @@ -768,8 +768,14 @@ int preferredHeight_SystemTextInput(const iSystemTextInput *d) { } void setFont_SystemTextInput(iSystemTextInput *d, int fontId) { - int height = lineHeight_Text(fontId); - UIFont *font = [UIFont systemFontOfSize:0.65f * height / get_Window()->pixelRatio]; + float height = lineHeight_Text(fontId) / get_Window()->pixelRatio; + UIFont *font; + if (fontId / maxVariants_Fonts * maxVariants_Fonts == monospace_FontId) { + font = [UIFont monospacedSystemFontOfSize:0.8f * height weight:UIFontWeightRegular]; + } + else { + font = [UIFont systemFontOfSize:0.65f * height]; + } if (d->field) { [REF_d_field setFont:font]; } -- cgit v1.2.3 From be8fcebadb4a12844dd367d1f7da03bbe920ea0a Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 5 Dec 2021 10:25:44 +0200 Subject: iOS: Custom editor font and adjusted positioning --- CMakeLists.txt | 8 ++++- res/iOSBundleInfo.plist.in | 4 +++ src/ios.h | 4 ++- src/ios.m | 81 +++++++++++++++++++++++++++++++++++++--------- src/ui/inputwidget.c | 16 ++++++--- 5 files changed, 90 insertions(+), 23 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4200c7c5..2d11e43e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -104,6 +104,12 @@ endif () set (EMB_BIN ${CMAKE_CURRENT_BINARY_DIR}/resources.lgr) make_resources (${EMB_BIN} ${RESOURCES}) set_source_files_properties (${EMB_BIN} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) +if (IOS) + set (EMB_FONTS res/fonts/SourceSans3-Regular.ttf) + set_source_files_properties (${EMB_FONTS} + PROPERTIES MACOSX_PACKAGE_LOCATION Resources + ) +endif () # Source files. set (SOURCES @@ -287,7 +293,7 @@ else () endif () # Target. -add_executable (app ${SOURCES} ${RESOURCES}) +add_executable (app ${SOURCES} ${RESOURCES} ${EMB_FONTS}) set_property (TARGET app PROPERTY C_STANDARD 11) if (TARGET ext-deps) add_dependencies (app ext-deps) diff --git a/res/iOSBundleInfo.plist.in b/res/iOSBundleInfo.plist.in index 09af4abf..5a02ed68 100644 --- a/res/iOSBundleInfo.plist.in +++ b/res/iOSBundleInfo.plist.in @@ -49,6 +49,10 @@ UILaunchStoryboardName LaunchScreen + UIAppFonts + + SourceSans3-Regular.ttf + UIBackgroundModes audio diff --git a/src/ios.h b/src/ios.h index ff07b2e8..6c5ec8d5 100644 --- a/src/ios.h +++ b/src/ios.h @@ -70,7 +70,9 @@ enum iSystemTextInputFlags { returnGo_SystemTextInputFlags = iBit(3), returnSend_SystemTextInputFlags = iBit(4), disableAutocorrect_SystemTextInputFlag = iBit(5), - alignRight_SystemTextInputFlag = iBit(6), + disableAutocapitalize_SystemTextInputFlag = iBit(6), + alignRight_SystemTextInputFlag = iBit(7), + insertNewlines_SystemTextInputFlag = iBit(8), }; iDeclareType(SystemTextInput) diff --git a/src/ios.m b/src/ios.m index 290d4804..47303b7f 100644 --- a/src/ios.m +++ b/src/ios.m @@ -61,6 +61,7 @@ static UIViewController *viewController_(iWindow *window) { } static void notifyChange_SystemTextInput_(iSystemTextInput *); +static BOOL isNewlineAllowed_SystemTextInput_(const iSystemTextInput *); /*----------------------------------------------------------------------------------------------*/ @@ -269,10 +270,19 @@ didPickDocumentsAtURLs:(NSArray *)urls { setKeyboardHeight_MainWindow(get_MainWindow(), 0); } -- (BOOL)textFieldShouldReturn:(UITextField *)textField { +static void sendReturnKeyPress_(void) { SDL_Event ev = { .type = SDL_KEYDOWN }; + ev.key.timestamp = SDL_GetTicks(); ev.key.keysym.sym = SDLK_RETURN; + ev.key.state = SDL_PRESSED; + SDL_PushEvent(&ev); + ev.type = SDL_KEYUP; + ev.key.state = SDL_RELEASED; SDL_PushEvent(&ev); +} + +- (BOOL)textFieldShouldReturn:(UITextField *)textField { + sendReturnKeyPress_(); return NO; } @@ -283,6 +293,17 @@ replacementString:(NSString *)string { return YES; } +- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range + replacementText:(NSString *)text { + if ([text isEqualToString:@"\n"]) { + if (!isNewlineAllowed_SystemTextInput_([appState_ systemTextInput])) { + sendReturnKeyPress_(); + return NO; + } + } + return YES; +} + - (void)textViewDidChange:(UITextView *)textView { iSystemTextInput *sysCtrl = [appState_ systemTextInput]; notifyChange_SystemTextInput_(sysCtrl); @@ -659,14 +680,23 @@ iDefineTypeConstructionArgs(SystemTextInput, (iRect rect, int flags), rect, flag #define REF_d_field (__bridge UITextField *)d->field #define REF_d_view (__bridge UITextView *)d->view -static CGRect convertToCGRect_(const iRect *rect) { +static CGRect convertToCGRect_(const iRect *rect, iBool expanded) { const iWindow *win = get_Window(); CGRect frame; // TODO: Convert coordinates properly! frame.origin.x = rect->pos.x / win->pixelRatio; - frame.origin.y = (rect->pos.y - gap_UI + 2) / win->pixelRatio; + frame.origin.y = (rect->pos.y - gap_UI + 1) / win->pixelRatio; frame.size.width = rect->size.x / win->pixelRatio; frame.size.height = rect->size.y / win->pixelRatio; + /* Some padding to account for insets. If we just zero out the insets, the insertion point + may be clipped at the edges. */ + if (expanded) { + const float inset = gap_UI / get_Window()->pixelRatio; + frame.origin.x -= inset + 1; + frame.origin.y -= inset + 1; + frame.size.width += 2 * inset + 2; + frame.size.height += inset + 1; + } return frame; } @@ -682,7 +712,7 @@ void init_SystemTextInput(iSystemTextInput *d, iRect rect, int flags) { d->flags = flags; d->field = NULL; d->view = NULL; - CGRect frame = convertToCGRect_(&rect); + CGRect frame = convertToCGRect_(&rect, (flags & multiLine_SystemTextInputFlags) != 0); if (flags & multiLine_SystemTextInputFlags) { d->view = (void *) CFBridgingRetain([[UITextView alloc] initWithFrame:frame textContainer:nil]); [[viewController_(get_Window()) view] addSubview:REF_d_view]; @@ -692,7 +722,9 @@ void init_SystemTextInput(iSystemTextInput *d, iRect rect, int flags) { [[viewController_(get_Window()) view] addSubview:REF_d_field]; } UIControl *traits = (UIControl *) (d->view ? REF_d_view : REF_d_field); - // TODO: Use the right font: https://developer.apple.com/documentation/uikit/text_display_and_fonts/adding_a_custom_font_to_your_app?language=objc + if (~flags & insertNewlines_SystemTextInputFlag) { + [traits setReturnKeyType:UIReturnKeyDone]; + } if (flags & returnGo_SystemTextInputFlags) { [traits setReturnKeyType:UIReturnKeyGo]; } @@ -701,29 +733,38 @@ void init_SystemTextInput(iSystemTextInput *d, iRect rect, int flags) { } if (flags & disableAutocorrect_SystemTextInputFlag) { [traits setAutocorrectionType:UITextAutocorrectionTypeNo]; - [traits setAutocapitalizationType:UITextAutocapitalizationTypeNone]; [traits setSpellCheckingType:UITextSpellCheckingTypeNo]; } + if (flags & disableAutocapitalize_SystemTextInputFlag) { + [traits setAutocapitalizationType:UITextAutocapitalizationTypeNone]; + } if (flags & alignRight_SystemTextInputFlag) { if (d->field) { [REF_d_field setTextAlignment:NSTextAlignmentRight]; } + if (d->view) { + [REF_d_view setTextAlignment:NSTextAlignmentRight]; + } } UIColor *textColor = makeUIColor_(uiInputTextFocused_ColorId); UIColor *backgroundColor = makeUIColor_(uiInputBackgroundFocused_ColorId); [appState_ setSystemTextInput:d]; if (d->field) { - [REF_d_field setTextColor:textColor]; - [REF_d_field setDelegate:appState_]; - [REF_d_field becomeFirstResponder]; + UITextField *field = REF_d_field; + [field setTextColor:textColor]; + [field setDelegate:appState_]; + [field becomeFirstResponder]; } else { - [REF_d_view setTextColor:textColor]; - [REF_d_view setBackgroundColor:backgroundColor]; - [REF_d_view setEditable:YES]; + UITextView *view = REF_d_view; + [view setTextColor:textColor]; + [view setBackgroundColor:backgroundColor]; + [view setEditable:YES]; // [REF_d_view setSelectable:YES]; - [REF_d_view setDelegate:appState_]; - [REF_d_view becomeFirstResponder]; + const float inset = gap_UI / get_Window()->pixelRatio; + //[view setTextContainerInset:(UIEdgeInsets){ inset - 1, -inset - 1, 0, -inset - 1 }]; + [view setDelegate:appState_]; + [view becomeFirstResponder]; } d->textChangedFunc = NULL; d->textChangedContext = NULL; @@ -774,7 +815,11 @@ void setFont_SystemTextInput(iSystemTextInput *d, int fontId) { font = [UIFont monospacedSystemFontOfSize:0.8f * height weight:UIFontWeightRegular]; } else { - font = [UIFont systemFontOfSize:0.65f * height]; +// font = [UIFont systemFontOfSize:0.65f * height]; +// for (NSString *name in [UIFont fontNamesForFamilyName:@"Source Sans 3"]) { +// printf("fontname: %s\n", [name cStringUsingEncoding:NSUTF8StringEncoding]); +// } + font = [UIFont fontWithName:@"SourceSans3-Regular" size:height * 0.7f]; } if (d->field) { [REF_d_field setFont:font]; @@ -795,7 +840,7 @@ const iString *text_SystemTextInput(const iSystemTextInput *d) { } void setRect_SystemTextInput(iSystemTextInput *d, iRect rect) { - CGRect frame = convertToCGRect_(&rect); + CGRect frame = convertToCGRect_(&rect, (d->flags & multiLine_SystemTextInputFlags) != 0); if (d->field) { [REF_d_field setFrame:frame]; } @@ -816,3 +861,7 @@ static void notifyChange_SystemTextInput_(iSystemTextInput *d) { d->textChangedFunc(d, d->textChangedContext); } } + +static BOOL isNewlineAllowed_SystemTextInput_(const iSystemTextInput *d) { + return (d->flags & insertNewlines_SystemTextInputFlag) != 0; +} diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index ea7ed523..ad45ed92 100644 --- a/src/ui/inputwidget.c +++ b/src/ui/inputwidget.c @@ -765,6 +765,12 @@ void deinit_InputWidget(iInputWidget *d) { deinit_Array(&d->lines); } +static iBool isAllowedToInsertNewline_InputWidget_(const iInputWidget *d) { + return ~d->inFlags & isSensitive_InputWidgetFlag && + ~d->inFlags & isUrl_InputWidgetFlag && + d->inFlags & lineBreaksEnabled_InputWidgetFlag && d->maxLen == 0; +} + #if defined (LAGRANGE_ENABLE_SYSTEM_INPUT) static void updateAfterVisualOffsetChange_InputWidget_(iInputWidget *d, iRoot *root) { iAssert(as_Widget(d)->root == root); @@ -1062,9 +1068,11 @@ void begin_InputWidget(iInputWidget *d) { #if defined (LAGRANGE_ENABLE_SYSTEM_INPUT) d->sysCtrl = new_SystemTextInput(contentBounds_InputWidget_(d), (d->maxWrapLines > 1 ? multiLine_SystemTextInputFlags : 0) | - (d->inFlags & isUrl_InputWidgetFlag ? disableAutocorrect_SystemTextInputFlag : 0) | + (d->inFlags & isUrl_InputWidgetFlag ? (disableAutocorrect_SystemTextInputFlag | + disableAutocapitalize_SystemTextInputFlag) : 0) | (!cmp_String(id_Widget(w), "url") ? returnGo_SystemTextInputFlags : 0) | - (flags_Widget(w) & alignRight_WidgetFlag ? alignRight_SystemTextInputFlag : 0)); + (flags_Widget(w) & alignRight_WidgetFlag ? alignRight_SystemTextInputFlag : 0) | + (isAllowedToInsertNewline_InputWidget_(d) ? insertNewlines_SystemTextInputFlag : 0)); setFont_SystemTextInput(d->sysCtrl, d->font); setText_SystemTextInput(d->sysCtrl, &d->oldText); setTextChangedFunc_SystemTextInput(d->sysCtrl, systemInputChanged_InputWidget_, d); @@ -2110,9 +2118,7 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { return iTrue; case SDLK_RETURN: case SDLK_KP_ENTER: - if (~d->inFlags & isSensitive_InputWidgetFlag && - ~d->inFlags & isUrl_InputWidgetFlag && - d->inFlags & lineBreaksEnabled_InputWidgetFlag && d->maxLen == 0) { + if (isAllowedToInsertNewline_InputWidget_(d)) { if (checkLineBreakMods_InputWidget_(d, mods)) { pushUndo_InputWidget_(d); deleteMarked_InputWidget_(d); -- cgit v1.2.3 From 78606839925a424bba90205ac6f986f5773fbfcd Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 5 Dec 2021 10:25:55 +0200 Subject: Mobile: Removed unnecessary menu items --- src/ui/root.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui/root.c b/src/ui/root.c index 5a579eb4..e2706898 100644 --- a/src/ui/root.c +++ b/src/ui/root.c @@ -95,7 +95,7 @@ static const iMenuItem tabletNavMenuItems_[] = { { close_Icon " ${menu.closetab}", 'w', KMOD_PRIMARY, "tabs.close" }, { "---" }, { magnifyingGlass_Icon " ${menu.find}", 0, 0, "focus.set id:find.input" }, - { leftHalf_Icon " ${menu.sidebar.left}", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" }, +// { leftHalf_Icon " ${menu.sidebar.left}", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" }, { rightHalf_Icon " ${menu.sidebar.right}", SDLK_p, KMOD_PRIMARY | KMOD_SHIFT, "sidebar2.toggle" }, { "${menu.view.split}", SDLK_j, KMOD_PRIMARY, "splitmenu.open" }, { "---" }, @@ -116,7 +116,7 @@ static const iMenuItem phoneNavMenuItems_[] = { { close_Icon " ${menu.closetab}", 'w', KMOD_PRIMARY, "tabs.close" }, { "---" }, { magnifyingGlass_Icon " ${menu.find}", 0, 0, "focus.set id:find.input" }, - { leftHalf_Icon " ${menu.sidebar}", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" }, +// { leftHalf_Icon " ${menu.sidebar}", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" }, { "---" }, { book_Icon " ${menu.bookmarks.list}", 0, 0, "!open url:about:bookmarks" }, //{ "${menu.downloads}", 0, 0, "downloads.open" }, -- cgit v1.2.3 From f3b1888035c267aad9ce69a71becc8f9eee4fc90 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 5 Dec 2021 15:01:11 +0200 Subject: Cleanup --- src/bookmarks.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/bookmarks.c b/src/bookmarks.c index f733dac3..500caa38 100644 --- a/src/bookmarks.c +++ b/src/bookmarks.c @@ -310,9 +310,6 @@ static void handleKeyValue_BookmarkLoader_(void *context, const iString *table, } else if (!cmp_String(key, "tags") && tv->type == string_TomlType) { set_String(&bm->tags, tv->value.string); - if (strstr(cstr_String(&bm->tags), "subscribed")) { - printf("a\n"); - } unpackDotTags_Bookmark_(bm); } else if (!cmp_String(key, "icon") && tv->type == int64_TomlType) { -- cgit v1.2.3 From 19ef7e89692f4a0d8cb78172b29603fdbb1d79b5 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 5 Dec 2021 15:02:37 +0200 Subject: InputWidget: Simplified version for system-provided input Instead of a tangle of `#if`s, it might be better to have a separate source file for the simple controller of the system-provided input control. --- src/ui/inputwidget.c | 587 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 366 insertions(+), 221 deletions(-) diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index ad45ed92..65710373 100644 --- a/src/ui/inputwidget.c +++ b/src/ui/inputwidget.c @@ -20,6 +20,10 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +/* InputWidget supports both fully custom and system-provided text editing. + The primary source of complexity is the handling of wrapped text content + in the custom text editor. */ + #include "inputwidget.h" #include "command.h" #include "paint.h" @@ -42,13 +46,19 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #if defined (iPlatformAppleMobile) # include "ios.h" -# define LAGRANGE_ENABLE_SYSTEM_INPUT 1 +# define LAGRANGE_USE_SYSTEM_TEXT_INPUT 1 /* System-provided UI control almost handles everything. */ +#else +# define LAGRANGE_USE_SYSTEM_TEXT_INPUT 0 +iDeclareType(SystemTextInput) #endif static const int refreshInterval_InputWidget_ = 512; static const size_t maxUndo_InputWidget_ = 64; static const int unlimitedWidth_InputWidget_ = 1000000; /* TODO: WrapText disables some functionality if maxWidth==0 */ +static const iChar sensitiveChar_ = 0x25cf; /* black circle */ +static const char * sensitive_ = "\u25cf"; + static void enableEditorKeysInMenus_(iBool enable) { #if defined (iPlatformAppleDesktop) enableMenuItemsByKey_MacOS(SDLK_LEFT, KMOD_PRIMARY, enable); @@ -62,7 +72,10 @@ static void enableEditorKeysInMenus_(iBool enable) { #endif } +static void updateMetrics_InputWidget_(iInputWidget *); + /*----------------------------------------------------------------------------------------------*/ +#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT iDeclareType(InputLine) @@ -183,6 +196,8 @@ static void deinit_InputUndo_(iInputUndo *d) { deinit_String(&d->text); } +#endif /* USE_SYSTEM_TEXT_INPUT */ + enum iInputWidgetFlag { isSensitive_InputWidgetFlag = iBit(1), isUrl_InputWidgetFlag = iBit(2), /* affected by decoding preference */ @@ -208,43 +223,45 @@ enum iInputWidgetFlag { struct Impl_InputWidget { iWidget widget; enum iInputMode mode; + int font; int inFlags; size_t maxLen; /* characters */ - iArray lines; /* iInputLine[] */ - iString oldText; /* for restoring if edits cancelled */ - int lastUpdateWidth; iString srcHint; iString hint; int leftPadding; int rightPadding; + int minWrapLines, maxWrapLines; /* min/max number of visible lines allowed */ + iRangei visWrapLines; /* which wrap lines are current visible */ + iClick click; + int wheelAccum; + iTextBuf * buffered; /* pre-rendered static text */ + iInputWidgetValidatorFunc validator; + void * validatorContext; + iString * backupPath; + int backupTimer; + iString oldText; /* for restoring if edits cancelled */ + int lastUpdateWidth; + iSystemTextInput *sysCtrl; +#if LAGRANGE_USE_SYSTEM_TEXT_INPUT + iString text; +#else + iArray lines; /* iInputLine[] */ iInt2 cursor; /* cursor position: x = byte offset, y = line index */ iInt2 prevCursor; /* previous cursor position */ - iRangei visWrapLines; /* which wrap lines are current visible */ - int minWrapLines, maxWrapLines; /* min/max number of visible lines allowed */ iRanges mark; /* TODO: would likely simplify things to use two Int2's for marking; no conversions needed */ iRanges initialMark; iArray undoStack; - int font; - iClick click; uint32_t tapStartTime; uint32_t lastTapTime; iInt2 lastTapPos; int tapCount; - int wheelAccum; int cursorVis; uint32_t timer; - iTextBuf * buffered; /* pre-rendered static text */ - iInputWidgetValidatorFunc validator; - void * validatorContext; - iString * backupPath; - int backupTimer; - iSystemTextInput *sysCtrl; +#endif }; iDefineObjectConstructionArgs(InputWidget, (size_t maxLen), maxLen) -static void updateMetrics_InputWidget_(iInputWidget *); - static void restoreBackup_InputWidget_(iInputWidget *d) { if (!d->backupPath) return; iFile *f = new_File(d->backupPath); @@ -258,17 +275,21 @@ static void saveBackup_InputWidget_(iInputWidget *d) { if (!d->backupPath) return; iFile *f = new_File(d->backupPath); if (open_File(f, writeOnly_FileMode | text_FileMode)) { +#if LAGRANGE_USE_SYSTEM_TEXT_INPUT + write_File(f, utf8_String(&d->text)); +#else iConstForEach(Array, i, &d->lines) { const iInputLine *line = i.value; write_File(f, utf8_String(&line->text)); } - d->inFlags &= ~needBackup_InputWidgetFlag; -#if !defined (NDEBUG) +# if !defined (NDEBUG) iConstForEach(Array, j, &d->lines) { iAssert(endsWith_String(&((const iInputLine *) j.value)->text, "\n") || index_ArrayConstIterator(&j) == size_Array(&d->lines) - 1); } +# endif #endif + d->inFlags &= ~needBackup_InputWidgetFlag; } iRelease(f); } @@ -317,6 +338,12 @@ void setBackupFileName_InputWidget(iInputWidget *d, const char *fileName) { restoreBackup_InputWidget_(d); } +iLocalDef iInt2 padding_(void) { + return init_I2(gap_UI / 2, gap_UI / 2); +} + +#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT + static void clearUndo_InputWidget_(iInputWidget *d) { iForEach(Array, i, &d->undoStack) { deinit_InputUndo_(i.value); @@ -324,10 +351,13 @@ static void clearUndo_InputWidget_(iInputWidget *d) { clear_Array(&d->undoStack); } -iLocalDef iInt2 padding_(void) { - return init_I2(gap_UI / 2, gap_UI / 2); +static const iInputLine *line_InputWidget_(const iInputWidget *d, size_t index) { + iAssert(!isEmpty_Array(&d->lines)); + return constAt_Array(&d->lines, index); } +#endif /* !LAGRANGE_USE_SYSTEM_TEXT_INPUT */ + #define extraPaddingHeight_ (1.25f * gap_UI) static iRect contentBounds_InputWidget_(const iInputWidget *d) { @@ -343,6 +373,25 @@ static iRect contentBounds_InputWidget_(const iInputWidget *d) { return bounds; } +static iWrapText wrap_InputWidget_(const iInputWidget *d, int y) { +#if LAGRANGE_USE_SYSTEM_TEXT_INPUT + iUnused(y); /* full text is wrapped always */ + iRangecc text = range_String(&d->text); +#else + iRangecc text = range_String(&line_InputWidget_(d, y)->text); +#endif + return (iWrapText){ + .text = text, + .maxWidth = d->maxLen == 0 ? width_Rect(contentBounds_InputWidget_(d)) + : unlimitedWidth_InputWidget_, + .mode = + (d->inFlags & isUrl_InputWidgetFlag ? anyCharacter_WrapTextMode : word_WrapTextMode), + .overrideChar = (d->inFlags & isSensitive_InputWidgetFlag ? sensitiveChar_ : 0), + }; +} + +#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT + iLocalDef iBool isLastLine_InputWidget_(const iInputWidget *d, const iInputLine *line) { return (const void *) line == constBack_Array(&d->lines); } @@ -356,11 +405,6 @@ static int numWrapLines_InputWidget_(const iInputWidget *d) { return lastLine_InputWidget_(d)->wrapLines.end; } -static const iInputLine *line_InputWidget_(const iInputWidget *d, size_t index) { - iAssert(!isEmpty_Array(&d->lines)); - return constAt_Array(&d->lines, index); -} - static const iString *lineString_InputWidget_(const iInputWidget *d, int y) { return &line_InputWidget_(d, y)->text; } @@ -461,20 +505,6 @@ static int visLineOffsetY_InputWidget_(const iInputWidget *d) { d->wheelAccum; } -static const iChar sensitiveChar_ = 0x25cf; /* black circle */ -static const char *sensitive_ = "\u25cf"; - -static iWrapText wrap_InputWidget_(const iInputWidget *d, int y) { - return (iWrapText){ - .text = range_String(&line_InputWidget_(d, y)->text), - .maxWidth = d->maxLen == 0 ? width_Rect(contentBounds_InputWidget_(d)) - : unlimitedWidth_InputWidget_, - .mode = - (d->inFlags & isUrl_InputWidgetFlag ? anyCharacter_WrapTextMode : word_WrapTextMode), - .overrideChar = (d->inFlags & isSensitive_InputWidgetFlag ? sensitiveChar_ : 0), - }; -} - static iRangei visibleLineRange_InputWidget_(const iInputWidget *d) { iRangei vis = { -1, -1 }; /* Determine which lines are in the potentially visible range. */ @@ -557,6 +587,19 @@ static void showCursor_InputWidget_(iInputWidget *d) { updateVisible_InputWidget_(d); } +#else /* if LAGRANGE_USE_SYSTEM_TEXT_INPUT */ + +static int visLineOffsetY_InputWidget_(const iInputWidget *d) { + return 0; /* offset for the buffered text */ +} + +static void updateVisible_InputWidget_(iInputWidget *d) { + iUnused(d); + /* TODO: Anything to do? */ +} + +#endif + static void invalidateBuffered_InputWidget_(iInputWidget *d) { if (d->buffered) { delete_TextBuf(d->buffered); @@ -580,11 +623,16 @@ static void updateSizeForFixedLength_InputWidget_(iInputWidget *d) { } static iString *text_InputWidget_(const iInputWidget *d) { +#if LAGRANGE_USE_SYSTEM_TEXT_INPUT + return copy_String(&d->text); +#else iString *text = new_String(); mergeLines_(&d->lines, text); return text; +#endif } +#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT static size_t length_InputWidget_(const iInputWidget *d) { /* Note: `d->length` is kept up to date, so don't call this normally. */ size_t len = 0; @@ -595,42 +643,6 @@ static size_t length_InputWidget_(const iInputWidget *d) { return len; } -static int contentHeight_InputWidget_(const iInputWidget *d) { - if (d->sysCtrl) { - const int preferred = preferredHeight_SystemTextInput(d->sysCtrl); - return iClamp(preferred, - d->minWrapLines * lineHeight_Text(d->font), - d->maxWrapLines * lineHeight_Text(d->font)); - } - return size_Range(&d->visWrapLines) * lineHeight_Text(d->font); -} - -static void updateTextInputRect_InputWidget_(const iInputWidget *d) { - if (d->sysCtrl) { - setRect_SystemTextInput(d->sysCtrl, contentBounds_InputWidget_(d)); - } -#if !defined (iPlatformAppleMobile) - const iRect bounds = bounds_Widget(constAs_Widget(d)); - SDL_SetTextInputRect(&(SDL_Rect){ bounds.pos.x, bounds.pos.y, bounds.size.x, bounds.size.y }); -#endif -} - -static void updateMetrics_InputWidget_(iInputWidget *d) { - iWidget *w = as_Widget(d); - updateSizeForFixedLength_InputWidget_(d); - /* Caller must arrange the width, but the height is set here. */ - const int oldHeight = height_Rect(w->rect); - w->rect.size.y = contentHeight_InputWidget_(d) + 3.0f * padding_().y; /* TODO: Why 3x? */ - if (flags_Widget(w) & extraPadding_WidgetFlag) { - w->rect.size.y += extraPaddingHeight_; - } - invalidateBuffered_InputWidget_(d); - if (height_Rect(w->rect) != oldHeight) { - postCommand_Widget(d, "input.resized"); - updateTextInputRect_InputWidget_(d); - } -} - static void updateLine_InputWidget_(iInputWidget *d, iInputLine *line) { iAssert(endsWith_String(&line->text, "\n") || isLastLine_InputWidget_(d, line)); iWrapText wrapText = wrap_InputWidget_(d, indexOf_Array(&d->lines, line)); @@ -693,6 +705,63 @@ static void startOrStopCursorTimer_InputWidget_(iInputWidget *d, iBool doStart) } } +#else /* using a system-provided text control */ + +static void updateAllLinesAndResizeHeight_InputWidget_(iInputWidget *d) { + /* Rewrap the buffered text and resize accordingly. */ + iWrapText wt = wrap_InputWidget_(d, 0); + const int height = measure_WrapText(&wt, d->font).bounds.size.y; + /* We use this to store the number wrapped lines for determining widget height. */ + d->visWrapLines.start = 0; + d->visWrapLines.end = height / lineHeight_Text(d->font); + updateMetrics_InputWidget_(d); +} + +#endif + +static int contentHeight_InputWidget_(const iInputWidget *d) { +#if LAGRANGE_USE_SYSTEM_TEXT_INPUT + const int minHeight = d->minWrapLines * lineHeight_Text(d->font); + const int maxHeight = d->maxWrapLines * lineHeight_Text(d->font); + if (d->sysCtrl) { + const int preferred = preferredHeight_SystemTextInput(d->sysCtrl); + return iClamp(preferred, minHeight, maxHeight); + } + if (d->buffered && ~d->inFlags & needUpdateBuffer_InputWidgetFlag) { + return iClamp(d->buffered->size.y, minHeight, maxHeight); + } +#endif + return size_Range(&d->visWrapLines) * lineHeight_Text(d->font); +} + +static void updateTextInputRect_InputWidget_(const iInputWidget *d) { +#if LAGRANGE_USE_SYSTEM_TEXT_INPUT + if (d->sysCtrl) { + setRect_SystemTextInput(d->sysCtrl, contentBounds_InputWidget_(d)); + } +#endif +#if !defined (iPlatformAppleMobile) + const iRect bounds = bounds_Widget(constAs_Widget(d)); + SDL_SetTextInputRect(&(SDL_Rect){ bounds.pos.x, bounds.pos.y, bounds.size.x, bounds.size.y }); +#endif +} + +static void updateMetrics_InputWidget_(iInputWidget *d) { + iWidget *w = as_Widget(d); + updateSizeForFixedLength_InputWidget_(d); + /* Caller must arrange the width, but the height is set here. */ + const int oldHeight = height_Rect(w->rect); + w->rect.size.y = contentHeight_InputWidget_(d) + 3.0f * padding_().y; /* TODO: Why 3x? */ + if (flags_Widget(w) & extraPadding_WidgetFlag) { + w->rect.size.y += extraPaddingHeight_; + } + invalidateBuffered_InputWidget_(d); + if (height_Rect(w->rect) != oldHeight) { + postCommand_Widget(d, "input.resized"); + updateTextInputRect_InputWidget_(d); + } +} + void init_InputWidget(iInputWidget *d, size_t maxLen) { iWidget *w = &d->widget; init_Widget(w); @@ -702,37 +771,40 @@ void init_InputWidget(iInputWidget *d, size_t maxLen) { #if defined (iPlatformMobile) setFlags_Widget(w, extraPadding_WidgetFlag, iTrue); #endif +#if LAGRANGE_USE_SYSTEM_TEXT_INPUT + init_String(&d->text); +#else init_Array(&d->lines, sizeof(iInputLine)); + init_Array(&d->undoStack, sizeof(iInputUndo)); + d->cursor = zero_I2(); + d->prevCursor = zero_I2(); + d->lastTapTime = 0; + d->tapCount = 0; + d->timer = 0; + d->cursorVis = 0; + iZap(d->mark); + splitToLines_(&iStringLiteral(""), &d->lines); +#endif init_String(&d->oldText); - init_Array(&d->lines, sizeof(iInputLine)); init_String(&d->srcHint); init_String(&d->hint); - init_Array(&d->undoStack, sizeof(iInputUndo)); d->font = uiInput_FontId | alwaysVariableFlag_FontId; d->leftPadding = 0; d->rightPadding = 0; - d->cursor = zero_I2(); - d->prevCursor = zero_I2(); d->lastUpdateWidth = 0; d->inFlags = eatEscape_InputWidgetFlag | enterKeyEnabled_InputWidgetFlag | lineBreaksEnabled_InputWidgetFlag | useReturnKeyBehavior_InputWidgetFlag; // if (deviceType_App() != desktop_AppDeviceType) { // d->inFlags |= enterKeyInsertsLineFeed_InputWidgetFlag; // } - iZap(d->mark); setMaxLen_InputWidget(d, maxLen); d->visWrapLines.start = 0; d->visWrapLines.end = 1; d->maxWrapLines = maxLen > 0 ? 1 : 20; /* TODO: Choose maximum dynamically? */ d->minWrapLines = 1; - splitToLines_(&iStringLiteral(""), &d->lines); setFlags_Widget(w, fixedHeight_WidgetFlag, iTrue); /* resizes its own height */ init_Click(&d->click, d, SDL_BUTTON_LEFT); - d->lastTapTime = 0; - d->tapCount = 0; d->wheelAccum = 0; - d->timer = 0; - d->cursorVis = 0; d->buffered = NULL; d->backupPath = NULL; d->backupTimer = 0; @@ -741,7 +813,6 @@ void init_InputWidget(iInputWidget *d, size_t maxLen) { } void deinit_InputWidget(iInputWidget *d) { - delete_SystemTextInput(d->sysCtrl); if (d->backupTimer) { SDL_RemoveTimer(d->backupTimer); } @@ -750,19 +821,24 @@ void deinit_InputWidget(iInputWidget *d) { } delete_String(d->backupPath); d->backupPath = NULL; + delete_TextBuf(d->buffered); + deinit_String(&d->srcHint); + deinit_String(&d->hint); + deinit_String(&d->oldText); +#if LAGRANGE_USE_SYSTEM_TEXT_INPUT + delete_SystemTextInput(d->sysCtrl); + deinit_String(&d->text); +#else + startOrStopCursorTimer_InputWidget_(d, iFalse); clearInputLines_(&d->lines); if (isSelected_Widget(d)) { SDL_StopTextInput(); enableEditorKeysInMenus_(iTrue); } - delete_TextBuf(d->buffered); clearUndo_InputWidget_(d); deinit_Array(&d->undoStack); - startOrStopCursorTimer_InputWidget_(d, iFalse); - deinit_String(&d->srcHint); - deinit_String(&d->hint); - deinit_String(&d->oldText); deinit_Array(&d->lines); +#endif } static iBool isAllowedToInsertNewline_InputWidget_(const iInputWidget *d) { @@ -771,7 +847,7 @@ static iBool isAllowedToInsertNewline_InputWidget_(const iInputWidget *d) { d->inFlags & lineBreaksEnabled_InputWidgetFlag && d->maxLen == 0; } -#if defined (LAGRANGE_ENABLE_SYSTEM_INPUT) +#if LAGRANGE_USE_SYSTEM_TEXT_INPUT static void updateAfterVisualOffsetChange_InputWidget_(iInputWidget *d, iRoot *root) { iAssert(as_Widget(d)->root == root); iUnused(root); @@ -786,6 +862,7 @@ void setFont_InputWidget(iInputWidget *d, int fontId) { updateMetrics_InputWidget_(d); } +#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT static void pushUndo_InputWidget_(iInputWidget *d) { iInputUndo undo; init_InputUndo_(&undo, &d->lines, d->cursor); @@ -799,7 +876,6 @@ static void pushUndo_InputWidget_(iInputWidget *d) { static iBool popUndo_InputWidget_(iInputWidget *d) { if (!isEmpty_Array(&d->undoStack)) { iInputUndo *undo = back_Array(&d->undoStack); - //setCopy_Array(&d->text, &undo->text); splitToLines_(&undo->text, &d->lines); d->cursor = undo->cursor; deinit_InputUndo_(undo); @@ -811,6 +887,43 @@ static iBool popUndo_InputWidget_(iInputWidget *d) { return iFalse; } +iLocalDef iInputLine *cursorLine_InputWidget_(iInputWidget *d) { + return at_Array(&d->lines, d->cursor.y); +} + +iLocalDef const iInputLine *constCursorLine_InputWidget_(const iInputWidget *d) { + return constAt_Array(&d->lines, d->cursor.y); +} + +iLocalDef iInt2 cursorMax_InputWidget_(const iInputWidget *d) { + const int yLast = size_Array(&d->lines) - 1; + return init_I2(endX_InputWidget_(d, yLast), yLast); +} + +static size_t cursorToIndex_InputWidget_(const iInputWidget *d, iInt2 pos) { + if (pos.y < 0) { + return 0; + } + if (pos.y >= size_Array(&d->lines)) { + return lastLine_InputWidget_(d)->range.end; + } + const iInputLine *line = line_InputWidget_(d, pos.y); + pos.x = iClamp(pos.x, 0, endX_InputWidget_(d, pos.y)); + return line->range.start + pos.x; +} + +static iInt2 indexToCursor_InputWidget_(const iInputWidget *d, size_t index) { + /* TODO: The lines are sorted; this could use a binary search. */ + iConstForEach(Array, i, &d->lines) { + const iInputLine *line = i.value; + if (contains_Range(&line->range, index)) { + return init_I2(index - line->range.start, index_ArrayConstIterator(&i)); + } + } + return cursorMax_InputWidget_(d); +} +#endif + void setMode_InputWidget(iInputWidget *d, enum iInputMode mode) { d->mode = mode; } @@ -909,7 +1022,11 @@ void setContentPadding_InputWidget(iInputWidget *d, int left, int right) { } iLocalDef iBool isEmpty_InputWidget_(const iInputWidget *d) { +#if LAGRANGE_USE_SYSTEM_TEXT_INPUT + return isEmpty_String(&d->text); +#else return size_Array(&d->lines) == 1 && isEmpty_String(&line_InputWidget_(d, 0)->text); +#endif } static iBool isHintVisible_InputWidget_(const iInputWidget *d) { @@ -919,15 +1036,20 @@ static iBool isHintVisible_InputWidget_(const iInputWidget *d) { static void updateBuffered_InputWidget_(iInputWidget *d) { invalidateBuffered_InputWidget_(d); if (isHintVisible_InputWidget_(d)) { + /* TODO: This should have a "maximum number of lines" parameter! */ d->buffered = newRange_TextBuf(d->font, uiAnnotation_ColorId, range_String(&d->hint)); } else { /* Draw all the potentially visible lines to a buffer. */ +#if LAGRANGE_USE_SYSTEM_TEXT_INPUT + iString *visText = copy_String(&d->text); +#else iString *visText = new_String(); const iRangei visRange = visibleLineRange_InputWidget_(d); for (int i = visRange.start; i < visRange.end; i++) { append_String(visText, &line_InputWidget_(d, i)->text); } +#endif if (d->inFlags & isUrl_InputWidgetFlag) { /* Highlight the host name. */ iUrl parts; @@ -953,19 +1075,6 @@ static void updateBuffered_InputWidget_(iInputWidget *d) { d->inFlags &= ~needUpdateBuffer_InputWidgetFlag; } -iLocalDef iInputLine *cursorLine_InputWidget_(iInputWidget *d) { - return at_Array(&d->lines, d->cursor.y); -} - -iLocalDef const iInputLine *constCursorLine_InputWidget_(const iInputWidget *d) { - return constAt_Array(&d->lines, d->cursor.y); -} - -iLocalDef iInt2 cursorMax_InputWidget_(const iInputWidget *d) { - const int yLast = size_Array(&d->lines) - 1; - return init_I2(endX_InputWidget_(d, yLast), yLast); -} - void setText_InputWidget(iInputWidget *d, const iString *text) { if (!d) return; if (d->inFlags & isUrl_InputWidgetFlag) { @@ -982,13 +1091,11 @@ void setText_InputWidget(iInputWidget *d, const iString *text) { text = omitDefaultScheme_(collect_String(copy_String(text))); } } - clearUndo_InputWidget_(d); iString *nfcText = collect_String(copy_String(text)); normalize_String(nfcText); +#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT + clearUndo_InputWidget_(d); splitToLines_(nfcText, &d->lines); - if (d->sysCtrl) { - setText_SystemTextInput(d->sysCtrl, nfcText); - } iAssert(!isEmpty_Array(&d->lines)); iForEach(Array, i, &d->lines) { updateLine_InputWidget_(d, i.value); /* count number of visible lines */ @@ -998,12 +1105,23 @@ void setText_InputWidget(iInputWidget *d, const iString *text) { if (!isFocused_Widget(d)) { iZap(d->mark); } +#else + set_String(&d->text, nfcText); + if (d->sysCtrl) { + setText_SystemTextInput(d->sysCtrl, nfcText); + } + else { + updateAllLinesAndResizeHeight_InputWidget_(d); /* need to know the new height */ + } +#endif if (!isFocused_Widget(d)) { d->inFlags |= needUpdateBuffer_InputWidgetFlag; } updateVisible_InputWidget_(d); updateMetrics_InputWidget_(d); - refresh_Widget(as_Widget(d)); + if (!d->sysCtrl) { + refresh_Widget(as_Widget(d)); + } } void setTextCStr_InputWidget(iInputWidget *d, const char *cstr) { @@ -1012,32 +1130,11 @@ void setTextCStr_InputWidget(iInputWidget *d, const char *cstr) { delete_String(str); } -static size_t cursorToIndex_InputWidget_(const iInputWidget *d, iInt2 pos) { - if (pos.y < 0) { - return 0; - } - if (pos.y >= size_Array(&d->lines)) { - return lastLine_InputWidget_(d)->range.end; - } - const iInputLine *line = line_InputWidget_(d, pos.y); - pos.x = iClamp(pos.x, 0, endX_InputWidget_(d, pos.y)); - return line->range.start + pos.x; -} - -static iInt2 indexToCursor_InputWidget_(const iInputWidget *d, size_t index) { - /* TODO: The lines are sorted; this could use a binary search. */ - iConstForEach(Array, i, &d->lines) { - const iInputLine *line = i.value; - if (contains_Range(&line->range, index)) { - return init_I2(index - line->range.start, index_ArrayConstIterator(&i)); - } - } - return cursorMax_InputWidget_(d); -} - void selectAll_InputWidget(iInputWidget *d) { +#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT d->mark = (iRanges){ 0, lastLine_InputWidget_(d)->range.end }; refresh_Widget(as_Widget(d)); +#endif } iLocalDef iBool isEditing_InputWidget_(const iInputWidget *d) { @@ -1046,10 +1143,10 @@ iLocalDef iBool isEditing_InputWidget_(const iInputWidget *d) { static void contentsWereChanged_InputWidget_(iInputWidget *); -#if defined (LAGRANGE_ENABLE_SYSTEM_INPUT) +#if LAGRANGE_USE_SYSTEM_TEXT_INPUT void systemInputChanged_InputWidget_(iSystemTextInput *sysCtrl, void *widget) { iInputWidget *d = widget; - splitToLines_(text_SystemTextInput(sysCtrl), &d->lines); + set_String(&d->text, text_SystemTextInput(sysCtrl)); contentsWereChanged_InputWidget_(d); updateMetrics_InputWidget_(d); } @@ -1064,22 +1161,24 @@ void begin_InputWidget(iInputWidget *d) { invalidateBuffered_InputWidget_(d); setFlags_Widget(w, hidden_WidgetFlag | disabled_WidgetFlag, iFalse); setFlags_Widget(w, selected_WidgetFlag, iTrue); - mergeLines_(&d->lines, &d->oldText); -#if defined (LAGRANGE_ENABLE_SYSTEM_INPUT) + d->inFlags &= ~enterPressed_InputWidgetFlag; +#if LAGRANGE_USE_SYSTEM_TEXT_INPUT + set_String(&d->oldText, &d->text); d->sysCtrl = new_SystemTextInput(contentBounds_InputWidget_(d), (d->maxWrapLines > 1 ? multiLine_SystemTextInputFlags : 0) | (d->inFlags & isUrl_InputWidgetFlag ? (disableAutocorrect_SystemTextInputFlag | disableAutocapitalize_SystemTextInputFlag) : 0) | (!cmp_String(id_Widget(w), "url") ? returnGo_SystemTextInputFlags : 0) | (flags_Widget(w) & alignRight_WidgetFlag ? alignRight_SystemTextInputFlag : 0) | - (isAllowedToInsertNewline_InputWidget_(d) ? insertNewlines_SystemTextInputFlag : 0)); + (isAllowedToInsertNewline_InputWidget_(d) ? insertNewlines_SystemTextInputFlag : 0) | + (d->inFlags & selectAllOnFocus_InputWidgetFlag ? selectAll_SystemTextInputFlags : 0)); setFont_SystemTextInput(d->sysCtrl, d->font); setText_SystemTextInput(d->sysCtrl, &d->oldText); setTextChangedFunc_SystemTextInput(d->sysCtrl, systemInputChanged_InputWidget_, d); iConnect(Root, w->root, visualOffsetsChanged, d, updateAfterVisualOffsetChange_InputWidget_); updateMetrics_InputWidget_(d); - return; -#endif +#else + mergeLines_(&d->lines, &d->oldText); if (d->mode == overwrite_InputMode) { d->cursor = zero_I2(); } @@ -1091,7 +1190,6 @@ void begin_InputWidget(iInputWidget *d) { showCursor_InputWidget_(d); refresh_Widget(w); startOrStopCursorTimer_InputWidget_(d, iTrue); - d->inFlags &= ~enterPressed_InputWidgetFlag; if (d->inFlags & selectAllOnFocus_InputWidgetFlag) { d->mark = (iRanges){ 0, lastLine_InputWidget_(d)->range.end }; d->cursor = cursorMax_InputWidget_(d); @@ -1102,6 +1200,7 @@ void begin_InputWidget(iInputWidget *d) { enableEditorKeysInMenus_(iFalse); updateTextInputRect_InputWidget_(d); updateVisible_InputWidget_(d); +#endif } void end_InputWidget(iInputWidget *d, iBool accept) { @@ -1110,23 +1209,29 @@ void end_InputWidget(iInputWidget *d, iBool accept) { /* Was not active. */ return; } +#if LAGRANGE_USE_SYSTEM_TEXT_INPUT if (d->sysCtrl) { iDisconnect(Root, w->root, visualOffsetsChanged, d, updateAfterVisualOffsetChange_InputWidget_); if (accept) { - splitToLines_(text_SystemTextInput(d->sysCtrl), &d->lines); + set_String(&d->text, text_SystemTextInput(d->sysCtrl)); + } + else { + set_String(&d->text, &d->oldText); } delete_SystemTextInput(d->sysCtrl); d->sysCtrl = NULL; } - else if (!accept) { +#else + if (!accept) { /* Overwrite the edited lines. */ splitToLines_(&d->oldText, &d->lines); SDL_StopTextInput(); } enableEditorKeysInMenus_(iTrue); - d->inFlags |= needUpdateBuffer_InputWidgetFlag; d->inFlags &= ~isMarking_InputWidgetFlag; startOrStopCursorTimer_InputWidget_(d, iFalse); +#endif + d->inFlags |= needUpdateBuffer_InputWidgetFlag; setFlags_Widget(w, selected_WidgetFlag | keepOnTop_WidgetFlag | touchDrag_WidgetFlag, iFalse); const char *id = cstr_String(id_Widget(as_Widget(d))); if (!*id) id = "_"; @@ -1138,6 +1243,7 @@ void end_InputWidget(iInputWidget *d, iBool accept) { accept ? 1 : 0); } +#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT static void textOfLinesWasChanged_InputWidget_(iInputWidget *d, iRangei lineRange) { for (int i = lineRange.start; i < lineRange.end; i++) { updateLine_InputWidget_(d, at_Array(&d->lines, i)); @@ -1271,27 +1377,6 @@ static iBool moveCursorByLine_InputWidget_(iInputWidget *d, int dir, int horiz) return iTrue; } -void setSensitiveContent_InputWidget(iInputWidget *d, iBool isSensitive) { - iChangeFlags(d->inFlags, isSensitive_InputWidgetFlag, isSensitive); -} - -void setUrlContent_InputWidget(iInputWidget *d, iBool isUrl) { - iChangeFlags(d->inFlags, isUrl_InputWidgetFlag, isUrl); - d->inFlags |= needUpdateBuffer_InputWidgetFlag; -} - -void setSelectAllOnFocus_InputWidget(iInputWidget *d, iBool selectAllOnFocus) { - iChangeFlags(d->inFlags, selectAllOnFocus_InputWidgetFlag, selectAllOnFocus); -} - -void setNotifyEdits_InputWidget(iInputWidget *d, iBool notifyEdits) { - iChangeFlags(d->inFlags, notifyEdits_InputWidgetFlag, notifyEdits); -} - -void setEatEscape_InputWidget(iInputWidget *d, iBool eatEscape) { - iChangeFlags(d->inFlags, eatEscape_InputWidgetFlag, eatEscape); -} - static iRanges mark_InputWidget_(const iInputWidget *d) { iRanges m = { iMin(d->mark.start, d->mark.end), iMax(d->mark.start, d->mark.end) }; const iInputLine *last = lastLine_InputWidget_(d); @@ -1300,15 +1385,6 @@ static iRanges mark_InputWidget_(const iInputWidget *d) { return m; } -static void contentsWereChanged_InputWidget_(iInputWidget *d) { - if (d->validator) { - d->validator(d, d->validatorContext); /* this may change the contents */ - } - if (d->inFlags & notifyEdits_InputWidgetFlag) { - postCommand_Widget(d, "input.edited id:%s", cstr_String(id_Widget(constAs_Widget(d)))); - } -} - static void deleteIndexRange_InputWidget_(iInputWidget *d, iRanges deleted) { size_t firstModified = iInvalidPos; restartBackupTimer_InputWidget_(d); @@ -1517,6 +1593,42 @@ static void extendRange_InputWidget_(iInputWidget *d, size_t *index, int dir) { *index = cursorToIndex_InputWidget_(d, pos); } +static void lineTextWasChanged_InputWidget_(iInputWidget *d, iInputLine *line) { + const int y = indexOf_Array(&d->lines, line); + textOfLinesWasChanged_InputWidget_(d, (iRangei){ y, y + 1 }); +} +#endif + +void setSensitiveContent_InputWidget(iInputWidget *d, iBool isSensitive) { + iChangeFlags(d->inFlags, isSensitive_InputWidgetFlag, isSensitive); +} + +void setUrlContent_InputWidget(iInputWidget *d, iBool isUrl) { + iChangeFlags(d->inFlags, isUrl_InputWidgetFlag, isUrl); + d->inFlags |= needUpdateBuffer_InputWidgetFlag; +} + +void setSelectAllOnFocus_InputWidget(iInputWidget *d, iBool selectAllOnFocus) { + iChangeFlags(d->inFlags, selectAllOnFocus_InputWidgetFlag, selectAllOnFocus); +} + +void setNotifyEdits_InputWidget(iInputWidget *d, iBool notifyEdits) { + iChangeFlags(d->inFlags, notifyEdits_InputWidgetFlag, notifyEdits); +} + +void setEatEscape_InputWidget(iInputWidget *d, iBool eatEscape) { + iChangeFlags(d->inFlags, eatEscape_InputWidgetFlag, eatEscape); +} + +static void contentsWereChanged_InputWidget_(iInputWidget *d) { + if (d->validator) { + d->validator(d, d->validatorContext); /* this may change the contents */ + } + if (d->inFlags & notifyEdits_InputWidgetFlag) { + postCommand_Widget(d, "input.edited id:%s", cstr_String(id_Widget(constAs_Widget(d)))); + } +} + static iRect bounds_InputWidget_(const iInputWidget *d) { const iWidget *w = constAs_Widget(d); iRect bounds = bounds_Widget(w); @@ -1535,11 +1647,6 @@ static iBool contains_InputWidget_(const iInputWidget *d, iInt2 coord) { return contains_Rect(bounds_InputWidget_(d), coord); } -static void lineTextWasChanged_InputWidget_(iInputWidget *d, iInputLine *line) { - const int y = indexOf_Array(&d->lines, line); - textOfLinesWasChanged_InputWidget_(d, (iRangei){ y, y + 1 }); -} - static iBool isArrowUpDownConsumed_InputWidget_(const iInputWidget *d) { return d->maxWrapLines > 1; } @@ -1564,6 +1671,7 @@ enum iEventResult { true_EventResult = 2, /* event was processed and should not be passed on */ }; +#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT static void markWordAtCursor_InputWidget_(iInputWidget *d) { d->mark.start = d->mark.end = cursorToIndex_InputWidget_(d, d->cursor); extendRange_InputWidget_(d, &d->mark.start, -1); @@ -1580,8 +1688,10 @@ static void showClipMenu_(iInt2 coord) { openMenuFlags_Widget(clipMenu, coord, iFalse); } } +#endif static enum iEventResult processPointerEvents_InputWidget_(iInputWidget *d, const SDL_Event *ev) { +#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT iWidget *w = as_Widget(d); if (ev->type == SDL_MOUSEMOTION && (isHover_Widget(d) || flags_Widget(w) & keepOnTop_WidgetFlag)) { const iInt2 coord = init_I2(ev->motion.x, ev->motion.y); @@ -1655,9 +1765,11 @@ static enum iEventResult processPointerEvents_InputWidget_(iInputWidget *d, cons return true_EventResult; } } +#endif return ignored_EventResult; } +#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT static iInt2 touchCoordCursor_InputWidget_(const iInputWidget *d, iInt2 coord) { /* Clamp to the bounds so the cursor doesn't wrap at the ends. */ iRect bounds = shrunk_Rect(contentBounds_InputWidget_(d), one_I2()); @@ -1679,9 +1791,11 @@ static int distanceToPos_InputWidget_(const iInputWidget *d, iInt2 uiCoord, iInt } return dist_I2(addY_I2(winCoord, lineHeight_Text(d->font) / 2), uiCoord); } +#endif static enum iEventResult processTouchEvents_InputWidget_(iInputWidget *d, const SDL_Event *ev) { iWidget *w = as_Widget(d); +#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT /* + first tap to focus & select all/place cursor + focused tap to place cursor @@ -1923,9 +2037,22 @@ static enum iEventResult processTouchEvents_InputWidget_(iInputWidget *d, const // /* Eat all mouse clicks on the widget. */ // return true_EventResult; // } +#else + /* Just a tap to activate the system-provided text input control. */ + switch (processEvent_Click(&d->click, ev)) { + case none_ClickResult: + break; + case started_ClickResult: + setFocus_Widget(w); + return true_EventResult; + default: + return true_EventResult; + } +#endif return ignored_EventResult; } +#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT static void clampWheelAccum_InputWidget_(iInputWidget *d, int wheel) { if (wheel > 0 && d->visWrapLines.start == 0) { d->wheelAccum = 0; @@ -1936,6 +2063,7 @@ static void clampWheelAccum_InputWidget_(iInputWidget *d, int wheel) { refresh_Widget(d); } } +#endif static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { iWidget *w = as_Widget(d); @@ -1953,13 +2081,6 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { begin_InputWidget(d); return iFalse; } - else if (isEditing_InputWidget_(d) && (isCommand_UserEvent(ev, "window.focus.lost") || - isCommand_UserEvent(ev, "window.focus.gained"))) { - startOrStopCursorTimer_InputWidget_(d, isCommand_UserEvent(ev, "window.focus.gained")); - d->cursorVis = 1; - refresh_Widget(d); - return iFalse; - } else if (isCommand_UserEvent(ev, "keyroot.changed")) { d->inFlags |= needUpdateBuffer_InputWidgetFlag; } @@ -1972,11 +2093,23 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { end_InputWidget(d, iTrue); return iFalse; } +#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT + else if (isEditing_InputWidget_(d) && (isCommand_UserEvent(ev, "window.focus.lost") || + isCommand_UserEvent(ev, "window.focus.gained"))) { + startOrStopCursorTimer_InputWidget_(d, isCommand_UserEvent(ev, "window.focus.gained")); + d->cursorVis = 1; + refresh_Widget(d); + return iFalse; + } else if ((isCommand_UserEvent(ev, "copy") || isCommand_UserEvent(ev, "input.copy")) && isEditing_InputWidget_(d)) { copy_InputWidget_(d, argLabel_Command(command_UserEvent(ev), "cut")); return iTrue; } +// else if (isFocused_Widget(d) && isCommand_UserEvent(ev, "copy")) { +// copy_InputWidget_(d, iFalse); +// return iTrue; +// } else if (isCommand_UserEvent(ev, "input.paste") && isEditing_InputWidget_(d)) { paste_InputWidget_(d); return iTrue; @@ -1992,6 +2125,14 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { selectAll_InputWidget(d); return iTrue; } + else if (isCommand_UserEvent(ev, "text.insert")) { + pushUndo_InputWidget_(d); + deleteMarked_InputWidget_(d); + insertChar_InputWidget_(d, arg_Command(command_UserEvent(ev))); + contentsWereChanged_InputWidget_(d); + return iTrue; + } +#endif else if (isCommand_UserEvent(ev, "theme.changed")) { if (d->buffered) { d->inFlags |= needUpdateBuffer_InputWidgetFlag; @@ -2010,13 +2151,6 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { // } // return iFalse; // } - else if (isCommand_UserEvent(ev, "text.insert")) { - pushUndo_InputWidget_(d); - deleteMarked_InputWidget_(d); - insertChar_InputWidget_(d, arg_Command(command_UserEvent(ev))); - contentsWereChanged_InputWidget_(d); - return iTrue; - } else if (isCommand_Widget(w, ev, "input.backup")) { if (d->inFlags & needBackup_InputWidgetFlag) { saveBackup_InputWidget_(d); @@ -2027,10 +2161,7 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { updateMetrics_InputWidget_(d); // updateLinesAndResize_InputWidget_(d); } - else if (isFocused_Widget(d) && isCommand_UserEvent(ev, "copy")) { - copy_InputWidget_(d, iFalse); - return iTrue; - } +#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT if (ev->type == SDL_MOUSEWHEEL && contains_Widget(w, coord_MouseWheelEvent(&ev->wheel))) { if (numWrapLines_InputWidget_(d) <= size_Range(&d->visWrapLines)) { return ignored_EventResult; @@ -2064,6 +2195,17 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { } return false_EventResult; } + if (ev->type == SDL_TEXTINPUT && isFocused_Widget(w)) { + pushUndo_InputWidget_(d); + deleteMarked_InputWidget_(d); + insertRange_InputWidget_(d, range_CStr(ev->text.text)); + contentsWereChanged_InputWidget_(d); + return iTrue; + } + const iInt2 curMax = cursorMax_InputWidget_(d); + const iInt2 lineFirst = init_I2(0, d->cursor.y); + const iInt2 lineLast = init_I2(endX_InputWidget_(d, d->cursor.y), d->cursor.y); +#endif /* Click behavior depends on device type. */ { const int mbResult = (deviceType_App() == desktop_AppDeviceType ? processPointerEvents_InputWidget_(d, ev) @@ -2075,12 +2217,10 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { if (ev->type == SDL_KEYUP && isFocused_Widget(w)) { return iTrue; } - const iInt2 curMax = cursorMax_InputWidget_(d); - const iInt2 lineFirst = init_I2(0, d->cursor.y); - const iInt2 lineLast = init_I2(endX_InputWidget_(d, d->cursor.y), d->cursor.y); if (ev->type == SDL_KEYDOWN && isFocused_Widget(w)) { const int key = ev->key.keysym.sym; const int mods = keyMods_Sym(ev->key.keysym.mod); +#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT if (mods == KMOD_PRIMARY) { switch (key) { case 'c': @@ -2098,7 +2238,7 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { return iTrue; } } -#if defined (iPlatformApple) +# if defined (iPlatformApple) if (mods == KMOD_PRIMARY || mods == (KMOD_PRIMARY | KMOD_SHIFT)) { switch (key) { case SDLK_UP: @@ -2108,16 +2248,13 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { return iTrue; } } -#endif +# endif d->prevCursor = d->cursor; +#endif switch (key) { - case SDLK_INSERT: - if (mods == KMOD_SHIFT) { - paste_InputWidget_(d); - } - return iTrue; case SDLK_RETURN: case SDLK_KP_ENTER: +#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT if (isAllowedToInsertNewline_InputWidget_(d)) { if (checkLineBreakMods_InputWidget_(d, mods)) { pushUndo_InputWidget_(d); @@ -2127,6 +2264,7 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { return iTrue; } } +#endif if (d->inFlags & enterKeyEnabled_InputWidgetFlag && (checkAcceptMods_InputWidget_(d, mods) || (~d->inFlags & lineBreaksEnabled_InputWidgetFlag))) { @@ -2139,6 +2277,12 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { end_InputWidget(d, iTrue); setFocus_Widget(NULL); return (d->inFlags & eatEscape_InputWidgetFlag) != 0; +#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT + case SDLK_INSERT: + if (mods == KMOD_SHIFT) { + paste_InputWidget_(d); + } + return iTrue; case SDLK_BACKSPACE: if (!isEmpty_Range(&d->mark)) { pushUndo_InputWidget_(d); @@ -2238,7 +2382,7 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { refresh_Widget(w); return iTrue; } -#if defined (iPlatformApple) +# if defined (iPlatformApple) /* fall through for Emacs-style Home/End */ case SDLK_e: if (mods == KMOD_CTRL || mods == (KMOD_CTRL | KMOD_SHIFT)) { @@ -2246,7 +2390,7 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { refresh_Widget(w); return iTrue; } -#endif +# endif break; case SDLK_LEFT: case SDLK_RIGHT: { @@ -2296,22 +2440,17 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { } refresh_Widget(d); return iTrue; +#endif } if (mods & (KMOD_PRIMARY | KMOD_SECONDARY)) { return iFalse; } return iTrue; } - else if (ev->type == SDL_TEXTINPUT && isFocused_Widget(w)) { - pushUndo_InputWidget_(d); - deleteMarked_InputWidget_(d); - insertRange_InputWidget_(d, range_CStr(ev->text.text)); - contentsWereChanged_InputWidget_(d); - return iTrue; - } return processEvent_Widget(w, ev); } +#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT iDeclareType(MarkPainter) struct Impl_MarkPainter { @@ -2370,6 +2509,7 @@ static iBool draw_MarkPainter_(iWrapText *wrapText, iRangecc wrappedText, iTextA } return iTrue; } +#endif static void draw_InputWidget_(const iInputWidget *d) { const iWidget *w = constAs_Widget(d); @@ -2393,16 +2533,18 @@ static void draw_InputWidget_(const iInputWidget *d) { isFocused ? uiInputFrameFocused_ColorId : isHover ? uiInputFrameHover_ColorId : uiInputFrame_ColorId); if (d->sysCtrl) { + /* The system-provided control is drawing the text. */ drawChildren_Widget(w); return; } - setClip_Paint(&p, adjusted_Rect(bounds, init_I2(d->leftPadding, 0), - init_I2(-d->rightPadding, w->flags & extraPadding_WidgetFlag ? -gap_UI / 2 : 0))); const iRect contentBounds = contentBounds_InputWidget_(d); iInt2 drawPos = topLeft_Rect(contentBounds); const int fg = isHint ? uiAnnotation_ColorId : isFocused /*&& !isEmpty_Array(&d->lines)*/ ? uiInputTextFocused_ColorId : uiInputText_ColorId; +#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT + setClip_Paint(&p, adjusted_Rect(bounds, init_I2(d->leftPadding, 0), + init_I2(-d->rightPadding, w->flags & extraPadding_WidgetFlag ? -gap_UI / 2 : 0))); iWrapText wrapText = { .maxWidth = d->maxLen == 0 ? width_Rect(contentBounds) : unlimitedWidth_InputWidget_, .mode = (d->inFlags & isUrl_InputWidgetFlag ? anyCharacter_WrapTextMode @@ -2410,8 +2552,9 @@ static void draw_InputWidget_(const iInputWidget *d) { .overrideChar = (d->inFlags & isSensitive_InputWidgetFlag ? sensitiveChar_ : 0), }; const iRangei visLines = visibleLineRange_InputWidget_(d); - const int visLineOffsetY = visLineOffsetY_InputWidget_(d); iRect markerRects[2] = { zero_Rect(), zero_Rect() }; +#endif + const int visLineOffsetY = visLineOffsetY_InputWidget_(d); /* If buffered, just draw the buffered copy. */ if (d->buffered && !isFocused) { /* Most input widgets will use this, since only one is focused at a time. */ @@ -2439,6 +2582,7 @@ static void draw_InputWidget_(const iInputWidget *d) { drawRange_Text(d->font, drawPos, uiAnnotation_ColorId, range_String(&d->hint)); } } +#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT else { iAssert(~d->inFlags & isSensitive_InputWidgetFlag || size_Range(&visLines) == 1); drawPos.y += visLineOffsetY; @@ -2517,6 +2661,7 @@ static void draw_InputWidget_(const iInputWidget *d) { drawPin_Paint(&p, markerRects[i], i, uiTextCaution_ColorId); } } +#endif drawChildren_Widget(w); } -- cgit v1.2.3 From 7ce63a96eafd7ee94a0ff34230909b8cb3f85ca4 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 5 Dec 2021 15:03:16 +0200 Subject: Updated language strings --- res/lang/cs.bin | Bin 31100 -> 31216 bytes res/lang/de.bin | Bin 30107 -> 30223 bytes res/lang/en.bin | Bin 26204 -> 26320 bytes res/lang/eo.bin | Bin 25158 -> 25274 bytes res/lang/es.bin | Bin 29931 -> 30047 bytes res/lang/es_MX.bin | Bin 27263 -> 27379 bytes res/lang/fi.bin | Bin 29764 -> 29880 bytes res/lang/fr.bin | Bin 30911 -> 31027 bytes res/lang/gl.bin | Bin 29116 -> 29232 bytes res/lang/hu.bin | Bin 30936 -> 31052 bytes res/lang/ia.bin | Bin 28263 -> 28379 bytes res/lang/ie.bin | Bin 28851 -> 28967 bytes res/lang/isv.bin | Bin 24924 -> 25040 bytes res/lang/pl.bin | Bin 29539 -> 29655 bytes res/lang/ru.bin | Bin 44299 -> 44415 bytes res/lang/sk.bin | Bin 25260 -> 25376 bytes res/lang/sr.bin | Bin 43725 -> 43841 bytes res/lang/tok.bin | Bin 26973 -> 27089 bytes res/lang/tr.bin | Bin 29157 -> 29273 bytes res/lang/uk.bin | Bin 43644 -> 43760 bytes res/lang/zh_Hans.bin | Bin 25158 -> 25274 bytes res/lang/zh_Hant.bin | Bin 25356 -> 25472 bytes 22 files changed, 0 insertions(+), 0 deletions(-) diff --git a/res/lang/cs.bin b/res/lang/cs.bin index a8981bd4..aacd5072 100644 Binary files a/res/lang/cs.bin and b/res/lang/cs.bin differ diff --git a/res/lang/de.bin b/res/lang/de.bin index 6d17ff3b..83039237 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 4749f358..03f69772 100644 Binary files a/res/lang/en.bin and b/res/lang/en.bin differ diff --git a/res/lang/eo.bin b/res/lang/eo.bin index b3e06797..638e3b7e 100644 Binary files a/res/lang/eo.bin and b/res/lang/eo.bin differ diff --git a/res/lang/es.bin b/res/lang/es.bin index 2265ddf1..11cdd187 100644 Binary files a/res/lang/es.bin and b/res/lang/es.bin differ diff --git a/res/lang/es_MX.bin b/res/lang/es_MX.bin index 810bc543..4725bc56 100644 Binary files a/res/lang/es_MX.bin and b/res/lang/es_MX.bin differ diff --git a/res/lang/fi.bin b/res/lang/fi.bin index ccc61bd4..254ae4e9 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 355073bd..d26e99db 100644 Binary files a/res/lang/fr.bin and b/res/lang/fr.bin differ diff --git a/res/lang/gl.bin b/res/lang/gl.bin index 30c5fb5f..f67655a4 100644 Binary files a/res/lang/gl.bin and b/res/lang/gl.bin differ diff --git a/res/lang/hu.bin b/res/lang/hu.bin index 90f32f47..6420c3c0 100644 Binary files a/res/lang/hu.bin and b/res/lang/hu.bin differ diff --git a/res/lang/ia.bin b/res/lang/ia.bin index a9672e07..0c55d980 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 058ab7ff..d95a1723 100644 Binary files a/res/lang/ie.bin and b/res/lang/ie.bin differ diff --git a/res/lang/isv.bin b/res/lang/isv.bin index ccf1f206..c61d9465 100644 Binary files a/res/lang/isv.bin and b/res/lang/isv.bin differ diff --git a/res/lang/pl.bin b/res/lang/pl.bin index d49b1fdd..370bbca2 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 cb2baebc..c04db5fb 100644 Binary files a/res/lang/ru.bin and b/res/lang/ru.bin differ diff --git a/res/lang/sk.bin b/res/lang/sk.bin index 50ee11ce..36a1f1d1 100644 Binary files a/res/lang/sk.bin and b/res/lang/sk.bin differ diff --git a/res/lang/sr.bin b/res/lang/sr.bin index d27c1c48..9d8c40de 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 89e9526c..c2432e42 100644 Binary files a/res/lang/tok.bin and b/res/lang/tok.bin differ diff --git a/res/lang/tr.bin b/res/lang/tr.bin index ef7f8f61..80497417 100644 Binary files a/res/lang/tr.bin and b/res/lang/tr.bin differ diff --git a/res/lang/uk.bin b/res/lang/uk.bin index cff8fb7d..e77375da 100644 Binary files a/res/lang/uk.bin and b/res/lang/uk.bin differ diff --git a/res/lang/zh_Hans.bin b/res/lang/zh_Hans.bin index e8dafce4..9c3a92b0 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 898bbddc..c5d2d7fa 100644 Binary files a/res/lang/zh_Hant.bin and b/res/lang/zh_Hant.bin differ -- cgit v1.2.3 From 14e4bd38636f7dccd8a22d868cf82134592a98db Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 5 Dec 2021 18:31:52 +0200 Subject: Mobile: Fixed issues with overflow-scrolling --- src/app.c | 12 +++++++++++- src/ui/root.c | 2 +- src/ui/root.h | 1 + src/ui/widget.c | 5 ++++- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/app.c b/src/app.c index 52072d39..5a76ba45 100644 --- a/src/app.c +++ b/src/app.c @@ -1484,7 +1484,9 @@ static void runTickers_App_(iApp *d) { } iForIndices(i, d->window->base.roots) { iRoot *root = d->window->base.roots[i]; - notifyVisualOffsetChange_Root(root); + if (root) { + notifyVisualOffsetChange_Root(root); + } } } @@ -1558,6 +1560,14 @@ void refresh_App(void) { iConstForEach(PtrArray, j, &windows) { iWindow *win = j.ptr; setCurrent_Window(win); + iForIndices(i, win->roots) { + iRoot *root = win->roots[i]; + if (root && root->didOverflowScroll) { + /* Some widgets may need a just-in-time visual update. */ + notifyVisualOffsetChange_Root(root); + root->didOverflowScroll = iFalse; + } + } switch (win->type) { case main_WindowType: // iTime draw; diff --git a/src/ui/root.c b/src/ui/root.c index e2706898..28aa4b92 100644 --- a/src/ui/root.c +++ b/src/ui/root.c @@ -661,7 +661,7 @@ void updateToolbarColors_Root(iRoot *d) { } void notifyVisualOffsetChange_Root(iRoot *d) { - if (d && d->didAnimateVisualOffsets) { + if (d && (d->didAnimateVisualOffsets || d->didOverflowScroll)) { iNotifyAudience(d, visualOffsetsChanged, RootVisualOffsetsChanged); } } diff --git a/src/ui/root.h b/src/ui/root.h index 2419b599..2f0d72c9 100644 --- a/src/ui/root.h +++ b/src/ui/root.h @@ -19,6 +19,7 @@ struct Impl_Root { iBool pendingArrange; int loadAnimTimer; iBool didAnimateVisualOffsets; + iBool didOverflowScroll; iAudience *visualOffsetsChanged; /* called after running tickers */ iColor tmPalette[tmMax_ColorId]; /* theme-specific palette */ }; diff --git a/src/ui/widget.c b/src/ui/widget.c index 225c0480..bb3a39f1 100644 --- a/src/ui/widget.c +++ b/src/ui/widget.c @@ -1203,6 +1203,9 @@ iBool scrollOverflow_Widget(iWidget *d, int delta) { bounds.pos.y = iMin(bounds.pos.y, validPosRange.end); } // printf("range: %d ... %d\n", range.start, range.end); + if (delta) { + d->root->didOverflowScroll = iTrue; /* ensure that widgets update if needed */ + } } else { bounds.pos.y = iClamp(bounds.pos.y, validPosRange.start, validPosRange.end); @@ -1871,7 +1874,7 @@ iAny *findParentClass_Widget(const iWidget *d, const iAnyClass *class) { } iAny *findOverflowScrollable_Widget(iWidget *d) { - const iRect rootRect = rect_Root(d->root); + const iRect rootRect = visibleRect_Root(d->root); for (iWidget *w = d; w; w = parent_Widget(w)) { if (flags_Widget(w) & overflowScrollable_WidgetFlag) { const iRect bounds = boundsWithoutVisualOffset_Widget(w); -- cgit v1.2.3 From 8a8040bffff335395ce37d7fe2c9801db05b4b0a Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 5 Dec 2021 19:02:27 +0200 Subject: Mobile: Scrolling to keep InputWidget visible --- src/ui/inputwidget.c | 55 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index 65710373..3572d043 100644 --- a/src/ui/inputwidget.c +++ b/src/ui/inputwidget.c @@ -241,6 +241,7 @@ struct Impl_InputWidget { int backupTimer; iString oldText; /* for restoring if edits cancelled */ int lastUpdateWidth; + uint32_t lastOverflowScrollTime; /* scrolling to show focused widget */ iSystemTextInput *sysCtrl; #if LAGRANGE_USE_SYSTEM_TEXT_INPUT iString text; @@ -2065,6 +2066,30 @@ static void clampWheelAccum_InputWidget_(iInputWidget *d, int wheel) { } #endif +static void overflowScrollToKeepVisible_InputWidget_(iAny *widget) { + iInputWidget *d = widget; + iWidget *w = as_Widget(d); + if (!isFocused_Widget(w) || isAffectedByVisualOffset_Widget(w)) { + return; + } + iRect rect = boundsWithoutVisualOffset_Widget(w); + iRect visible = visibleRect_Root(w->root); + const uint32_t nowTime = SDL_GetTicks(); + const double elapsed = (nowTime - d->lastOverflowScrollTime) / 1000.0; + int dist = bottom_Rect(rect) + gap_UI - bottom_Rect(visible); + const int step = iRound(10 * dist * elapsed); + if (step > 0) { + iWidget *scrollable = findOverflowScrollable_Widget(w); + if (scrollable) { + scrollOverflow_Widget(scrollable, -iClamp(step, 1, dist)); + d->lastOverflowScrollTime = nowTime; + } + } + if (dist > 0) { + addTicker_App(overflowScrollToKeepVisible_InputWidget_, widget); + } +} + static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { iWidget *w = as_Widget(d); /* Resize according to width immediately. */ @@ -2077,6 +2102,13 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { updateAllLinesAndResizeHeight_InputWidget_(d); d->lastUpdateWidth = w->rect.size.x; } +#if LAGRANGE_USE_SYSTEM_TEXT_INPUT + if (isResize_UserEvent(ev)) { + if (d->sysCtrl) { + updateAfterVisualOffsetChange_InputWidget_(d, w->root); + } + } +#endif if (isCommand_Widget(w, ev, "focus.gained")) { begin_InputWidget(d); return iFalse; @@ -2139,18 +2171,19 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { } return iFalse; } - /* TODO: Scroll to keep widget visible when keyboard appears. */ -// else if (isCommand_UserEvent(ev, "keyboard.changed")) { -// if (isFocused_Widget(d) && arg_Command(command_UserEvent(ev))) { -// iRect rect = bounds_Widget(w); + else if (isCommand_UserEvent(ev, "keyboard.changed")) { + /* Scroll to keep widget visible when keyboard appears. */ + if (isFocused_Widget(d) && arg_Command(command_UserEvent(ev))) { + d->lastOverflowScrollTime = SDL_GetTicks(); + overflowScrollToKeepVisible_InputWidget_(d); // rect.pos.y -= value_Anim(&get_Window()->rootOffset); -// const iInt2 visRoot = visibleSize_Root(w->root); -// if (bottom_Rect(rect) > visRoot.y) { -// setValue_Anim(&get_Window()->rootOffset, -(bottom_Rect(rect) - visRoot.y), 250); -// } -// } -// return iFalse; -// } + //const iInt2 visRoot = visibleSize_Root(w->root); + //if (bottom_Rect(rect) > visRoot.y) { + //setValue_Anim(&get_Window()->rootOffset, -(bottom_Rect(rect) - visRoot.y), 250); + //} + } + return iFalse; + } else if (isCommand_Widget(w, ev, "input.backup")) { if (d->inFlags & needBackup_InputWidgetFlag) { saveBackup_InputWidget_(d); -- cgit v1.2.3 From e5305a5e45a0f8a4e7ea2a670b4afd2c34f7840c Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 5 Dec 2021 22:02:36 +0200 Subject: Mobile: Pull to refresh --- src/ui/documentwidget.c | 48 +++++++++++++++++++---- src/ui/util.c | 101 +++++++++++++++++++++++++++++++++++++++--------- src/ui/util.h | 2 + 3 files changed, 125 insertions(+), 26 deletions(-) diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 0ef80690..823c37ac 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -649,7 +649,8 @@ static const iGmRun *lastVisibleLink_DocumentWidget_(const iDocumentWidget *d) { static float normScrollPos_DocumentWidget_(const iDocumentWidget *d) { const int docSize = pageHeight_DocumentWidget_(d); // size_GmDocument(d->doc).y; if (docSize) { - return pos_SmoothScroll(&d->scrollY) / (float) docSize; + float pos = pos_SmoothScroll(&d->scrollY) / (float) docSize; + return iMax(pos, 0.0f); } return 0; } @@ -2179,15 +2180,24 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) { insertChildAfter_Widget(buttons, iClob(lineBreak), 0); } else { +#if !defined (iPlatformAppleMobile) lineBreak = new_LabelWidget("${dlg.input.linebreak}", "text.insert arg:10"); +#endif + } + if (lineBreak) { + setFlags_Widget(as_Widget(lineBreak), frameless_WidgetFlag, iTrue); + setTextColor_LabelWidget(lineBreak, uiTextDim_ColorId); } - setFlags_Widget(as_Widget(lineBreak), frameless_WidgetFlag, iTrue); - setTextColor_LabelWidget(lineBreak, uiTextDim_ColorId); } - setId_Widget(addChildPosFlags_Widget(buttons, - iClob(new_LabelWidget("", NULL)), - front_WidgetAddPos, frameless_WidgetFlag), - "valueinput.counter"); + iWidget *counter = (iWidget *) new_LabelWidget("", NULL); + setId_Widget(counter, "valueinput.counter"); + setFlags_Widget(counter, frameless_WidgetFlag, iTrue); + if (deviceType_App() == desktop_AppDeviceType) { + addChildPos_Widget(buttons, iClob(counter), front_WidgetAddPos); + } + else { + insertChildAfter_Widget(buttons, iClob(counter), 0); + } if (lineBreak && deviceType_App() != desktop_AppDeviceType) { addChildPos_Widget(buttons, iClob(lineBreak), front_WidgetAddPos); } @@ -2207,6 +2217,7 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) { /* Keep scroll position when reloading the same page. */ reset_SmoothScroll(&d->scrollY); } + d->scrollY.pullActionTriggered = 0; pauseAllPlayers_Media(media_GmDocument(d->doc), iTrue); iRelease(d->doc); /* new content incoming */ d->doc = new_GmDocument(); @@ -3698,6 +3709,10 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e return iTrue; } else if (ev->type == SDL_USEREVENT && ev->user.code == command_UserEventCode) { + if (isCommand_Widget(w, ev, "pullaction")) { + postCommand_Widget(w, "navigate.reload"); + return iTrue; + } if (!handleCommand_DocumentWidget_(d, command_UserEvent(ev))) { /* Base class commands. */ return processEvent_Widget(w, ev); @@ -5132,6 +5147,25 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) { if (colorTheme_App() == pureWhite_ColorTheme) { drawHLine_Paint(&ctx.paint, topLeft_Rect(bounds), width_Rect(bounds), uiSeparator_ColorId); } + /* Pull action indicator. */ + if (deviceType_App() != desktop_AppDeviceType) { + float pullPos = pullActionPos_SmoothScroll(&d->scrollY); + /* Account for the part where the indicator isn't yet visible. */ + pullPos = (pullPos - 0.2f) / 0.8f; + iRect indRect = initCentered_Rect(init_I2(mid_Rect(bounds).x, + top_Rect(bounds) - 5 * gap_UI - + pos_SmoothScroll(&d->scrollY)), + init_I2(25 * gap_UI, 2 * gap_UI)); + setClip_Paint(&ctx.paint, clipBounds); + int color = pullPos < 1.0f ? tmBannerItemFrame_ColorId : tmBannerItemText_ColorId; + drawRect_Paint(&ctx.paint, indRect, color); + if (pullPos > 0) { + shrink_Rect(&indRect, divi_I2(gap2_UI, 2)); + indRect.size.x *= pullPos; + fillRect_Paint(&ctx.paint, indRect, color); + } + unsetClip_Paint(&ctx.paint); + } drawChildren_Widget(w); if (d->flags & drawDownloadCounter_DocumentWidgetFlag && isRequestOngoing_DocumentWidget(d)) { const int font = uiLabelLarge_FontId; diff --git a/src/ui/util.c b/src/ui/util.c index de838769..8aedef99 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -479,12 +479,14 @@ void init_SmoothScroll(iSmoothScroll *d, iWidget *owner, iSmoothScrollNotifyFunc reset_SmoothScroll(d); d->widget = owner; d->notify = notify; + d->pullActionTriggered = 0; } void reset_SmoothScroll(iSmoothScroll *d) { init_Anim(&d->pos, 0); d->max = 0; d->overscroll = (deviceType_App() != desktop_AppDeviceType ? 100 * gap_UI : 0); + d->pullActionTriggered = 0; } void setMax_SmoothScroll(iSmoothScroll *d, int max) { @@ -518,6 +520,29 @@ iBool isFinished_SmoothScroll(const iSmoothScroll *d) { return isFinished_Anim(&d->pos); } +iLocalDef int pullActionThreshold_SmoothScroll_(const iSmoothScroll *d) { + return d->overscroll * 6 / 10; +} + +float pullActionPos_SmoothScroll(const iSmoothScroll *d) { + if (d->pullActionTriggered >= 1) { + return 1.0f; + } + float pos = overscroll_SmoothScroll_(d); + if (pos >= 0.0f) { + return 0.0f; + } + pos = -pos / (float) pullActionThreshold_SmoothScroll_(d); + return iMin(pos, 1.0f); +} + +static void checkPullAction_SmoothScroll_(iSmoothScroll *d) { + if (d->pullActionTriggered == 1 && d->widget) { + postCommand_Widget(d->widget, "pullaction"); + d->pullActionTriggered = 2; /* pending handling */ + } +} + void moveSpan_SmoothScroll(iSmoothScroll *d, int offset, uint32_t span) { #if !defined (iPlatformMobile) if (!prefs_App()->smoothScrolling) { @@ -525,6 +550,14 @@ void moveSpan_SmoothScroll(iSmoothScroll *d, int offset, uint32_t span) { } #endif int destY = targetValue_Anim(&d->pos) + offset; + if (destY < -pullActionThreshold_SmoothScroll_(d)) { + if (d->pullActionTriggered == 0) { + d->pullActionTriggered = iTrue; +#if defined (iPlatformAppleMobile) + playHapticEffect_iOS(tap_HapticEffect); +#endif + } + } if (destY < -d->overscroll) { destY = -d->overscroll; } @@ -552,6 +585,7 @@ void moveSpan_SmoothScroll(iSmoothScroll *d, int offset, uint32_t span) { // printf("remaining: %f dur: %d\n", remaining, duration); d->pos.bounce = (osDelta < 0 ? -1 : 1) * iMini(5 * d->overscroll, remaining * remaining * 0.00005f); + checkPullAction_SmoothScroll_(d); } } if (d->notify) { @@ -570,6 +604,7 @@ iBool processEvent_SmoothScroll(iSmoothScroll *d, const SDL_Event *ev) { moveSpan_SmoothScroll(d, -osDelta, 100 * sqrt(iAbs(osDelta) / gap_UI)); d->pos.flags = easeOut_AnimFlag | muchSofter_AnimFlag; } + checkPullAction_SmoothScroll_(d); return iTrue; } return iFalse; @@ -1622,6 +1657,11 @@ static void updateValueInputWidth_(iWidget *dlg) { } /* Adjust the maximum number of visible lines. */ int footer = 6 * gap_UI + get_MainWindow()->keyboardHeight; +#if defined (iPlatformAppleMobile) + if (deviceType_App() == phone_AppDeviceType) { + footer -= 12 * gap_UI; /* A little suspect, this... Check the math? */ + } +#endif iWidget *buttons = findChild_Widget(dlg, "dialogbuttons"); if (buttons) { footer += height_Widget(buttons); @@ -1695,8 +1735,8 @@ iWidget *makeDialogButtons_Widget(const iMenuItem *actions, size_t numActions) { } int fonts[2] = { uiLabel_FontId, uiLabelBold_FontId }; if (deviceType_App() == phone_AppDeviceType) { - fonts[0] = uiLabelMedium_FontId; - fonts[1] = uiLabelMediumBold_FontId; + fonts[0] = uiLabelBig_FontId; + fonts[1] = uiLabelBigBold_FontId; } for (size_t i = 0; i < numActions; i++) { const char *label = actions[i].label; @@ -1738,6 +1778,10 @@ iWidget *makeDialogButtons_Widget(const iMenuItem *actions, size_t numActions) { setId_Widget(as_Widget(button), "default"); } setFlags_Widget(as_Widget(button), alignLeft_WidgetFlag | drawKey_WidgetFlag, isDefault); + if (deviceType_App() != desktop_AppDeviceType) { + setFlags_Widget(as_Widget(button), frameless_WidgetFlag | noBackground_WidgetFlag, iTrue); + setTextColor_LabelWidget(button, uiTextAction_ColorId); + } setFont_LabelWidget(button, isDefault ? fonts[1] : fonts[0]); } return div; @@ -1753,9 +1797,12 @@ iWidget *makeValueInput_Widget(iWidget *parent, const iString *initialValue, con if (parent) { addChild_Widget(parent, iClob(dlg)); } - setId_Widget( - addChildFlags_Widget(dlg, iClob(new_LabelWidget(title, NULL)), frameless_WidgetFlag), - "valueinput.title"); + if (deviceType_App() != phone_AppDeviceType) { + setId_Widget( + addChildFlags_Widget(dlg, iClob(new_LabelWidget(title, NULL)), + frameless_WidgetFlag), + "valueinput.title"); + } iLabelWidget *promptLabel; setId_Widget(addChildFlags_Widget( dlg, iClob(promptLabel = new_LabelWidget(prompt, NULL)), frameless_WidgetFlag @@ -1775,15 +1822,25 @@ iWidget *makeValueInput_Widget(iWidget *parent, const iString *initialValue, con } setId_Widget(as_Widget(input), "input"); addChild_Widget(dlg, iClob(makePadding_Widget(gap_UI))); - addChild_Widget(dlg, - iClob(makeDialogButtons_Widget( - (iMenuItem[]){ { "${cancel}", SDLK_ESCAPE, 0, "valueinput.cancel" }, - { acceptLabel, - SDLK_RETURN, - acceptKeyMod_ReturnKeyBehavior(prefs_App()->returnKey), - "valueinput.accept" } }, - 2))); -// finalizeSheet_Mobile(dlg); + /* On mobile, the actions are laid out a bit differently: buttons on top, on opposite edges. */ + iArray actions; + init_Array(&actions, sizeof(iMenuItem)); + pushBack_Array(&actions, &(iMenuItem){ "${cancel}", SDLK_ESCAPE, 0, "valueinput.cancel" }); + if (deviceType_App() != desktop_AppDeviceType) { + pushBack_Array(&actions, &(iMenuItem){ "---" }); + } + pushBack_Array(&actions, &(iMenuItem){ + acceptLabel, + SDLK_RETURN, + acceptKeyMod_ReturnKeyBehavior(prefs_App()->returnKey), + "valueinput.accept" + }); + addChildPos_Widget(dlg, + iClob(makeDialogButtons_Widget(constData_Array(&actions), + size_Array(&actions))), + deviceType_App() != desktop_AppDeviceType ? + front_WidgetAddPos : back_WidgetAddPos); + deinit_Array(&actions); arrange_Widget(dlg); if (parent) { setFocus_Widget(as_Widget(input)); @@ -2396,7 +2453,7 @@ iWidget *makePreferences_Widget(void) { }; const iMenuItem colorPanelItems[] = { { "title id:heading.prefs.colors" }, - { "heading id:heading.prefs.uitheme" }, + //{ "heading id:heading.prefs.uitheme" }, { "toggle id:prefs.ostheme" }, { "radio id:prefs.theme", 0, 0, (const void *) themeItems }, { "radio id:prefs.accent", 0, 0, (const void *) accentItems }, @@ -2414,11 +2471,16 @@ iWidget *makePreferences_Widget(void) { { "dropdown id:prefs.font.body", 0, 0, (const void *) constData_Array(makeFontItems_("body")) }, { "dropdown id:prefs.font.mono", 0, 0, (const void *) constData_Array(makeFontItems_("mono")) }, { "buttons id:prefs.mono", 0, 0, (const void *) monoFontItems }, - { "dropdown id:prefs.font.monodoc", 0, 0, (const void *) constData_Array(makeFontItems_("monodoc")) }, { "padding" }, - { "toggle id:prefs.font.smooth" }, - { "padding" }, - { "dropdown id:prefs.font.ui", 0, 0, (const void *) constData_Array(makeFontItems_("ui")) }, + { "dropdown id:prefs.font.monodoc", 0, 0, (const void *) constData_Array(makeFontItems_("monodoc")) }, +// { "padding" }, + { "heading id:prefs.gemtext.ansi" }, + { "toggle id:prefs.gemtext.ansi.fg" }, + { "toggle id:prefs.gemtext.ansi.bg" }, + { "toggle id:prefs.gemtext.ansi.fontstyle" }, +// { "toggle id:prefs.font.smooth" }, +// { "padding" }, +// { "dropdown id:prefs.font.ui", 0, 0, (const void *) constData_Array(makeFontItems_("ui")) }, { "padding" }, { "button text:" fontpack_Icon " " uiTextAction_ColorEscape "${menu.fonts}", 0, 0, "!open url:about:fonts" }, { NULL } @@ -3048,6 +3110,7 @@ iWidget *makeFeedSettings_Widget(uint32_t bookmarkId) { { format_CStr("title id:feedcfg.heading text:%s", headingText) }, { "input id:feedcfg.title text:${dlg.feed.title}" }, { "radio id:dlg.feed.entrytype", 0, 0, (const void *) typeItems }, + { "padding" }, { "toggle id:feedcfg.ignoreweb text:${dlg.feed.ignoreweb}" }, { NULL } }, actions, iElemCount(actions)); diff --git a/src/ui/util.h b/src/ui/util.h index 7ee94f1d..023fa7ae 100644 --- a/src/ui/util.h +++ b/src/ui/util.h @@ -184,6 +184,7 @@ struct Impl_SmoothScroll { int max; int overscroll; iWidget *widget; + int pullActionTriggered; iSmoothScrollNotifyFunc notify; }; @@ -197,6 +198,7 @@ iBool processEvent_SmoothScroll (iSmoothScroll *, const SDL_Event *ev); float pos_SmoothScroll (const iSmoothScroll *); iBool isFinished_SmoothScroll (const iSmoothScroll *); +float pullActionPos_SmoothScroll (const iSmoothScroll *); /* 0...1 */ /*-----------------------------------------------------------------------------------------------*/ -- cgit v1.2.3 From 4f39ded4a343aabd25067ee5f8f1ff061af71eb0 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 5 Dec 2021 22:04:16 +0200 Subject: Mobile: Fixed layout issue during animation Update sliding sidebar height with the correct values for the frame. --- src/app.c | 20 ++++++-------------- src/ui/sidebarwidget.c | 43 +++++++++++++++++++++++++------------------ src/ui/window.c | 8 ++++++++ 3 files changed, 39 insertions(+), 32 deletions(-) diff --git a/src/app.c b/src/app.c index 5a76ba45..160d76f6 100644 --- a/src/app.c +++ b/src/app.c @@ -1482,12 +1482,12 @@ static void runTickers_App_(iApp *d) { if (isEmpty_SortedArray(&d->tickers)) { d->lastTickerTime = 0; } - iForIndices(i, d->window->base.roots) { - iRoot *root = d->window->base.roots[i]; - if (root) { - notifyVisualOffsetChange_Root(root); - } - } +// iForIndices(i, d->window->base.roots) { +// iRoot *root = d->window->base.roots[i]; +// if (root) { +// notifyVisualOffsetChange_Root(root); +// } +// } } static int resizeWatcher_(void *user, SDL_Event *event) { @@ -1560,14 +1560,6 @@ void refresh_App(void) { iConstForEach(PtrArray, j, &windows) { iWindow *win = j.ptr; setCurrent_Window(win); - iForIndices(i, win->roots) { - iRoot *root = win->roots[i]; - if (root && root->didOverflowScroll) { - /* Some widgets may need a just-in-time visual update. */ - notifyVisualOffsetChange_Root(root); - root->didOverflowScroll = iFalse; - } - } switch (win->type) { case main_WindowType: // iTime draw; diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index d91c85af..d4b1ee20 100644 --- a/src/ui/sidebarwidget.c +++ b/src/ui/sidebarwidget.c @@ -220,6 +220,7 @@ static iLabelWidget *addActionButton_SidebarWidget_(iSidebarWidget *d, const cha if (deviceType_App() != desktop_AppDeviceType) { setFlags_Widget(as_Widget(btn), frameless_WidgetFlag, iTrue); setBackgroundColor_Widget(as_Widget(btn), uiBackground_ColorId); + setTextColor_LabelWidget(btn, uiTextAction_ColorId); } return btn; } @@ -741,6 +742,20 @@ static void updateMetrics_SidebarWidget_(iSidebarWidget *d) { updateItemHeight_SidebarWidget_(d); } +static void updateSlidingSheetHeight_SidebarWidget_(iSidebarWidget *sidebar, iRoot *root) { + if (!isPortraitPhone_App() || !isVisible_Widget(sidebar)) return; + iWidget *d = as_Widget(sidebar); + const int oldSize = d->rect.size.y; + const int newSize = bottom_Rect(safeRect_Root(d->root)) - top_Rect(bounds_Widget(d)); + if (oldSize != newSize) { + d->rect.size.y = newSize; + arrange_Widget(d); + } +// printf("[%p] %u: %d animating %d\n", d, window_Widget(d)->frameTime, +// (flags_Widget(d) & visualOffset_WidgetFlag) != 0, +// newSize); +} + void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) { iWidget *w = as_Widget(d); init_Widget(w); @@ -800,6 +815,7 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) { setId_Widget(as_Widget(closeButton), "sidebar.close"); setFont_LabelWidget(sheetTitle, uiLabelBig_FontId); setFont_LabelWidget(closeButton, uiLabelBigBold_FontId); + iConnect(Root, get_Root(), visualOffsetsChanged, d, updateSlidingSheetHeight_SidebarWidget_); } iWidget *buttons = new_Widget(); setId_Widget(buttons, "buttons"); @@ -1073,22 +1089,6 @@ iBool handleBookmarkEditorCommands_SidebarWidget_(iWidget *editor, const char *c return iFalse; } -static void animateSlidingSheetHeight_SidebarWidget_(iAny *sidebar) { - iWidget *d = sidebar; - const int oldSize = d->rect.size.y; - const int newSize = bottom_Rect(safeRect_Root(d->root)) - top_Rect(bounds_Widget(d)); - if (oldSize != newSize) { - d->rect.size.y = newSize; - arrange_Widget(d); - } -// printf("[%p] %u: %d animating %d\n", d, window_Widget(d)->frameTime, -// (flags_Widget(sidebar) & visualOffset_WidgetFlag) != 0, -// newSize); - if (!isFinished_Anim(&d->visualOffset)) { - addTicker_App(animateSlidingSheetHeight_SidebarWidget_, sidebar); - } -} - enum iSlidingSheetPos { top_SlidingSheetPos, middle_SlidingSheetPos, @@ -1116,7 +1116,7 @@ static void setSlidingSheetPos_SidebarWidget_(iSidebarWidget *d, enum iSlidingSh setVisualOffset_Widget(w, 0, 200, easeOut_AnimFlag | softer_AnimFlag); setScrollMode_ListWidget(d->list, disabledAtTopBothDirections_ScrollMode); } - animateSlidingSheetHeight_SidebarWidget_(d); +// animateSlidingSheetHeight_SidebarWidget_(d); } static iBool handleSidebarCommand_SidebarWidget_(iSidebarWidget *d, const char *cmd) { @@ -1193,7 +1193,7 @@ static iBool handleSidebarCommand_SidebarWidget_(iSidebarWidget *d, const char * w->rect.pos.y = height_Rect(safeRect_Root(w->root)) - d->midHeight; setVisualOffset_Widget(w, bottom_Rect(rect_Root(w->root)) - w->rect.pos.y, 0, 0); setVisualOffset_Widget(w, 0, 300, animFlags); - animateSlidingSheetHeight_SidebarWidget_(d); + //animateSlidingSheetHeight_SidebarWidget_(d); setScrollMode_ListWidget(d->list, disabledAtTopBothDirections_ScrollMode); } else { @@ -1719,6 +1719,13 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) } /* Update context menu items. */ if (d->menu && ev->type == SDL_MOUSEBUTTONDOWN) { + if (isSlidingSheet_SidebarWidget_(d) && + ev->button.button == SDL_BUTTON_LEFT && + isVisible_Widget(d) && + !contains_Widget(w, init_I2(ev->button.x, ev->button.y))) { + setSlidingSheetPos_SidebarWidget_(d, bottom_SlidingSheetPos); + return iTrue; + } if (ev->button.button == SDL_BUTTON_RIGHT) { d->contextItem = NULL; if (!isVisible_Widget(d->menu)) { diff --git a/src/ui/window.c b/src/ui/window.c index 0bbe588c..76dd1105 100644 --- a/src/ui/window.c +++ b/src/ui/window.c @@ -1260,6 +1260,14 @@ void draw_MainWindow(iMainWindow *d) { } /* Draw widgets. */ w->frameTime = SDL_GetTicks(); + iForIndices(i, d->base.roots) { + iRoot *root = d->base.roots[i]; + if (root) { + /* Some widgets may need a just-in-time visual update. */ + notifyVisualOffsetChange_Root(root); + root->didOverflowScroll = iFalse; + } + } if (isExposed_Window(w)) { w->isInvalidated = iFalse; extern int drawCount_; -- cgit v1.2.3 From e5464259ffe9a6834ee1f4913965089112e9eecb Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 5 Dec 2021 22:04:44 +0200 Subject: Widget: Added flag to always fade the background --- src/ui/root.c | 4 ++++ src/ui/widget.c | 3 ++- src/ui/widget.h | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/ui/root.c b/src/ui/root.c index 28aa4b92..18a71200 100644 --- a/src/ui/root.c +++ b/src/ui/root.c @@ -490,6 +490,7 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) { /* Place the sidebar next to or under doctabs depending on orientation. */ iSidebarWidget *sidebar = findChild_Widget(root, "sidebar"); removeChild_Widget(parent_Widget(sidebar), sidebar); + iChangeFlags(as_Widget(sidebar)->flags2, fadeBackground_WidgetFlag2, isPortrait_App()); if (isLandscape_App()) { setVisualOffset_Widget(as_Widget(sidebar), 0, 0, 0); addChildPos_Widget(findChild_Widget(root, "tabs.content"), iClob(sidebar), front_WidgetAddPos); @@ -829,6 +830,8 @@ static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) { } else if (equal_Command(cmd, "navbar.clear")) { iInputWidget *url = findChild_Widget(navBar, "url"); + setText_InputWidget(url, collectNew_String()); +#if 0 selectAll_InputWidget(url); /* Emulate a Backspace keypress. */ class_InputWidget(url)->processEvent( @@ -837,6 +840,7 @@ static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) { .timestamp = SDL_GetTicks(), .state = SDL_PRESSED, .keysym = { .sym = SDLK_BACKSPACE } }); +#endif return iTrue; } else if (equal_Command(cmd, "navbar.cancel")) { diff --git a/src/ui/widget.c b/src/ui/widget.c index bb3a39f1..9f1c8640 100644 --- a/src/ui/widget.c +++ b/src/ui/widget.c @@ -1393,7 +1393,8 @@ void drawLayerEffects_Widget(const iWidget *d) { shadowBorder = iFalse; } } - const iBool isFaded = fadeBackground && ~d->flags & noFadeBackground_WidgetFlag; + const iBool isFaded = (fadeBackground && ~d->flags & noFadeBackground_WidgetFlag) || + (d->flags2 & fadeBackground_WidgetFlag2); if (shadowBorder && ~d->flags & noShadowBorder_WidgetFlag) { iPaint p; init_Paint(&p); diff --git a/src/ui/widget.h b/src/ui/widget.h index 35be1bcb..9c4c44fb 100644 --- a/src/ui/widget.h +++ b/src/ui/widget.h @@ -125,6 +125,7 @@ enum iWidgetFlag { enum iWidgetFlag2 { slidingSheetDraggable_WidgetFlag2 = iBit(1), + fadeBackground_WidgetFlag2 = iBit(2), }; enum iWidgetAddPos { -- cgit v1.2.3 From 18e92db64278c0db70de8e21fd9db65b783d5975 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 5 Dec 2021 22:05:09 +0200 Subject: iOS: Register Iosevka for the monospace input widget --- CMakeLists.txt | 5 ++++- res/iOSBundleInfo.plist.in | 1 + src/ios.m | 12 ++++++++---- src/ui/inputwidget.c | 3 ++- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2d11e43e..71940aa4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -105,7 +105,10 @@ set (EMB_BIN ${CMAKE_CURRENT_BINARY_DIR}/resources.lgr) make_resources (${EMB_BIN} ${RESOURCES}) set_source_files_properties (${EMB_BIN} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) if (IOS) - set (EMB_FONTS res/fonts/SourceSans3-Regular.ttf) + set (EMB_FONTS + res/fonts/SourceSans3-Regular.ttf + res/fonts/IosevkaTerm-Extended.ttf + ) set_source_files_properties (${EMB_FONTS} PROPERTIES MACOSX_PACKAGE_LOCATION Resources ) diff --git a/res/iOSBundleInfo.plist.in b/res/iOSBundleInfo.plist.in index 5a02ed68..c0c1a288 100644 --- a/res/iOSBundleInfo.plist.in +++ b/res/iOSBundleInfo.plist.in @@ -51,6 +51,7 @@ LaunchScreen UIAppFonts + IosevkaTerm-Extended.ttf SourceSans3-Regular.ttf UIBackgroundModes diff --git a/src/ios.m b/src/ios.m index 47303b7f..559316a2 100644 --- a/src/ios.m +++ b/src/ios.m @@ -811,14 +811,18 @@ int preferredHeight_SystemTextInput(const iSystemTextInput *d) { void setFont_SystemTextInput(iSystemTextInput *d, int fontId) { float height = lineHeight_Text(fontId) / get_Window()->pixelRatio; UIFont *font; + // for (NSString *name in [UIFont familyNames]) { + // printf("family: %s\n", [name cStringUsingEncoding:NSUTF8StringEncoding]); + // } if (fontId / maxVariants_Fonts * maxVariants_Fonts == monospace_FontId) { - font = [UIFont monospacedSystemFontOfSize:0.8f * height weight:UIFontWeightRegular]; +// font = [UIFont monospacedSystemFontOfSize:0.8f * height weight:UIFontWeightRegular]; +// for (NSString *name in [UIFont fontNamesForFamilyName:@"Iosevka Term"]) { +// printf("fontname: %s\n", [name cStringUsingEncoding:NSUTF8StringEncoding]); +// } + font = [UIFont fontWithName:@"Iosevka-Term-Extended" size:height * 0.8f]; } else { // font = [UIFont systemFontOfSize:0.65f * height]; -// for (NSString *name in [UIFont fontNamesForFamilyName:@"Source Sans 3"]) { -// printf("fontname: %s\n", [name cStringUsingEncoding:NSUTF8StringEncoding]); -// } font = [UIFont fontWithName:@"SourceSans3-Regular" size:height * 0.7f]; } if (d->field) { diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index 3572d043..d62bc77e 100644 --- a/src/ui/inputwidget.c +++ b/src/ui/inputwidget.c @@ -711,10 +711,11 @@ static void startOrStopCursorTimer_InputWidget_(iInputWidget *d, iBool doStart) static void updateAllLinesAndResizeHeight_InputWidget_(iInputWidget *d) { /* Rewrap the buffered text and resize accordingly. */ iWrapText wt = wrap_InputWidget_(d, 0); + /* TODO: Set max lines limit for WrapText. */ const int height = measure_WrapText(&wt, d->font).bounds.size.y; /* We use this to store the number wrapped lines for determining widget height. */ d->visWrapLines.start = 0; - d->visWrapLines.end = height / lineHeight_Text(d->font); + d->visWrapLines.end = iMin(d->maxWrapLines, height / lineHeight_Text(d->font)); updateMetrics_InputWidget_(d); } -- cgit v1.2.3 From f135d7c989e397d00434d8e8b4b90dbb9a9950bd Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Mon, 6 Dec 2021 07:23:49 +0200 Subject: DocumentWidget: Footer buttons vs. phone toolbar The toolbar should only be accounted for in portrait orientation. Footer buttons no longer have a padding for the toolbar. --- src/ui/documentwidget.c | 60 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 823c37ac..fe2816d2 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -532,12 +532,28 @@ static int pageHeight_DocumentWidget_(const iDocumentWidget *d) { return height_Banner(d->banner) + documentTopPad_DocumentWidget_(d) + size_GmDocument(d->doc).y; } -static int footerButtonsHeight_DocumentWidget_(const iDocumentWidget *d) { - int height = height_Widget(d->footerButtons); -// if (height) { -// height += 3 * gap_UI; /* padding */ -// } - return height; +//static int footerButtonsHeight_DocumentWidget_(const iDocumentWidget *d) { +// int height = height_Widget(d->footerButtons); +//// if (height) { +//// height += 3 * gap_UI; /* padding */ +//// } +// return height; +//} + +static int phoneToolbarHeight_DocumentWidget_(const iDocumentWidget *d) { + if (!d->phoneToolbar) { + return 0; + } + const iWidget *w = constAs_Widget(d); + return bottom_Rect(rect_Root(w->root)) - top_Rect(boundsWithoutVisualOffset_Widget(d->phoneToolbar)); +} + +static int footerHeight_DocumentWidget_(const iDocumentWidget *d) { + int hgt = height_Widget(d->footerButtons); + if (isPortraitPhone_App()) { + hgt += phoneToolbarHeight_DocumentWidget_(d); + } + return hgt; } static iRect documentBounds_DocumentWidget_(const iDocumentWidget *d) { @@ -559,10 +575,10 @@ static iRect documentBounds_DocumentWidget_(const iDocumentWidget *d) { rect.size.y = 0; wasCentered = iTrue; } - else if (docSize < rect.size.y - footerButtonsHeight_DocumentWidget_(d)) { + else if (docSize < rect.size.y - footerHeight_DocumentWidget_(d)) { /* TODO: Phone toolbar? */ /* Center vertically when the document is short. */ - const int relMidY = (height_Rect(bounds) - footerButtonsHeight_DocumentWidget_(d)) / 2; + const int relMidY = (height_Rect(bounds) - footerHeight_DocumentWidget_(d)) / 2; const int visHeight = size_GmDocument(d->doc).y; const int offset = -height_Banner(d->banner) - documentTopPad_DocumentWidget_(d); rect.pos.y = top_Rect(bounds) + iMaxi(0, relMidY - visHeight / 2 + offset); @@ -657,9 +673,9 @@ static float normScrollPos_DocumentWidget_(const iDocumentWidget *d) { static int scrollMax_DocumentWidget_(const iDocumentWidget *d) { const iWidget *w = constAs_Widget(d); - int sm = pageHeight_DocumentWidget_(d) - height_Rect(bounds_Widget(w)) + + int sm = pageHeight_DocumentWidget_(d) + (isEmpty_Banner(d->banner) ? 2 : 1) * d->pageMargin * gap_UI + /* top and bottom margins */ - iMax(height_Widget(d->phoneToolbar), height_Widget(d->footerButtons)); + footerHeight_DocumentWidget_(d) - height_Rect(bounds_Widget(w)); return sm; } @@ -912,11 +928,8 @@ static void updateVisible_DocumentWidget_(iDocumentWidget *d) { const iRect bounds = bounds_Widget(as_Widget(d)); const int scrollMax = updateScrollMax_DocumentWidget_(d); /* Reposition the footer buttons as appropriate. */ - /* TODO: You can just position `footerButtons` here completely without having to get - `Widget` involved with the offset in any way. */ setRange_ScrollWidget(d->scroll, (iRangei){ 0, scrollMax }); - const int docSize = pageHeight_DocumentWidget_(d) + iMax(height_Widget(d->phoneToolbar), - height_Widget(d->footerButtons)); + const int docSize = pageHeight_DocumentWidget_(d) + footerHeight_DocumentWidget_(d); const float scrollPos = pos_SmoothScroll(&d->scrollY); setThumb_ScrollWidget(d->scroll, pos_SmoothScroll(&d->scrollY), @@ -927,7 +940,7 @@ static void updateVisible_DocumentWidget_(iDocumentWidget *d) { const int hPad = (width_Rect(bounds) - iMin(120 * gap_UI, width_Rect(docBounds))) / 2; const int vPad = 3 * gap_UI; setPadding_Widget(d->footerButtons, hPad, 0, hPad, vPad); - d->footerButtons->rect.pos.y = height_Rect(bounds) - height_Widget(d->footerButtons) + + d->footerButtons->rect.pos.y = height_Rect(bounds) - footerHeight_DocumentWidget_(d) + (scrollMax > 0 ? scrollMax - scrollPos : 0); } clear_PtrArray(&d->visibleLinks); @@ -1198,10 +1211,10 @@ static void makeFooterButtons_DocumentWidget_(iDocumentWidget *d, const iMenuIte checkIcon_LabelWidget(button); setFont_LabelWidget(button, uiContent_FontId); } - if (deviceType_App() == phone_AppDeviceType) { +// if (deviceType_App() == phone_AppDeviceType) { /* Footer buttons shouldn't be under the toolbar. */ - addChild_Widget(d->footerButtons, iClob(makePadding_Widget(height_Widget(d->phoneToolbar)))); - } +// addChild_Widget(d->footerButtons, iClob(makePadding_Widget(height_Widget(d->phoneToolbar)))); +// } addChild_Widget(as_Widget(d), iClob(d->footerButtons)); arrange_Widget(d->footerButtons); arrange_Widget(w); @@ -2081,7 +2094,12 @@ static iString *makeQueryUrl_DocumentWidget_(const iDocumentWidget *d, remove_Block(&url->chars, qPos, iInvalidSize); } appendCStr_String(url, "?"); - append_String(url, collect_String(urlEncode_String(userEnteredText))); + iString *cleaned = copy_String(userEnteredText); + if (deviceType_App() != desktop_AppDeviceType) { + trim_String(cleaned); /* autocorrect may insert an extra space */ + } + append_String(url, collect_String(urlEncode_String(cleaned))); + delete_String(cleaned); return url; } @@ -5155,7 +5173,7 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) { iRect indRect = initCentered_Rect(init_I2(mid_Rect(bounds).x, top_Rect(bounds) - 5 * gap_UI - pos_SmoothScroll(&d->scrollY)), - init_I2(25 * gap_UI, 2 * gap_UI)); + init_I2(20 * gap_UI, 2 * gap_UI)); setClip_Paint(&ctx.paint, clipBounds); int color = pullPos < 1.0f ? tmBannerItemFrame_ColorId : tmBannerItemText_ColorId; drawRect_Paint(&ctx.paint, indRect, color); @@ -5240,7 +5258,7 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) { mut->flags &= ~refChildrenOffset_WidgetFlag; } } -// drawRect_Paint(&ctx.paint, docBounds, red_ColorId); + drawRect_Paint(&ctx.paint, docBounds, red_ColorId); } /*----------------------------------------------------------------------------------------------*/ -- cgit v1.2.3 From f3e1ede71056e121b80206055fab0b4045107c2f Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Mon, 6 Dec 2021 08:11:31 +0200 Subject: Mobile: Tablet fixes Various issues with safe areas and font sizes. --- src/app.c | 2 +- src/ui/documentwidget.c | 2 +- src/ui/mobile.c | 13 ++++++++++++- src/ui/mobile.h | 2 ++ src/ui/root.c | 14 +++++++++++++- src/ui/sidebarwidget.c | 31 +++++++++++++++++++++---------- src/ui/util.c | 22 +++++++++++----------- 7 files changed, 61 insertions(+), 25 deletions(-) diff --git a/src/app.c b/src/app.c index 160d76f6..8cdb4903 100644 --- a/src/app.c +++ b/src/app.c @@ -498,7 +498,7 @@ static iBool loadState_App_(iApp *d) { if (flags & 8) { postCommand_Widget(sidebar2, "feeds.mode arg:%d", unread_FeedsMode); } - if (deviceType_App() != phone_AppDeviceType) { + if (deviceType_App() == desktop_AppDeviceType) { setWidth_SidebarWidget(sidebar, widths[0]); setWidth_SidebarWidget(sidebar2, widths[1]); if (flags & 1) postCommand_Root(root, "sidebar.toggle noanim:1"); diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index fe2816d2..c8430009 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -5258,7 +5258,7 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) { mut->flags &= ~refChildrenOffset_WidgetFlag; } } - drawRect_Paint(&ctx.paint, docBounds, red_ColorId); +// drawRect_Paint(&ctx.paint, docBounds, red_ColorId); } /*----------------------------------------------------------------------------------------------*/ diff --git a/src/ui/mobile.c b/src/ui/mobile.c index 90dfafde..947c0cbf 100644 --- a/src/ui/mobile.c +++ b/src/ui/mobile.c @@ -590,7 +590,8 @@ void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) { setFlags_Widget(as_Widget(button), fixedWidth_WidgetFlag, iFalse); } setId_Widget(as_Widget(button), radId); - setFont_LabelWidget(button, isHorizontal ? uiLabelMedium_FontId : uiLabelBig_FontId); + setFont_LabelWidget(button, deviceType_App() == phone_AppDeviceType ? + (isHorizontal ? uiLabelMedium_FontId : uiLabelBig_FontId) : labelFont_()); setCheckMark_LabelWidget(button, !isHorizontal); setPadding_Widget(as_Widget(button), gap_UI, 1 * gap_UI, 0, 1 * gap_UI); updateSize_LabelWidget(button); @@ -984,3 +985,13 @@ void setupSheetTransition_Mobile(iWidget *sheet, int flags) { } } } + +int bottomSafeInset_Mobile(void) { +#if defined (iPlatformAppleMobile) + float bot; + safeAreaInsets_iOS(NULL, NULL, NULL, &bot); + return iRound(bot); +#else + return 0; +#endif +} diff --git a/src/ui/mobile.h b/src/ui/mobile.h index 06955945..54f55fd2 100644 --- a/src/ui/mobile.h +++ b/src/ui/mobile.h @@ -56,3 +56,5 @@ enum iTransitionDir { void setupMenuTransition_Mobile (iWidget *menu, iBool isIncoming); void setupSheetTransition_Mobile (iWidget *sheet, int flags); + +int bottomSafeInset_Mobile (void); diff --git a/src/ui/root.c b/src/ui/root.c index 18a71200..a5dcd46d 100644 --- a/src/ui/root.c +++ b/src/ui/root.c @@ -333,6 +333,11 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) { openMenuFlags_Widget(menu, zero_I2(), postCommands_MenuOpenFlags | center_MenuOpenFlags); return iTrue; } + else if (deviceType_App() == tablet_AppDeviceType && equal_Command(cmd, "toolbar.showident")) { + /* No toolbar on tablet, so we handle this command here. */ + postCommand_App("preferences idents:1"); + return iTrue; + } else if (equal_Command(cmd, "identmenu.open")) { iWidget *toolBar = findWidget_Root("toolbar"); iWidget *button = findWidget_Root(toolBar && isPortraitPhone_App() ? "toolbar.ident" : "navbar.ident"); @@ -486,6 +491,13 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) { SDL_PushEvent(&(SDL_Event){ .type = SDL_QUIT }); return iTrue; } + else if (deviceType_App() == tablet_AppDeviceType && equal_Command(cmd, "window.resized")) { + iSidebarWidget *sidebar = findChild_Widget(root, "sidebar"); + iSidebarWidget *sidebar2 = findChild_Widget(root, "sidebar2"); + setWidth_SidebarWidget(sidebar, 73.0f); + setWidth_SidebarWidget(sidebar2, 73.0f); + return iFalse; + } else if (deviceType_App() == phone_AppDeviceType && equal_Command(cmd, "window.resized")) { /* Place the sidebar next to or under doctabs depending on orientation. */ iSidebarWidget *sidebar = findChild_Widget(root, "sidebar"); @@ -1056,7 +1068,7 @@ static iBool handleToolBarCommands_(iWidget *toolBar, const char *cmd) { return iTrue; } else if (equal_Command(cmd, "toolbar.showident")) { - iWidget *sidebar = findWidget_App("sidebar"); + iWidget *sidebar = findWidget_App("sidebar"); if (isVisible_Widget(sidebar)) { postCommandf_App("sidebar.toggle"); } diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index d4b1ee20..c7acc0ef 100644 --- a/src/ui/sidebarwidget.c +++ b/src/ui/sidebarwidget.c @@ -35,6 +35,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "inputwidget.h" #include "labelwidget.h" #include "listwidget.h" +#include "mobile.h" #include "keys.h" #include "paint.h" #include "root.h" @@ -206,6 +207,18 @@ int cmpTree_Bookmark(const iBookmark **a, const iBookmark **b) { return cmpStringCase_String(&bm1->title, &bm2->title); } +static enum iFontId actionButtonFont_SidebarWidget_(const iSidebarWidget *d) { + switch (deviceType_App()) { + default: + break; + case phone_AppDeviceType: + return uiLabelBig_FontId; + case tablet_AppDeviceType: + return uiLabelMedium_FontId; + } + return d->buttonFont; +} + static iLabelWidget *addActionButton_SidebarWidget_(iSidebarWidget *d, const char *label, const char *command, int64_t flags) { iLabelWidget *btn = addChildFlags_Widget(d->actions, @@ -213,14 +226,12 @@ static iLabelWidget *addActionButton_SidebarWidget_(iSidebarWidget *d, const cha //(deviceType_App() != desktop_AppDeviceType ? // extraPadding_WidgetFlag : 0) | flags); - setFont_LabelWidget(btn, deviceType_App() == desktop_AppDeviceType ? /*deviceType_App() == phone_AppDeviceType && d->side == right_SidebarSide - ? uiLabelBig_FontId : */ - d->buttonFont : uiLabelBig_FontId); + setFont_LabelWidget(btn, actionButtonFont_SidebarWidget_(d)); checkIcon_LabelWidget(btn); if (deviceType_App() != desktop_AppDeviceType) { setFlags_Widget(as_Widget(btn), frameless_WidgetFlag, iTrue); - setBackgroundColor_Widget(as_Widget(btn), uiBackground_ColorId); setTextColor_LabelWidget(btn, uiTextAction_ColorId); + setBackgroundColor_Widget(as_Widget(btn), uiBackground_ColorId); } return btn; } @@ -886,7 +897,7 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) { resizeToParentHeight_WidgetFlag | (side == left_SidebarSide ? moveToParentRightEdge_WidgetFlag : moveToParentLeftEdge_WidgetFlag)); - if (deviceType_App() == phone_AppDeviceType) { + if (deviceType_App() != desktop_AppDeviceType) { setFlags_Widget(d->resizer, hidden_WidgetFlag | disabled_WidgetFlag, iTrue); } setId_Widget(d->resizer, side == left_SidebarSide ? "sidebar.grab" : "sidebar2.grab"); @@ -1308,8 +1319,12 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) drawBackgroundToBottom_WidgetFlag, isPortrait_App()); setBackgroundColor_Widget(w, isPortrait_App() ? uiBackgroundSidebar_ColorId : none_ColorId); - return iFalse; } + if (!isPortraitPhone_App()) { + /* In sliding sheet mode, sidebar is resized to fit in the safe area. */ + setPadding_Widget(d->actions, 0, 0, 0, bottomSafeInset_Mobile()); + } + return iFalse; } else if (isMetricsChange_UserEvent(ev)) { if (isVisible_Widget(w)) { @@ -1348,10 +1363,6 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) } } } -// else if (deviceType_App() == tablet_AppDeviceType && equal_Command(cmd, "toolbar.showident")) { -// postCommandf_App("sidebar.mode arg:%d toggle:1", identities_SidebarMode); -// return iTrue; -// } else if (isPortraitPhone_App() && isVisible_Widget(w) && d->side == left_SidebarSide && equal_Command(cmd, "swipe.forward")) { postCommand_App("sidebar.toggle"); diff --git a/src/ui/util.c b/src/ui/util.c index 8aedef99..9576f5e2 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -924,17 +924,17 @@ static void updateMenuItemFonts_Widget_(iWidget *d) { if (isWrapped_LabelWidget(label)) { continue; } - if (deviceType_App() == desktop_AppDeviceType) { - setFont_LabelWidget(label, isCaution ? uiLabelBold_FontId : uiLabel_FontId); + switch (deviceType_App()) { + case desktop_AppDeviceType: + setFont_LabelWidget(label, isCaution ? uiLabelBold_FontId : uiLabel_FontId); + break; + case tablet_AppDeviceType: + setFont_LabelWidget(label, isCaution ? uiLabelMediumBold_FontId : uiLabelMedium_FontId); + break; + case phone_AppDeviceType: + setFont_LabelWidget(label, isCaution ? uiLabelBigBold_FontId : uiLabelBig_FontId); + break; } - else { //if (isPortraitPhone) { - //if (!isSlidePanel) { - setFont_LabelWidget(label, isCaution ? uiLabelBigBold_FontId : uiLabelBig_FontId); - // } - } -// else { -// setFont_LabelWidget(label, isCaution ? uiContentBold_FontId : uiContent_FontId); -// } } else if (childCount_Widget(i.object)) { updateMenuItemFonts_Widget_(i.object); @@ -1734,7 +1734,7 @@ iWidget *makeDialogButtons_Widget(const iMenuItem *actions, size_t numActions) { addChildFlags_Widget(div, iClob(new_Widget()), expand_WidgetFlag); } int fonts[2] = { uiLabel_FontId, uiLabelBold_FontId }; - if (deviceType_App() == phone_AppDeviceType) { + if (deviceType_App() != desktop_AppDeviceType) { fonts[0] = uiLabelBig_FontId; fonts[1] = uiLabelBigBold_FontId; } -- cgit v1.2.3 From 96e469636c8573238b04f48d648c22caa7c6c0fa Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Mon, 6 Dec 2021 09:19:36 +0200 Subject: iOS: Portrait phone sidebar actions padding --- src/ui/sidebarwidget.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index c7acc0ef..249bf373 100644 --- a/src/ui/sidebarwidget.c +++ b/src/ui/sidebarwidget.c @@ -1300,6 +1300,7 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) checkModeButtonLayout_SidebarWidget_(d); if (deviceType_App() == phone_AppDeviceType) { // && d->side == left_SidebarSide) { // setFlags_Widget(w, rightEdgeDraggable_WidgetFlag, isPortrait_App()); + setPadding_Widget(d->actions, 0, 0, 0, 0); setFlags_Widget(findChild_Widget(w, "sidebar.title"), hidden_WidgetFlag, isLandscape_App()); setFlags_Widget(findChild_Widget(w, "sidebar.close"), hidden_WidgetFlag, isLandscape_App()); /* In landscape, visibility of the toolbar is controlled separately. */ -- cgit v1.2.3 From ff316e540269a34214d298249190db8891b2d885 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Mon, 6 Dec 2021 09:19:47 +0200 Subject: Mobile: Tablet fixes --- src/ui/documentwidget.c | 2 +- src/ui/mobile.c | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index c8430009..d5ab5704 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -2983,7 +2983,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) // setFixedSize_Widget(sizer, init_I2(gap_UI * 65, 1)); // addChildFlags_Widget(dlg, iClob(sizer), frameless_WidgetFlag); // setFlags_Widget(dlg, centerHorizontal_WidgetFlag, iFalse); - if (deviceType_App() != phone_AppDeviceType) { + if (deviceType_App() == desktop_AppDeviceType) { const iWidget *lockButton = findWidget_Root("navbar.lock"); setPos_Widget(dlg, windowToLocal_Widget(dlg, bottomLeft_Rect(bounds_Widget(lockButton)))); } diff --git a/src/ui/mobile.c b/src/ui/mobile.c index 947c0cbf..fdcc4248 100644 --- a/src/ui/mobile.c +++ b/src/ui/mobile.c @@ -130,9 +130,10 @@ static iBool mainDetailSplitHandler_(iWidget *mainDetailSplit, const char *cmd) iAssert(topPanel); topPanel->rect.size.x = (deviceType_App() == phone_AppDeviceType ? safeRoot.size.x * 2 / 5 : (safeRoot.size.x / 3)); - } + } if (deviceType_App() == tablet_AppDeviceType) { setPadding_Widget(topPanel, pad, 0, pad, pad); +#if 0 if (numPanels == 0) { setFlags_Widget(sheet, centerHorizontal_WidgetFlag, iTrue); const int sheetWidth = iMin(safeRoot.size.x, safeRoot.size.y); @@ -140,6 +141,7 @@ static iBool mainDetailSplitHandler_(iWidget *mainDetailSplit, const char *cmd) setFixedSize_Widget(sheet, init_I2(sheetWidth, -1)); setFixedSize_Widget(navi, init_I2(sheetWidth, -1)); } +#endif } iWidget *detailTitle = findChild_Widget(navi, "detailtitle"); { setPos_Widget(detailTitle, init_I2(width_Widget(topPanel), 0)); @@ -722,6 +724,7 @@ void initPanels_Mobile(iWidget *panels, iWidget *parentWidget, setFlags_Widget(panels, resizeToParentWidth_WidgetFlag | resizeToParentHeight_WidgetFlag | frameless_WidgetFlag | focusRoot_WidgetFlag | commandOnClick_WidgetFlag | + horizontalOffset_WidgetFlag | /*overflowScrollable_WidgetFlag |*/ leftEdgeDraggable_WidgetFlag, iTrue); setFlags_Widget(panels, overflowScrollable_WidgetFlag, iFalse); -- cgit v1.2.3 From 7e69aeb1a8157eb34981a21930f108a0a7efad9c Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Mon, 6 Dec 2021 09:20:30 +0200 Subject: iOS: Platform-specific release notes and Help TODO: Help needs rewriting from a mobile perspective. --- CMakeLists.txt | 15 ++++++++++++--- res/about/ios-help.gmi | 47 +++++++++++++++++++++++++++++++++++++++++++++++ res/about/ios-version.gmi | 20 ++++++++++++++++++++ src/resources.c | 7 ++++++- 4 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 res/about/ios-help.gmi create mode 100644 res/about/ios-version.gmi diff --git a/CMakeLists.txt b/CMakeLists.txt index 71940aa4..a6943885 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -66,11 +66,8 @@ include (Depends.cmake) message (STATUS "Preparing resources...") set (RESOURCES res/about/about.gmi - res/about/help.gmi res/about/lagrange.gmi res/about/license.gmi - res/about/version.gmi - res/arg-help.txt res/lang/cs.bin res/lang/de.bin res/lang/en.bin @@ -98,6 +95,18 @@ set (RESOURCES ) file (GLOB FONTS RELATIVE ${CMAKE_SOURCE_DIR} res/fonts/*) list (APPEND RESOURCES ${FONTS}) +if (IOS) + list (APPEND RESOURCES + res/about/ios-help.gmi + res/about/ios-version.gmi + ) +else () + list (APPEND RESOURCES + res/about/help.gmi + res/about/version.gmi + res/arg-help.txt + ) +endif () if ((UNIX AND NOT APPLE) OR MSYS) list (APPEND RESOURCES res/lagrange-64.png) endif () diff --git a/res/about/ios-help.gmi b/res/about/ios-help.gmi new file mode 100644 index 00000000..1425b6bc --- /dev/null +++ b/res/about/ios-help.gmi @@ -0,0 +1,47 @@ +```LAGRANGE + __ __ __ ___ +| /\ / _` |__) /\ |\ | / _` |__ +|___ /~~\ \__> | \ /~~\ | \| \__> |___ + +``` +# Help + +## What is Gemini + +Gemini is a simple protocol for serving content over the internet. It specifies a Markdown inspired format allowing basic plain text document markup. Compared to HTTP and HTML, Gemini is vastly simpler and easier to work with. + +=> gemini://gemini.circumlunar.space/docs/faq.gmi Project Gemini FAQ +=> gemini://gemini.circumlunar.space/docs/specification.gmi Protocol and 'text/gemini' specification + +## What is Lagrange + +Lagrange is a GUI client for browsing Geminispace. It offers modern conveniences familiar from web browsers, such as smooth scrolling, inline image viewing, multiple tabs, visual themes, Unicode fonts, bookmarks, history, and page outlines. + +Like Gemini, Lagrange has been designed with minimalism in mind. It depends on a small number of essential libraries. It is written in C and uses SDL for hardware-accelerated graphics. OpenSSL is used for secure communications. + +=> about:lagrange About Lagrange +=> https://www.libsdl.org SDL: Simple DirectMedia Layer +=> https://www.openssl.org OpenSSL: Cryptography and SSL/TLS Toolkit + +### Features + +* Beautiful typography with full Unicode support +* Autogenerated page style and symbol for each Gemini domain +* Light and dark color themes +* Scaling page content (50%...200%) +* Freely adjustable scaling factor for the UI +* Sidebar for managing bookmarks, subscribed feeds, and viewing browsing history and the page outline +* Multiple tabs +* Split view for browsing two pages at once (iPad only) +* Find text on the page +* Open image and audio links inline on the same page +* Smart suggestions when typing an URL — search bookmarks, history, identities +* Search engine integration +* Identity management — create and use TLS client certificates +* Subscribe to Gemini and Atom feeds +* Use Gemini pages as a source of bookmarks +* Audio playback: MP3, Ogg Vorbis, WAV +* Read Gempub books and view ZIP archive contents +* Built-in support for Gopher +* Built-in support for uploading data using the Titan protocol +* Use proxy servers for HTTP, Gopher, or Gemini content diff --git a/res/about/ios-version.gmi b/res/about/ios-version.gmi new file mode 100644 index 00000000..1f35129b --- /dev/null +++ b/res/about/ios-version.gmi @@ -0,0 +1,20 @@ +```LAGRANGE + __ __ __ ___ +| /\ / _` |__) /\ |\ | / _` |__ +|___ /~~\ \__> | \ /~~\ | \| \__> |___ + +``` +# Release notes + +## 1.10 (1) +* Use native iOS text input UI controls for entering text. +* Redesigned phone sidebar that mimics Safari on iPhone. Opens half-height for better reachability of tab buttons, can be swiped up and down. +* Bookmark edit mode for reordering and organizing bookmarks into folders. +* Sidebar has new action buttons for Feeds and History. +* Identity toolbar button has been revised for quickly switching between alternate identities for current site, and opening Settings > Identities. +* Settings: Managing identities. +* Settings: Improved widgets used in button/radio groups to closer match iOS design. +* Settings: Added new options from desktop version. +* Mobile-friendly layout for value input dialogs. +* Various animation issues fixes, e.g., context menu animations on iPad. +* Various layout issues fixed, e.g., related to safe area insets. diff --git a/src/resources.c b/src/resources.c index c60a2916..bb601cca 100644 --- a/src/resources.c +++ b/src/resources.c @@ -63,10 +63,15 @@ static struct { const char *archivePath; } entries_[] = { { &blobAbout_Resources, "about/about.gmi" }, - { &blobHelp_Resources, "about/help.gmi" }, { &blobLagrange_Resources, "about/lagrange.gmi" }, { &blobLicense_Resources, "about/license.gmi" }, +#if defined (iPlatformAppleMobile) + { &blobHelp_Resources, "about/ios-help.gmi" }, + { &blobVersion_Resources, "about/ios-version.gmi" }, +#else + { &blobHelp_Resources, "about/help.gmi" }, { &blobVersion_Resources, "about/version.gmi" }, +#endif { &blobArghelp_Resources, "arg-help.txt" }, { &blobCs_Resources, "lang/cs.bin" }, { &blobDe_Resources, "lang/de.bin" }, -- cgit v1.2.3 From 7ed5ee3318d6d228f0163c536d87701107bd6ad2 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Mon, 6 Dec 2021 09:22:21 +0200 Subject: iOS: Updated build date --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a6943885..85a32b69 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,7 +26,7 @@ set (COPYRIGHT_YEAR 2021) if (IOS) set (PROJECT_VERSION 1.10) set (IOS_BUNDLE_VERSION 1) - set (IOS_BUILD_DATE "2021-12-05") + set (IOS_BUILD_DATE "2021-12-06") endif () # Defaults that depend on environment. -- cgit v1.2.3 From b6ed68e5d4bb273e227c105186a84acf18736382 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Mon, 6 Dec 2021 09:42:37 +0200 Subject: iOS: Updated release notes --- res/about/ios-version.gmi | 1 + 1 file changed, 1 insertion(+) diff --git a/res/about/ios-version.gmi b/res/about/ios-version.gmi index 1f35129b..45fde9e6 100644 --- a/res/about/ios-version.gmi +++ b/res/about/ios-version.gmi @@ -8,6 +8,7 @@ ## 1.10 (1) * Use native iOS text input UI controls for entering text. +* Pull-to-refresh on pages. * Redesigned phone sidebar that mimics Safari on iPhone. Opens half-height for better reachability of tab buttons, can be swiped up and down. * Bookmark edit mode for reordering and organizing bookmarks into folders. * Sidebar has new action buttons for Feeds and History. -- cgit v1.2.3 From 39dfbe85259d2e67b355a6a840ce47e06474202c Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 7 Dec 2021 14:00:40 +0200 Subject: SidebarWidget: Edit mode vs. folders; "No Unread Entries" --- po/en.po | 3 +++ src/ui/sidebarwidget.c | 30 +++++++++++++++++++----------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/po/en.po b/po/en.po index 0ac1dd2e..8e690649 100644 --- a/po/en.po +++ b/po/en.po @@ -686,6 +686,9 @@ msgstr "Delete Identity and Files" msgid "sidebar.empty.idents" msgstr "No Identities" +msgid "sidebar.empty.unread" +msgstr "No Unread Entries" + # The %s format characters are used to highlight the word "Help" and must be used in the translation in same way as here. #, c-format msgid "ident.gotohelp" diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index 249bf373..c5596b9f 100644 --- a/src/ui/sidebarwidget.c +++ b/src/ui/sidebarwidget.c @@ -299,12 +299,12 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct if (secondsSince_Time(&now, &entry->discovered) > maxAge_Visited) { break; /* the rest are even older */ } - isEmpty = iFalse; const iBool isOpen = equal_String(docUrl, &entry->url); const iBool isUnread = isUnread_FeedEntry(entry); if (d->feedsMode == unread_FeedsMode && !isUnread && !isOpen) { continue; } + isEmpty = iFalse; /* Insert date separators. */ { iDate entryDate; init_Date(&entryDate, &entry->posted); @@ -600,9 +600,18 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct if (isEmpty) { if (d->mode == feeds_SidebarMode) { iWidget *div = makeVDiv_Widget(); - setPadding_Widget(div, 3 * gap_UI, 0, 3 * gap_UI, 2 * gap_UI); + //setPadding_Widget(div, 3 * gap_UI, 0, 3 * gap_UI, 2 * gap_UI); + arrange_Widget(d->actions); + setPadding_Widget(div, 0, 0, 0, height_Widget(d->actions)); addChildFlags_Widget(div, iClob(new_Widget()), expand_WidgetFlag); /* pad */ - addChild_Widget(div, iClob(new_LabelWidget("${menu.feeds.refresh}", "feeds.refresh"))); + if (d->feedsMode == all_FeedsMode) { + addChild_Widget(div, iClob(new_LabelWidget("${menu.feeds.refresh}", "feeds.refresh"))); + } + else { + iLabelWidget *msg = addChildFlags_Widget(div, iClob(new_LabelWidget("${sidebar.empty.unread}", NULL)), + frameless_WidgetFlag); + setFont_LabelWidget(msg, uiLabelLarge_FontId); + } addChildFlags_Widget(div, iClob(new_Widget()), expand_WidgetFlag); /* pad */ addChild_Widget(d->blank, iClob(div)); } @@ -963,12 +972,6 @@ static void itemClicked_SidebarWidget_(iSidebarWidget *d, iSidebarItem *item, si break; } case bookmarks_SidebarMode: - if (d->isEditing) { - d->contextItem = item; - d->contextIndex = itemIndex; - postCommand_Widget(d, "bookmark.edit"); - break; - } if (isEmpty_String(&item->url)) /* a folder */ { if (contains_IntSet(d->closedFolders, item->id)) { remove_IntSet(d->closedFolders, item->id); @@ -981,6 +984,12 @@ static void itemClicked_SidebarWidget_(iSidebarWidget *d, iSidebarItem *item, si updateItems_SidebarWidget_(d); break; } + if (d->isEditing) { + d->contextItem = item; + d->contextIndex = itemIndex; + postCommand_Widget(d, "bookmark.edit"); + break; + } /* fall through */ case history_SidebarMode: { if (!isEmpty_String(&item->url)) { @@ -1298,8 +1307,7 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) /* Handle commands. */ if (isResize_UserEvent(ev)) { checkModeButtonLayout_SidebarWidget_(d); - if (deviceType_App() == phone_AppDeviceType) { // && d->side == left_SidebarSide) { -// setFlags_Widget(w, rightEdgeDraggable_WidgetFlag, isPortrait_App()); + if (deviceType_App() == phone_AppDeviceType) { setPadding_Widget(d->actions, 0, 0, 0, 0); setFlags_Widget(findChild_Widget(w, "sidebar.title"), hidden_WidgetFlag, isLandscape_App()); setFlags_Widget(findChild_Widget(w, "sidebar.close"), hidden_WidgetFlag, isLandscape_App()); -- cgit v1.2.3 From f09b3f74dc512ee2fc09cba43c4ee7f07b895d90 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 7 Dec 2021 14:01:05 +0200 Subject: InputWidget: Buffered content is limited by max height --- src/ui/inputwidget.c | 2 +- src/ui/text.c | 5 +++++ src/ui/text.h | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index d62bc77e..1935826b 100644 --- a/src/ui/inputwidget.c +++ b/src/ui/inputwidget.c @@ -1038,7 +1038,6 @@ static iBool isHintVisible_InputWidget_(const iInputWidget *d) { static void updateBuffered_InputWidget_(iInputWidget *d) { invalidateBuffered_InputWidget_(d); if (isHintVisible_InputWidget_(d)) { - /* TODO: This should have a "maximum number of lines" parameter! */ d->buffered = newRange_TextBuf(d->font, uiAnnotation_ColorId, range_String(&d->hint)); } else { @@ -1069,6 +1068,7 @@ static void updateBuffered_InputWidget_(iInputWidget *d) { } } iWrapText wt = wrap_InputWidget_(d, 0); + wt.maxLines = d->maxWrapLines; wt.text = range_String(visText); const int fg = uiInputText_ColorId; d->buffered = new_TextBuf(&wt, d->font, fg); diff --git a/src/ui/text.c b/src/ui/text.c index 76a1ee19..94569a6a 100644 --- a/src/ui/text.c +++ b/src/ui/text.c @@ -1406,6 +1406,7 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) { iBool isFirst = iTrue; const iBool checkHitPoint = wrap && !isEqual_I2(wrap->hitPoint, zero_I2()); const iBool checkHitChar = wrap && wrap->hitChar; + size_t numWrapLines = 0; while (!isEmpty_Range(&wrapRuns)) { if (isFirst) { isFirst = iFalse; @@ -1635,6 +1636,10 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) { iRound(wrapAdvance))) { willAbortDueToWrap = iTrue; } + numWrapLines++; + if (wrap && wrap->maxLines && numWrapLines == wrap->maxLines) { + willAbortDueToWrap = iTrue; + } wrapAttrib = lastAttrib; xCursor = origin; /* We have determined a possible wrap position and alignment for the work runs, diff --git a/src/ui/text.h b/src/ui/text.h index 63499484..b7934855 100644 --- a/src/ui/text.h +++ b/src/ui/text.h @@ -202,6 +202,7 @@ struct Impl_WrapText { /* arguments */ iRangecc text; int maxWidth; + size_t maxLines; /* 0: unlimited */ enum iWrapTextMode mode; iBool (*wrapFunc)(iWrapText *, iRangecc wrappedText, iTextAttrib attrib, int origin, int advance); @@ -239,6 +240,5 @@ struct Impl_TextBuf { iInt2 size; }; -iTextBuf * newRange_TextBuf (int font, int color, iRangecc text); - +iTextBuf * newRange_TextBuf(int font, int color, iRangecc text); void draw_TextBuf (const iTextBuf *, iInt2 pos, int color); -- cgit v1.2.3 From 91da66e855872972618d30e945c8b8932690388e Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 7 Dec 2021 14:01:40 +0200 Subject: CertListWidget: Show current page's URL in menu --- src/ui/certlistwidget.c | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/ui/certlistwidget.c b/src/ui/certlistwidget.c index 7c140c19..31d8bac6 100644 --- a/src/ui/certlistwidget.c +++ b/src/ui/certlistwidget.c @@ -91,6 +91,12 @@ static iGmIdentity *menuIdentity_CertListWidget_(const iCertListWidget *d) { static void updateContextMenu_CertListWidget_(iCertListWidget *d) { iArray *items = collectNew_Array(sizeof(iMenuItem)); + const iString *docUrl = url_DocumentWidget(document_App()); + size_t firstIndex = 0; + if (deviceType_App() != desktop_AppDeviceType && !isEmpty_String(docUrl)) { + pushBack_Array(items, &(iMenuItem){ format_CStr("```%s", cstr_String(docUrl)) }); + firstIndex = 1; + } pushBackN_Array(items, (iMenuItem[]){ { person_Icon " ${ident.use}", 0, 0, "ident.use arg:1" }, { close_Icon " ${ident.stopuse}", 0, 0, "ident.use arg:0" }, @@ -105,11 +111,10 @@ static void updateContextMenu_CertListWidget_(iCertListWidget *d) { /* Used URLs. */ const iGmIdentity *ident = menuIdentity_CertListWidget_(d); if (ident) { - size_t insertPos = 3; + size_t insertPos = firstIndex + 3; if (!isEmpty_StringSet(ident->useUrls)) { insert_Array(items, insertPos++, &(iMenuItem){ "---", 0, 0, NULL }); } - const iString *docUrl = url_DocumentWidget(document_App()); iBool usedOnCurrentPage = iFalse; iConstForEach(StringSet, i, ident->useUrls) { const iString *url = i.value; @@ -126,10 +131,10 @@ static void updateContextMenu_CertListWidget_(iCertListWidget *d) { format_CStr("!open url:%s", cstr_String(url)) }); } if (!usedOnCurrentPage) { - remove_Array(items, 1); + remove_Array(items, firstIndex + 1); } else { - remove_Array(items, 0); + remove_Array(items, firstIndex); } } destroy_Widget(d->menu); -- cgit v1.2.3 From c6513581b19a020577cf170aa3299db2ed2f2317 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 7 Dec 2021 14:02:00 +0200 Subject: DocumentWidget: Don't trim space if user entered only spaces --- src/ui/documentwidget.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index d5ab5704..c4d7c56a 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -2096,7 +2096,10 @@ static iString *makeQueryUrl_DocumentWidget_(const iDocumentWidget *d, appendCStr_String(url, "?"); iString *cleaned = copy_String(userEnteredText); if (deviceType_App() != desktop_AppDeviceType) { - trim_String(cleaned); /* autocorrect may insert an extra space */ + trimEnd_String(cleaned); /* autocorrect may insert an extra space */ + if (isEmpty_String(cleaned)) { + set_String(cleaned, userEnteredText); /* user wanted just spaces? */ + } } append_String(url, collect_String(urlEncode_String(cleaned))); delete_String(cleaned); -- cgit v1.2.3 From 90adcc9a5bc4936470578e6c3e03e224529827c3 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 7 Dec 2021 14:02:13 +0200 Subject: Cleanup --- src/ui/root.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ui/root.c b/src/ui/root.c index a5dcd46d..d3eb221e 100644 --- a/src/ui/root.c +++ b/src/ui/root.c @@ -1397,7 +1397,6 @@ void createUserInterface_Root(iRoot *d) { } /* The active identity menu. */ { iLabelWidget *idButton = new_LabelWidget(person_Icon, "identmenu.open"); -// "\U0001f464", identityButtonMenuItems_, iElemCount(identityButtonMenuItems_)); setAlignVisually_LabelWidget(idButton, iTrue); setId_Widget(addChildFlags_Widget(navBar, iClob(idButton), collapse_WidgetFlag), "navbar.ident"); } @@ -1459,8 +1458,8 @@ void createUserInterface_Root(iRoot *d) { addChildPos_Widget(content, iClob(sidebar2), back_WidgetAddPos); } else { - /* Sidebar is a slide-over. */ - addChild_Widget(/*findChild_Widget(root, "stack")*/ root, iClob(sidebar1)); + /* Sidebar is a slide-over sheet. */ + addChild_Widget(root, iClob(sidebar1)); setFlags_Widget(as_Widget(sidebar1), hidden_WidgetFlag, iTrue); } } -- cgit v1.2.3 From e51267c1c60534a3684e0730c0b3c0fd425765c9 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 7 Dec 2021 14:02:46 +0200 Subject: Mobile: Use compact input prompt on tablet --- src/ui/util.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/util.c b/src/ui/util.c index 9576f5e2..5a957b47 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -1797,7 +1797,7 @@ iWidget *makeValueInput_Widget(iWidget *parent, const iString *initialValue, con if (parent) { addChild_Widget(parent, iClob(dlg)); } - if (deviceType_App() != phone_AppDeviceType) { + if (deviceType_App() == desktop_AppDeviceType) { /* conserve space on mobile */ setId_Widget( addChildFlags_Widget(dlg, iClob(new_LabelWidget(title, NULL)), frameless_WidgetFlag), -- cgit v1.2.3 From 3652671d15ca208b8f1ce23e9ee4e753cb86f1e8 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 7 Dec 2021 14:11:27 +0200 Subject: SmoothScroll: Allow overflow scrolling always This is needed for triggering pull actions. --- src/ui/util.c | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/ui/util.c b/src/ui/util.c index 5a957b47..097eb548 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -561,13 +561,8 @@ void moveSpan_SmoothScroll(iSmoothScroll *d, int offset, uint32_t span) { if (destY < -d->overscroll) { destY = -d->overscroll; } - if (d->max > 0) { - if (destY >= d->max + d->overscroll) { - destY = d->max + d->overscroll; - } - } - else { - destY = 0; + if (destY >= d->max + d->overscroll) { + destY = d->max + d->overscroll; } if (span) { setValueEased_Anim(&d->pos, destY, span); -- cgit v1.2.3 From 9f192d380b98c05a5edd04a99d868875ea342e94 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 7 Dec 2021 14:25:26 +0200 Subject: iOS: Text field tint colors --- src/ios.m | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ios.m b/src/ios.m index 559316a2..b2a5bae4 100644 --- a/src/ios.m +++ b/src/ios.m @@ -748,10 +748,12 @@ void init_SystemTextInput(iSystemTextInput *d, iRect rect, int flags) { } UIColor *textColor = makeUIColor_(uiInputTextFocused_ColorId); UIColor *backgroundColor = makeUIColor_(uiInputBackgroundFocused_ColorId); + UIColor *tintColor = makeUIColor_(uiInputCursor_ColorId); [appState_ setSystemTextInput:d]; if (d->field) { UITextField *field = REF_d_field; [field setTextColor:textColor]; + [field setTintColor:tintColor]; [field setDelegate:appState_]; [field becomeFirstResponder]; } @@ -759,6 +761,7 @@ void init_SystemTextInput(iSystemTextInput *d, iRect rect, int flags) { UITextView *view = REF_d_view; [view setTextColor:textColor]; [view setBackgroundColor:backgroundColor]; + [view setTintColor:tintColor]; [view setEditable:YES]; // [REF_d_view setSelectable:YES]; const float inset = gap_UI / get_Window()->pixelRatio; -- cgit v1.2.3 From 1c15be5ee3672a5e066be718fa35a01111c7ec9a Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 7 Dec 2021 14:26:16 +0200 Subject: DocumentWidget: Footer button background color With the white theme(s), the footer buttons were not prominent enough. --- src/ui/documentwidget.c | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index c4d7c56a..94a58a35 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -1200,7 +1200,6 @@ static void makeFooterButtons_DocumentWidget_(iDocumentWidget *d, const iMenuIte resizeWidthOfChildren_WidgetFlag | arrangeHeight_WidgetFlag | fixedPosition_WidgetFlag | resizeToParentWidth_WidgetFlag, iTrue); - //setBackgroundColor_Widget(d->footerButtons, tmBackground_ColorId); for (size_t i = 0; i < count; ++i) { iLabelWidget *button = addChildFlags_Widget( d->footerButtons, @@ -1210,11 +1209,8 @@ static void makeFooterButtons_DocumentWidget_(iDocumentWidget *d, const iMenuIte setPadding1_Widget(as_Widget(button), gap_UI / 2); checkIcon_LabelWidget(button); setFont_LabelWidget(button, uiContent_FontId); + setBackgroundColor_Widget(as_Widget(button), uiBackgroundSidebar_ColorId); } -// if (deviceType_App() == phone_AppDeviceType) { - /* Footer buttons shouldn't be under the toolbar. */ -// addChild_Widget(d->footerButtons, iClob(makePadding_Widget(height_Widget(d->phoneToolbar)))); -// } addChild_Widget(as_Widget(d), iClob(d->footerButtons)); arrange_Widget(d->footerButtons); arrange_Widget(w); -- cgit v1.2.3 From 3820426634f2fb18a19baee00c7e445f3a2b7d30 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 7 Dec 2021 15:05:46 +0200 Subject: Cleanup --- src/ios.m | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/ios.m b/src/ios.m index b2a5bae4..6d0a8d0f 100644 --- a/src/ios.m +++ b/src/ios.m @@ -759,13 +759,10 @@ void init_SystemTextInput(iSystemTextInput *d, iRect rect, int flags) { } else { UITextView *view = REF_d_view; - [view setTextColor:textColor]; [view setBackgroundColor:backgroundColor]; + [view setTextColor:textColor]; [view setTintColor:tintColor]; [view setEditable:YES]; -// [REF_d_view setSelectable:YES]; - const float inset = gap_UI / get_Window()->pixelRatio; - //[view setTextContainerInset:(UIEdgeInsets){ inset - 1, -inset - 1, 0, -inset - 1 }]; [view setDelegate:appState_]; [view becomeFirstResponder]; } -- cgit v1.2.3 From 71e149f463b18193a0a9e27822390708a2cca83c Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 7 Dec 2021 15:06:26 +0200 Subject: DocumentWidget: Fixing glitches in swipe navigation TODO: There should be a better method for copying the full state of the DocumentWidget to another one. --- src/ui/documentwidget.c | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 94a58a35..1760be9e 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -2614,14 +2614,15 @@ static void swap_DocumentWidget_(iDocumentWidget *d, iGmDocument *doc, iDocumentWidget *swapBuffersWith) { if (doc) { iAssert(isInstance_Object(doc, &Class_GmDocument)); - iGmDocument *copy = ref_Object(doc); - iRelease(d->doc); - d->doc = copy; + replaceDocument_DocumentWidget_(d, doc); d->scrollY = swapBuffersWith->scrollY; - updateVisible_DocumentWidget_(d); + iSwap(iBanner *, d->banner, swapBuffersWith->banner); + setOwner_Banner(d->banner, d); + setOwner_Banner(swapBuffersWith->banner, swapBuffersWith); iSwap(iVisBuf *, d->visBuf, swapBuffersWith->visBuf); iSwap(iVisBufMeta *, d->visBufMeta, swapBuffersWith->visBufMeta); iSwap(iDrawBufs *, d->drawBufs, swapBuffersWith->drawBufs); + updateVisible_DocumentWidget_(d); invalidate_DocumentWidget_(swapBuffersWith); } } @@ -2645,12 +2646,9 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { return iTrue; } iWidget *swipeParent = swipeParent_DocumentWidget_(d); - /* The temporary "swipeIn" will display the previous page until the finger is lifted. */ + /* The temporary "swipein" will display the previous page until the finger is lifted. */ iDocumentWidget *swipeIn = findChild_Widget(swipeParent, "swipein"); if (!swipeIn) { - const iBool sidebarSwipe = iFalse; /* && (isPortraitPhone_App() && - d->flags & openedFromSidebar_DocumentWidgetFlag && - !isVisible_Widget(findWidget_App("sidebar"))); */ swipeIn = new_DocumentWidget(); setId_Widget(as_Widget(swipeIn), "swipein"); setFlags_Widget(as_Widget(swipeIn), @@ -2659,11 +2657,12 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { swipeIn->widget.rect.pos = windowToInner_Widget(swipeParent, localToWindow_Widget(w, w->rect.pos)); swipeIn->widget.rect.size = d->widget.rect.size; swipeIn->widget.offsetRef = parent_Widget(w); - if (!sidebarSwipe) { + /* Use a cached document for the layer underneath. */ { iRecentUrl *recent = new_RecentUrl(); preceding_History(d->mod.history, recent); if (recent->cachedDoc) { iChangeRef(swipeIn->doc, recent->cachedDoc); + updateBanner_DocumentWidget_(swipeIn); updateScrollMax_DocumentWidget_(d); setValue_Anim(&swipeIn->scrollY.pos, pageHeight_DocumentWidget_(d) * recent->normScrollY, 0); @@ -2687,7 +2686,7 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { iWidget *swipeParent = swipeParent_DocumentWidget_(d); iDocumentWidget *target = new_DocumentWidget(); setId_Widget(as_Widget(target), "swipeout"); - /* The target takes the old document and jumps on top. */ + /* The target takes the old document and goes underneath. */ target->widget.rect.pos = windowToInner_Widget(swipeParent, localToWindow_Widget(w, w->rect.pos)); target->widget.rect.size = d->widget.rect.size; setFlags_Widget(as_Widget(target), fixedPosition_WidgetFlag | fixedSize_WidgetFlag, iTrue); -- cgit v1.2.3 From 824da6cbec4a6a28d0fde710777b2bbf771006a0 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 7 Dec 2021 21:00:39 +0200 Subject: Document-specific palettes Manage a color palette separately for each GmDocument, and activate one of them globally whenever a document is being drawn. Palettes are cached in memory. --- src/gmdocument.c | 8 +++- src/gmutil.c | 11 +++++ src/gmutil.h | 2 + src/history.c | 50 ++++++++++++++------- src/history.h | 5 ++- src/ui/documentwidget.c | 116 ++++++++++++++++++++++++++++-------------------- 6 files changed, 125 insertions(+), 67 deletions(-) diff --git a/src/gmdocument.c b/src/gmdocument.c index 884f8c07..e3c06d49 100644 --- a/src/gmdocument.c +++ b/src/gmdocument.c @@ -1710,9 +1710,12 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) { } void makePaletteGlobal_GmDocument(const iGmDocument *d) { - if (d->isPaletteValid) { - memcpy(get_Root()->tmPalette, d->palette, sizeof(d->palette)); + if (!d->isPaletteValid) { + /* Recompute the palette since it's needed now. */ + setThemeSeed_GmDocument((iGmDocument *) d, urlThemeSeed_String(&d->url)); } + iAssert(d->isPaletteValid); + memcpy(get_Root()->tmPalette, d->palette, sizeof(d->palette)); } void invalidatePalette_GmDocument(iGmDocument *d) { @@ -1885,6 +1888,7 @@ static void normalize_GmDocument(iGmDocument *d) { void setUrl_GmDocument(iGmDocument *d, const iString *url) { url = canonicalUrl_String(url); set_String(&d->url, url); + setThemeSeed_GmDocument(d, urlThemeSeed_String(url)); iUrl parts; init_Url(&parts, url); setRange_String(&d->localHost, parts.host); diff --git a/src/gmutil.c b/src/gmutil.c index 643fbea7..79462e41 100644 --- a/src/gmutil.c +++ b/src/gmutil.c @@ -253,6 +253,17 @@ iRangecc urlRoot_String(const iString *d) { return (iRangecc){ constBegin_String(d), rootEnd }; } +const iBlock *urlThemeSeed_String(const iString *url) { + if (equalCase_Rangecc(urlScheme_String(url), "file")) { + return collect_Block(new_Block(0)); + } + const iRangecc user = urlUser_String(url); + if (isEmpty_Range(&user)) { + return collect_Block(newRange_Block(urlHost_String(url))); + } + return collect_Block(newRange_Block(user)); +} + static iBool isAbsolutePath_(iRangecc path) { return isAbsolute_Path(collect_String(urlDecode_String(collect_String(newRange_String(path))))); } diff --git a/src/gmutil.h b/src/gmutil.h index 6b10b444..35a7ee0d 100644 --- a/src/gmutil.h +++ b/src/gmutil.h @@ -117,6 +117,8 @@ iRangecc urlHost_String (const iString *); uint16_t urlPort_String (const iString *); iRangecc urlUser_String (const iString *); iRangecc urlRoot_String (const iString *); +const iBlock * urlThemeSeed_String (const iString *); + const iString * absoluteUrl_String (const iString *, const iString *urlMaybeRelative); iBool isLikelyUrl_String (const iString *); iBool isKnownScheme_Rangecc (iRangecc scheme); /* any URI scheme */ diff --git a/src/history.c b/src/history.c index 7185912f..8c268ec0 100644 --- a/src/history.c +++ b/src/history.c @@ -110,6 +110,14 @@ iHistory *copy_History(const iHistory *d) { return copy; } +void lock_History(iHistory *d) { + lock_Mutex(d->mtx); +} + +void unlock_History(iHistory *d) { + unlock_Mutex(d->mtx); +} + iMemInfo memoryUsage_History(const iHistory *d) { iMemInfo mem = { 0, 0 }; iConstForEach(Array, i, &d->recent) { @@ -297,20 +305,22 @@ void add_History(iHistory *d, const iString *url) { unlock_Mutex(d->mtx); } -iBool preceding_History(iHistory *d, iRecentUrl *recent_out) { +iRecentUrl *precedingLocked_History(iHistory *d) { + /* NOTE: Manual lock and unlock are required when using this; returning an internal pointer. */ iBool ok = iFalse; - lock_Mutex(d->mtx); - if (!isEmpty_Array(&d->recent) && d->recentPos < size_Array(&d->recent) - 1) { - const iRecentUrl *recent = constAt_Array(&d->recent, size_Array(&d->recent) - 1 - - (d->recentPos + 1)); - set_String(&recent_out->url, &recent->url); - recent_out->normScrollY = recent->normScrollY; - iChangeRef(recent_out->cachedDoc, recent->cachedDoc); + //lock_Mutex(d->mtx); + const size_t lastIndex = size_Array(&d->recent) - 1; + if (!isEmpty_Array(&d->recent) && d->recentPos < lastIndex) { + return at_Array(&d->recent, lastIndex - (d->recentPos + 1)); +// set_String(&recent_out->url, &recent->url); +// recent_out->normScrollY = recent->normScrollY; +// iChangeRef(recent_out->cachedDoc, recent->cachedDoc); /* Cached response is not returned, would involve a deep copy. */ - ok = iTrue; +// ok = iTrue; } - unlock_Mutex(d->mtx); - return ok; + //unlock_Mutex(d->mtx); +// return ok; + return NULL; } #if 0 @@ -395,12 +405,20 @@ void setCachedDocument_History(iHistory *d, iGmDocument *doc, iBool openedFromSi lock_Mutex(d->mtx); iRecentUrl *item = mostRecentUrl_History(d); if (item) { - iAssert(equal_String(url_GmDocument(doc), &item->url)); - item->flags.openedFromSidebar = openedFromSidebar; - if (item->cachedDoc != doc) { - iRelease(item->cachedDoc); - item->cachedDoc = ref_Object(doc); + if (equal_String(url_GmDocument(doc), &item->url)) { + item->flags.openedFromSidebar = openedFromSidebar; + if (item->cachedDoc != doc) { + iRelease(item->cachedDoc); + item->cachedDoc = ref_Object(doc); + } + } +#if !defined (NDEBUG) + else { + printf("[History] Not updating cached document; expecting {%s} but document URL is {%s}\n", + cstr_String(&item->url), + cstr_String(url_GmDocument(doc))); } +#endif } unlock_Mutex(d->mtx); } diff --git a/src/history.h b/src/history.h index d3daae80..0c07759b 100644 --- a/src/history.h +++ b/src/history.h @@ -58,6 +58,8 @@ iDeclareTypeConstruction(History) iDeclareTypeSerialization(History) iHistory * copy_History (const iHistory *); +void lock_History (iHistory *); +void unlock_History (iHistory *); void clear_History (iHistory *); void add_History (iHistory *, const iString *url); @@ -66,8 +68,7 @@ void setCachedResponse_History (iHistory *, const iGmResponse *response void setCachedDocument_History (iHistory *, iGmDocument *doc, iBool openedFromSidebar); iBool goBack_History (iHistory *); iBool goForward_History (iHistory *); -iBool preceding_History (iHistory *d, iRecentUrl *recent_out); -//iBool following_History (iHistory *d, iRecentUrl *recent_out); +iRecentUrl *precedingLocked_History (iHistory *); /* requires manual lock/unlock! */ iRecentUrl *recentUrl_History (iHistory *, size_t pos); iRecentUrl *mostRecentUrl_History (iHistory *); iRecentUrl *findUrl_History (iHistory *, const iString *url); diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 1760be9e..1c861b8f 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -330,7 +330,7 @@ void init_DocumentWidget(iDocumentWidget *d) { } init_PersistentDocumentState(&d->mod); d->flags = 0; - d->phoneToolbar = NULL; + d->phoneToolbar = findWidget_App("toolbar"); d->footerButtons = NULL; iZap(d->certExpiry); d->certFingerprint = new_Block(0); @@ -961,7 +961,8 @@ static void updateVisible_DocumentWidget_(iDocumentWidget *d) { animateMedia_DocumentWidget_(d); /* Remember scroll positions of recently visited pages. */ { iRecentUrl *recent = mostRecentUrl_History(d->mod.history); - if (recent && docSize && d->state == ready_RequestState) { + if (recent && docSize && d->state == ready_RequestState && + equal_String(&recent->url, d->mod.url)) { recent->normScrollY = normScrollPos_DocumentWidget_(d); } } @@ -1162,27 +1163,14 @@ static void replaceDocument_DocumentWidget_(iDocumentWidget *d, iGmDocument *new } static void updateBanner_DocumentWidget_(iDocumentWidget *d) { - /* TODO: Set width. */ setSite_Banner(d->banner, siteText_DocumentWidget_(d), siteIcon_GmDocument(d->doc)); } static void updateTheme_DocumentWidget_(iDocumentWidget *d) { if (document_App() != d || category_GmStatusCode(d->sourceStatus) == categoryInput_GmStatusCode) { return; - } - if (equalCase_Rangecc(urlScheme_String(d->mod.url), "file")) { - iBlock empty; - init_Block(&empty, 0); - setThemeSeed_GmDocument(d->doc, &empty); - deinit_Block(&empty); - } - else if (isEmpty_String(d->titleUser)) { - setThemeSeed_GmDocument(d->doc, - collect_Block(newRange_Block(urlHost_String(d->mod.url)))); - } - else { - setThemeSeed_GmDocument(d->doc, &d->titleUser->chars); - } + } +// setThemeSeed_GmDocument(d->doc, urlThemeSeed_String(d->mod.url)); /* theme palette and icon */ d->drawBufs->flags |= updateTimestampBuf_DrawBufsFlag; updateBanner_DocumentWidget_(d); } @@ -2616,6 +2604,7 @@ static void swap_DocumentWidget_(iDocumentWidget *d, iGmDocument *doc, iAssert(isInstance_Object(doc, &Class_GmDocument)); replaceDocument_DocumentWidget_(d, doc); d->scrollY = swapBuffersWith->scrollY; + d->scrollY.widget = as_Widget(d); iSwap(iBanner *, d->banner, swapBuffersWith->banner); setOwner_Banner(d->banner, d); setOwner_Banner(swapBuffersWith->banner, swapBuffersWith); @@ -2631,6 +2620,14 @@ static iWidget *swipeParent_DocumentWidget_(iDocumentWidget *d) { return findChild_Widget(as_Widget(d)->root->widget, "doctabs"); } +static void setUrl_DocumentWidget_(iDocumentWidget *d, const iString *url) { + url = canonicalUrl_String(url); + if (!equal_String(d->mod.url, url)) { + d->flags |= urlChanged_DocumentWidgetFlag; + set_String(d->mod.url, url); + } +} + static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { iWidget *w = as_Widget(d); /* Swipe animations are rather complex and utilize both cached GmDocument content @@ -2658,18 +2655,31 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { swipeIn->widget.rect.size = d->widget.rect.size; swipeIn->widget.offsetRef = parent_Widget(w); /* Use a cached document for the layer underneath. */ { - iRecentUrl *recent = new_RecentUrl(); - preceding_History(d->mod.history, recent); - if (recent->cachedDoc) { +// iRecentUrl *recent = new_RecentUrl(); + lock_History(d->mod.history); + iRecentUrl *recent = precedingLocked_History(d->mod.history); + if (recent && recent->cachedResponse) { + /* iChangeRef(swipeIn->doc, recent->cachedDoc); updateBanner_DocumentWidget_(swipeIn); updateScrollMax_DocumentWidget_(d); setValue_Anim(&swipeIn->scrollY.pos, pageHeight_DocumentWidget_(d) * recent->normScrollY, 0); updateVisible_DocumentWidget_(swipeIn); - swipeIn->drawBufs->flags |= updateTimestampBuf_DrawBufsFlag | updateSideBuf_DrawBufsFlag; + swipeIn->drawBufs->flags |= updateTimestampBuf_DrawBufsFlag | updateSideBuf_DrawBufsFlag;*/ + setUrl_DocumentWidget_(swipeIn, &recent->url); + updateFromCachedResponse_DocumentWidget_(swipeIn, + recent->normScrollY, + recent->cachedResponse, + recent->cachedDoc); } - delete_RecentUrl(recent); + else { + setUrlAndSource_DocumentWidget(swipeIn, &recent->url, + collectNewCStr_String("text/gemini"), + collect_Block(new_Block(0))); + } +// delete_RecentUrl(recent); + unlock_History(d->mod.history); } addChildPos_Widget(swipeParent, iClob(swipeIn), front_WidgetAddPos); } @@ -2677,8 +2687,7 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { if (side == 2) { /* right edge */ if (offset < -get_Window()->pixelRatio * 10) { int animSpan = 10; - if (!atLatest_History(d->mod.history) && - ~flags_Widget(w) & dragged_WidgetFlag) { + if (!atLatest_History(d->mod.history) && ~flags_Widget(w) & dragged_WidgetFlag) { animSpan = 0; postCommand_Widget(d, "navigate.forward"); setFlags_Widget(w, dragged_WidgetFlag, iTrue); @@ -2695,6 +2704,13 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { setFlags_Widget(as_Widget(target), refChildrenOffset_WidgetFlag, iTrue); as_Widget(target)->offsetRef = parent_Widget(w); destroy_Widget(as_Widget(target)); /* will be actually deleted after animation finishes */ + /* The `d` document will now navigate forward and be replaced with a cached + copy. However, if a cached response isn't available, we'll need to show a + blank page. */ + setUrlAndSource_DocumentWidget(d, + collectNewCStr_String("about:blank"), + collectNewCStr_String("text/gemini"), + collect_Block(new_Block(0))); } if (flags_Widget(w) & dragged_WidgetFlag) { setVisualOffset_Widget(w, width_Widget(w) + @@ -2718,10 +2734,19 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { } if (equal_Command(cmd, "edgeswipe.ended") && argLabel_Command(cmd, "side") == 1) { iWidget *swipeParent = swipeParent_DocumentWidget_(d); - iWidget *swipeIn = findChild_Widget(swipeParent, "swipein"); + iDocumentWidget *swipeIn = findChild_Widget(swipeParent, "swipein"); +// iDocumentWidget *swipeInDoc = (iDocumentWidget *) swipeIn; + /* "swipe.back" will soon follow. The `d` document will do the actual back navigation, + switching immediately to a cached page. However, if one is not available, we'll need + to show a blank page for a while. */ if (swipeIn) { - swipeIn->offsetRef = NULL; - destroy_Widget(swipeIn); + setUrlAndSource_DocumentWidget(d, + swipeIn->mod.url, + collectNewCStr_String("text/gemini"), + collect_Block(new_Block(0))); + //swap_DocumentWidget_(d, swipeIn->doc, swipeIn); + as_Widget(swipeIn)->offsetRef = NULL; + destroy_Widget(as_Widget(swipeIn)); } } if (equal_Command(cmd, "swipe.back")) { @@ -2824,14 +2849,16 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) else if (equal_Command(cmd, "window.mouse.exited")) { return iFalse; } - else if (equal_Command(cmd, "theme.changed") && document_App() == d) { -// invalidateTheme_History(d->mod.history); /* cached colors */ - updateTheme_DocumentWidget_(d); - updateVisible_DocumentWidget_(d); - updateTrust_DocumentWidget_(d, NULL); - d->drawBufs->flags |= updateSideBuf_DrawBufsFlag; - invalidate_DocumentWidget_(d); - refresh_Widget(w); + else if (equal_Command(cmd, "theme.changed")) { + invalidateTheme_History(d->mod.history); /* forget cached color palettes */ + if (document_App() == d) { + updateTheme_DocumentWidget_(d); + updateVisible_DocumentWidget_(d); + updateTrust_DocumentWidget_(d, NULL); + d->drawBufs->flags |= updateSideBuf_DrawBufsFlag; + invalidate_DocumentWidget_(d); + refresh_Widget(w); + } } else if (equal_Command(cmd, "document.layout.changed") && document_Root(get_Root()) == d) { if (argLabel_Command(cmd, "redo")) { @@ -5047,6 +5074,7 @@ static void prerender_DocumentWidget_(iAny *context) { }; // printf("%u prerendering\n", SDL_GetTicks()); if (d->visBuf->buffers[0].texture) { + makePaletteGlobal_GmDocument(d->doc); if (render_DocumentWidget_(d, &ctx, iTrue /* just fill up progressively */)) { /* Something was drawn, should check if there is still more to do. */ addTicker_App(prerender_DocumentWidget_, context); @@ -5062,10 +5090,11 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) { if (width_Rect(bounds) <= 0) { return; } - /* TODO: Come up with a better palette caching system. - It should be able to recompute cached colors in `History` when the theme has changed. - Cache the theme seed in `GmDocument`? */ -// makePaletteGlobal_GmDocument(d->doc); + /* Each document has its own palette, but the drawing routines rely on a global one. + As we're now drawing a document, ensure that the right palette is in effect. + Document theme colors can be used elsewhere, too, but first a document's palette + must be made global. */ + makePaletteGlobal_GmDocument(d->doc); if (d->drawBufs->flags & updateTimestampBuf_DrawBufsFlag) { updateTimestampBuf_DocumentWidget_(d); } @@ -5319,14 +5348,6 @@ void deserializeState_DocumentWidget(iDocumentWidget *d, iStream *ins) { updateFromHistory_DocumentWidget_(d); } -static void setUrl_DocumentWidget_(iDocumentWidget *d, const iString *url) { - url = canonicalUrl_String(url); - if (!equal_String(d->mod.url, url)) { - d->flags |= urlChanged_DocumentWidgetFlag; - set_String(d->mod.url, url); - } -} - void setUrlFlags_DocumentWidget(iDocumentWidget *d, const iString *url, int setUrlFlags) { iChangeFlags(d->flags, openedFromSidebar_DocumentWidgetFlag, (setUrlFlags & openedFromSidebar_DocumentWidgetSetUrlFlag) != 0); @@ -5352,6 +5373,7 @@ void setUrlAndSource_DocumentWidget(iDocumentWidget *d, const iString *url, cons set_String(&resp->meta, mime); set_Block(&resp->body, source); updateFromCachedResponse_DocumentWidget_(d, 0, resp, NULL); + updateBanner_DocumentWidget_(d); delete_GmResponse(resp); } -- cgit v1.2.3 From 63ad3813f76d53a6fa98f45868e8b096331a2d18 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 7 Dec 2021 21:20:25 +0200 Subject: DocumentWidget: Fixed swipe navigation TODO: If the swipe is aborted, the state ends up wrong. --- src/ui/documentwidget.c | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 1c861b8f..f4c934f6 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -2735,35 +2735,41 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { if (equal_Command(cmd, "edgeswipe.ended") && argLabel_Command(cmd, "side") == 1) { iWidget *swipeParent = swipeParent_DocumentWidget_(d); iDocumentWidget *swipeIn = findChild_Widget(swipeParent, "swipein"); -// iDocumentWidget *swipeInDoc = (iDocumentWidget *) swipeIn; /* "swipe.back" will soon follow. The `d` document will do the actual back navigation, switching immediately to a cached page. However, if one is not available, we'll need to show a blank page for a while. */ if (swipeIn) { + iWidget *swipeParent = swipeParent_DocumentWidget_(d); + iDocumentWidget *target = new_DocumentWidget(); + addChildPos_Widget(swipeParent, iClob(target), back_WidgetAddPos); + setId_Widget(as_Widget(target), "swipeout"); + swap_DocumentWidget_(target, d->doc, d); setUrlAndSource_DocumentWidget(d, swipeIn->mod.url, collectNewCStr_String("text/gemini"), collect_Block(new_Block(0))); - //swap_DocumentWidget_(d, swipeIn->doc, swipeIn); as_Widget(swipeIn)->offsetRef = NULL; destroy_Widget(as_Widget(swipeIn)); } } if (equal_Command(cmd, "swipe.back")) { + iWidget *swipeParent = swipeParent_DocumentWidget_(d); + iDocumentWidget *target = findChild_Widget(swipeParent, "swipeout"); if (atOldest_History(d->mod.history)) { setVisualOffset_Widget(w, 0, 100, 0); + if (target) { + destroy_Widget(as_Widget(target)); /* didn't need it after all */ + } return iTrue; } - iWidget *swipeParent = swipeParent_DocumentWidget_(d); - iDocumentWidget *target = new_DocumentWidget(); - setId_Widget(as_Widget(target), "swipeout"); +// iDocumentWidget *target = new_DocumentWidget(); +// setId_Widget(as_Widget(target), "swipeout"); /* The target takes the old document and jumps on top. */ target->widget.rect.pos = windowToInner_Widget(swipeParent, innerToWindow_Widget(w, zero_I2())); /* Note: `innerToWindow_Widget` does not apply visual offset. */ target->widget.rect.size = w->rect.size; setFlags_Widget(as_Widget(target), fixedPosition_WidgetFlag | fixedSize_WidgetFlag, iTrue); - swap_DocumentWidget_(target, d->doc, d); - addChildPos_Widget(swipeParent, iClob(target), back_WidgetAddPos); +// swap_DocumentWidget_(target, d->doc, d); setFlags_Widget(as_Widget(d), refChildrenOffset_WidgetFlag, iTrue); as_Widget(d)->offsetRef = swipeParent; setVisualOffset_Widget(as_Widget(target), value_Anim(&w->visualOffset), 0, 0); -- cgit v1.2.3 From c1d3cf1f0acb8ea8e45ccd4daebebba63df89fca Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Wed, 8 Dec 2021 07:09:44 +0200 Subject: Mobile: Outline mode for disabled toolbar arrows --- src/ui/root.c | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/ui/root.c b/src/ui/root.c index d3eb221e..2a7bf4ff 100644 --- a/src/ui/root.c +++ b/src/ui/root.c @@ -581,14 +581,20 @@ static void updateNavBarIdentity_(iWidget *navBar) { static void updateNavDirButtons_(iWidget *navBar) { const iHistory *history = history_DocumentWidget(document_App()); - setFlags_Widget(findChild_Widget(navBar, "navbar.back"), disabled_WidgetFlag, - atOldest_History(history)); - setFlags_Widget(findChild_Widget(navBar, "navbar.forward"), disabled_WidgetFlag, - atLatest_History(history)); - setFlags_Widget(findWidget_App("toolbar.back"), disabled_WidgetFlag, - atOldest_History(history)); - setFlags_Widget(findWidget_App("toolbar.forward"), disabled_WidgetFlag, - atLatest_History(history)); + const iBool atOldest = atOldest_History(history); + const iBool atNewest = atNewest_History(history); + setFlags_Widget(findChild_Widget(navBar, "navbar.back"), disabled_WidgetFlag, atOldest); + setFlags_Widget(findChild_Widget(navBar, "navbar.forward"), disabled_WidgetFlag, atNewest); + iWidget *toolBar = findWidget_App("toolbar"); + if (toolBar) { + iLabelWidget *back = findChild_Widget(toolBar, "toolbar.back"); + iLabelWidget *fwd = findChild_Widget(toolBar, "toolbar.forward"); + setFlags_Widget(as_Widget(back), disabled_WidgetFlag, atOldest); + setOutline_LabelWidget(back, atOldest); + setFlags_Widget(as_Widget(fwd), disabled_WidgetFlag, atNewest); + setOutline_LabelWidget(fwd, atNewest); + refresh_Widget(toolBar); + } } static const char *loadAnimationCStr_(void) { -- cgit v1.2.3 From ddd7f1a32ca00533ef33583efdf457a572c1ea03 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Wed, 8 Dec 2021 07:11:21 +0200 Subject: DocumentWidget: Trying to fix swipe animations Document swipe animations are now almost fully working, except for an aborted forward navigation (swipe from right edge). --- src/history.c | 2 +- src/history.h | 2 +- src/ui/documentwidget.c | 113 +++++++++++++++++++++++++++++++----------------- 3 files changed, 75 insertions(+), 42 deletions(-) diff --git a/src/history.c b/src/history.c index 8c268ec0..d1a731fb 100644 --- a/src/history.c +++ b/src/history.c @@ -370,7 +370,7 @@ iBool goForward_History(iHistory *d) { return iFalse; } -iBool atLatest_History(const iHistory *d) { +iBool atNewest_History(const iHistory *d) { iBool isLatest; iGuardMutex(d->mtx, isLatest = (d->recentPos == 0)); return isLatest; diff --git a/src/history.h b/src/history.h index 0c07759b..3bb3808e 100644 --- a/src/history.h +++ b/src/history.h @@ -79,7 +79,7 @@ size_t pruneLeastImportantMemory_History (iHistory *); void invalidateTheme_History (iHistory *); /* theme has changed, cached contents need updating */ void invalidateCachedLayout_History (iHistory *); -iBool atLatest_History (const iHistory *); +iBool atNewest_History (const iHistory *); iBool atOldest_History (const iHistory *); const iStringArray * searchContents_History (const iHistory *, const iRegExp *pattern); /* chronologically ascending */ diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index f4c934f6..2b46c7b5 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -2628,11 +2628,36 @@ static void setUrl_DocumentWidget_(iDocumentWidget *d, const iString *url) { } } +static void setupSwipeOverlay_DocumentWidget_(iDocumentWidget *d, iWidget *overlay) { + iWidget *w = as_Widget(d); + iWidget *swipeParent = swipeParent_DocumentWidget_(d); + /* The target takes the old document and jumps on top. */ + overlay->rect.pos = windowToInner_Widget(swipeParent, innerToWindow_Widget(w, zero_I2())); + /* Note: `innerToWindow_Widget` does not apply visual offset. */ + overlay->rect.size = w->rect.size; + setFlags_Widget(overlay, fixedPosition_WidgetFlag | fixedSize_WidgetFlag, iTrue); +// swap_DocumentWidget_(target, d->doc, d); + setFlags_Widget(as_Widget(d), refChildrenOffset_WidgetFlag, iTrue); + as_Widget(d)->offsetRef = swipeParent; + /* `overlay` animates off the screen to the right. */ + setVisualOffset_Widget(overlay, value_Anim(&w->visualOffset), 0, 0); + setVisualOffset_Widget(overlay, width_Widget(overlay), 150, 0); + setVisualOffset_Widget(w, 0, 0, 0); +} + static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { + /* TODO: Cleanup + If DocumentWidget is refactored to split the document presentation from state + and request management (a new DocumentView class), plain views could be used for this + animation without having to mess with the complete state of the DocumentWidget. That + seems like a less error-prone approach -- the current implementation will likely break + down (again) if anything is changed in the document internals. + */ iWidget *w = as_Widget(d); - /* Swipe animations are rather complex and utilize both cached GmDocument content - and temporary DocumentWidgets. Depending on the swipe direction, this DocumentWidget - may wait until the finger is released to actually perform the navigation action. */ + /* The swipe animation is implemented in a rather complex way. It utilizes both cached + GmDocument content and temporary underlay/overlay DocumentWidgets. Depending on the + swipe direction, the DocumentWidget `d` may wait until the finger is released to actually + perform the navigation action. */ if (equal_Command(cmd, "edgeswipe.moved")) { //printf("[%p] responds to edgeswipe.moved\n", d); as_Widget(d)->offsetRef = NULL; @@ -2651,34 +2676,27 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { setFlags_Widget(as_Widget(swipeIn), disabled_WidgetFlag | refChildrenOffset_WidgetFlag | fixedPosition_WidgetFlag | fixedSize_WidgetFlag, iTrue); + setFlags_Widget(findChild_Widget(as_Widget(swipeIn), "scroll"), hidden_WidgetFlag, iTrue); swipeIn->widget.rect.pos = windowToInner_Widget(swipeParent, localToWindow_Widget(w, w->rect.pos)); swipeIn->widget.rect.size = d->widget.rect.size; swipeIn->widget.offsetRef = parent_Widget(w); /* Use a cached document for the layer underneath. */ { -// iRecentUrl *recent = new_RecentUrl(); lock_History(d->mod.history); iRecentUrl *recent = precedingLocked_History(d->mod.history); if (recent && recent->cachedResponse) { - /* - iChangeRef(swipeIn->doc, recent->cachedDoc); - updateBanner_DocumentWidget_(swipeIn); - updateScrollMax_DocumentWidget_(d); - setValue_Anim(&swipeIn->scrollY.pos, - pageHeight_DocumentWidget_(d) * recent->normScrollY, 0); - updateVisible_DocumentWidget_(swipeIn); - swipeIn->drawBufs->flags |= updateTimestampBuf_DrawBufsFlag | updateSideBuf_DrawBufsFlag;*/ setUrl_DocumentWidget_(swipeIn, &recent->url); updateFromCachedResponse_DocumentWidget_(swipeIn, recent->normScrollY, recent->cachedResponse, recent->cachedDoc); + parseUser_DocumentWidget_(swipeIn); + updateBanner_DocumentWidget_(swipeIn); } else { setUrlAndSource_DocumentWidget(swipeIn, &recent->url, collectNewCStr_String("text/gemini"), collect_Block(new_Block(0))); } -// delete_RecentUrl(recent); unlock_History(d->mod.history); } addChildPos_Widget(swipeParent, iClob(swipeIn), front_WidgetAddPos); @@ -2687,7 +2705,8 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { if (side == 2) { /* right edge */ if (offset < -get_Window()->pixelRatio * 10) { int animSpan = 10; - if (!atLatest_History(d->mod.history) && ~flags_Widget(w) & dragged_WidgetFlag) { + if (!atNewest_History(d->mod.history) && ~flags_Widget(w) & dragged_WidgetFlag) { + /* Setup the drag. `d` will be moving with the finger. */ animSpan = 0; postCommand_Widget(d, "navigate.forward"); setFlags_Widget(w, dragged_WidgetFlag, iTrue); @@ -2695,7 +2714,7 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { iWidget *swipeParent = swipeParent_DocumentWidget_(d); iDocumentWidget *target = new_DocumentWidget(); setId_Widget(as_Widget(target), "swipeout"); - /* The target takes the old document and goes underneath. */ + /* "swipeout" takes `d`'s document and goes underneath. */ target->widget.rect.pos = windowToInner_Widget(swipeParent, localToWindow_Widget(w, w->rect.pos)); target->widget.rect.size = d->widget.rect.size; setFlags_Widget(as_Widget(target), fixedPosition_WidgetFlag | fixedSize_WidgetFlag, iTrue); @@ -2703,7 +2722,8 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { addChildPos_Widget(swipeParent, iClob(target), front_WidgetAddPos); setFlags_Widget(as_Widget(target), refChildrenOffset_WidgetFlag, iTrue); as_Widget(target)->offsetRef = parent_Widget(w); - destroy_Widget(as_Widget(target)); /* will be actually deleted after animation finishes */ + /* Mark it for deletion after animation finishes. */ + destroy_Widget(as_Widget(target)); /* The `d` document will now navigate forward and be replaced with a cached copy. However, if a cached response isn't available, we'll need to show a blank page. */ @@ -2726,10 +2746,26 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { } if (equal_Command(cmd, "edgeswipe.ended") && argLabel_Command(cmd, "side") == 2) { if (argLabel_Command(cmd, "abort") && flags_Widget(w) & dragged_WidgetFlag) { + setFlags_Widget(w, dragged_WidgetFlag, iFalse); postCommand_Widget(d, "navigate.back"); + /* We must now undo the swap that was done when the drag started. */ + /* TODO: Currently not animated! What exactly is the appropriate thing to do here? */ + iWidget *swipeParent = swipeParent_DocumentWidget_(d); + iDocumentWidget *swipeOut = findChild_Widget(swipeParent, "swipeout"); + swap_DocumentWidget_(d, swipeOut->doc, swipeOut); +// const int visOff = visualOffsetByReference_Widget(w); + w->offsetRef = NULL; +// setVisualOffset_Widget(w, visOff, 0, 0); +// setVisualOffset_Widget(w, 0, 150, 0); + setVisualOffset_Widget(w, 0, 0, 0); + /* Make it an overlay instead. */ +// removeChild_Widget(swipeParent, swipeOut); +// addChildPos_Widget(swipeParent, iClob(swipeOut), back_WidgetAddPos); +// setupSwipeOverlay_DocumentWidget_(d, as_Widget(swipeOut)); + return iTrue; } setFlags_Widget(w, dragged_WidgetFlag, iFalse); - setVisualOffset_Widget(w, 0, 100, 0); + setVisualOffset_Widget(w, 0, 150, 0); return iTrue; } if (equal_Command(cmd, "edgeswipe.ended") && argLabel_Command(cmd, "side") == 1) { @@ -2739,16 +2775,20 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { switching immediately to a cached page. However, if one is not available, we'll need to show a blank page for a while. */ if (swipeIn) { - iWidget *swipeParent = swipeParent_DocumentWidget_(d); - iDocumentWidget *target = new_DocumentWidget(); - addChildPos_Widget(swipeParent, iClob(target), back_WidgetAddPos); - setId_Widget(as_Widget(target), "swipeout"); - swap_DocumentWidget_(target, d->doc, d); - setUrlAndSource_DocumentWidget(d, - swipeIn->mod.url, - collectNewCStr_String("text/gemini"), - collect_Block(new_Block(0))); - as_Widget(swipeIn)->offsetRef = NULL; + if (!argLabel_Command(cmd, "abort")) { + iWidget *swipeParent = swipeParent_DocumentWidget_(d); + /* What was being shown in the `d` document is now being swapped to + the outgoing page animation. */ + iDocumentWidget *target = new_DocumentWidget(); + addChildPos_Widget(swipeParent, iClob(target), back_WidgetAddPos); + setId_Widget(as_Widget(target), "swipeout"); + swap_DocumentWidget_(target, d->doc, d); + setUrlAndSource_DocumentWidget(d, + swipeIn->mod.url, + collectNewCStr_String("text/gemini"), + collect_Block(new_Block(0))); + as_Widget(swipeIn)->offsetRef = NULL; + } destroy_Widget(as_Widget(swipeIn)); } } @@ -2762,20 +2802,8 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { } return iTrue; } -// iDocumentWidget *target = new_DocumentWidget(); -// setId_Widget(as_Widget(target), "swipeout"); - /* The target takes the old document and jumps on top. */ - target->widget.rect.pos = windowToInner_Widget(swipeParent, innerToWindow_Widget(w, zero_I2())); - /* Note: `innerToWindow_Widget` does not apply visual offset. */ - target->widget.rect.size = w->rect.size; - setFlags_Widget(as_Widget(target), fixedPosition_WidgetFlag | fixedSize_WidgetFlag, iTrue); -// swap_DocumentWidget_(target, d->doc, d); - setFlags_Widget(as_Widget(d), refChildrenOffset_WidgetFlag, iTrue); - as_Widget(d)->offsetRef = swipeParent; - setVisualOffset_Widget(as_Widget(target), value_Anim(&w->visualOffset), 0, 0); - setVisualOffset_Widget(as_Widget(target), width_Widget(target), 150, 0); + setupSwipeOverlay_DocumentWidget_(d, as_Widget(target)); destroy_Widget(as_Widget(target)); /* will be actually deleted after animation finishes */ - setVisualOffset_Widget(w, 0, 0, 0); postCommand_Widget(d, "navigate.back"); return iTrue; } @@ -5292,6 +5320,11 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) { } } // drawRect_Paint(&ctx.paint, docBounds, red_ColorId); + if (deviceType_App() == phone_AppDeviceType) { + /* The phone toolbar uses the palette of the active tab, but there may be other + documents drawn before the toolbar, causing the colors to be incorrect. */ + makePaletteGlobal_GmDocument(document_App()->doc); + } } /*----------------------------------------------------------------------------------------------*/ -- cgit v1.2.3 From 776d917183f179545dbed6888788a8e3c0e5a7a7 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Wed, 8 Dec 2021 15:54:05 +0200 Subject: iOS: Fixed text field position was shifting The field was not tall enough, so it was scrolling slightly. Some of the changes in InputWidget may have been unnecessary. --- src/ios.m | 2 +- src/ui/inputwidget.c | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/ios.m b/src/ios.m index 6d0a8d0f..6a30c8a8 100644 --- a/src/ios.m +++ b/src/ios.m @@ -695,7 +695,7 @@ static CGRect convertToCGRect_(const iRect *rect, iBool expanded) { frame.origin.x -= inset + 1; frame.origin.y -= inset + 1; frame.size.width += 2 * inset + 2; - frame.size.height += inset + 1; + frame.size.height += inset + 1 + inset; } return frame; } diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index 1935826b..3bee3d21 100644 --- a/src/ui/inputwidget.c +++ b/src/ui/inputwidget.c @@ -722,18 +722,19 @@ static void updateAllLinesAndResizeHeight_InputWidget_(iInputWidget *d) { #endif static int contentHeight_InputWidget_(const iInputWidget *d) { + const int lineHeight = lineHeight_Text(d->font); #if LAGRANGE_USE_SYSTEM_TEXT_INPUT - const int minHeight = d->minWrapLines * lineHeight_Text(d->font); - const int maxHeight = d->maxWrapLines * lineHeight_Text(d->font); + const int minHeight = d->minWrapLines * lineHeight; + const int maxHeight = d->maxWrapLines * lineHeight; if (d->sysCtrl) { - const int preferred = preferredHeight_SystemTextInput(d->sysCtrl); - return iClamp(preferred, minHeight, maxHeight); + const int preferred = (preferredHeight_SystemTextInput(d->sysCtrl) + gap_UI) / lineHeight; + return iClamp(preferred * lineHeight, minHeight, maxHeight); } if (d->buffered && ~d->inFlags & needUpdateBuffer_InputWidgetFlag) { return iClamp(d->buffered->size.y, minHeight, maxHeight); } #endif - return size_Range(&d->visWrapLines) * lineHeight_Text(d->font); + return (int) size_Range(&d->visWrapLines) * lineHeight; } static void updateTextInputRect_InputWidget_(const iInputWidget *d) { @@ -1178,6 +1179,7 @@ void begin_InputWidget(iInputWidget *d) { setText_SystemTextInput(d->sysCtrl, &d->oldText); setTextChangedFunc_SystemTextInput(d->sysCtrl, systemInputChanged_InputWidget_, d); iConnect(Root, w->root, visualOffsetsChanged, d, updateAfterVisualOffsetChange_InputWidget_); + updateTextInputRect_InputWidget_(d); updateMetrics_InputWidget_(d); #else mergeLines_(&d->lines, &d->oldText); -- cgit v1.2.3 From fde64385b7cf0a51e471112ce80e6b2d81540e08 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Wed, 8 Dec 2021 17:11:56 +0200 Subject: Mobile: SmoothScroll pull flags; menu and value input sizing --- src/ui/documentwidget.c | 3 +++ src/ui/util.c | 52 +++++++++++++++++++++++++------------------------ src/ui/util.h | 6 ++++++ 3 files changed, 36 insertions(+), 25 deletions(-) diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 2b46c7b5..87c86192 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -348,6 +348,9 @@ void init_DocumentWidget(iDocumentWidget *d) { d->ordinalBase = 0; d->initNormScrollY = 0; init_SmoothScroll(&d->scrollY, w, scrollBegan_DocumentWidget_); + if (deviceType_App() != desktop_AppDeviceType) { + d->scrollY.flags |= pullDownAction_SmoothScrollFlag; /* pull to refresh */ + } d->animWideRunId = 0; init_Anim(&d->animWideRunOffset, 0); d->selectMark = iNullRange; diff --git a/src/ui/util.c b/src/ui/util.c index 097eb548..6e139856 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -480,6 +480,7 @@ void init_SmoothScroll(iSmoothScroll *d, iWidget *owner, iSmoothScrollNotifyFunc d->widget = owner; d->notify = notify; d->pullActionTriggered = 0; + d->flags = 0; } void reset_SmoothScroll(iSmoothScroll *d) { @@ -550,7 +551,7 @@ void moveSpan_SmoothScroll(iSmoothScroll *d, int offset, uint32_t span) { } #endif int destY = targetValue_Anim(&d->pos) + offset; - if (destY < -pullActionThreshold_SmoothScroll_(d)) { + if (d->flags & pullDownAction_SmoothScrollFlag && destY < -pullActionThreshold_SmoothScroll_(d)) { if (d->pullActionTriggered == 0) { d->pullActionTriggered = iTrue; #if defined (iPlatformAppleMobile) @@ -764,12 +765,13 @@ void makeMenuItems_Widget(iWidget *menu, const iMenuItem *items, size_t n) { drawKey_WidgetFlag | itemFlags); setWrap_LabelWidget(label, isInfo); haveIcons |= checkIcon_LabelWidget(label); - updateSize_LabelWidget(label); /* drawKey was set */ setFlags_Widget(as_Widget(label), disabled_WidgetFlag, isDisabled); if (isInfo) { - setFlags_Widget(as_Widget(label), fixedHeight_WidgetFlag, iTrue); /* wrap changes height */ + setFlags_Widget(as_Widget(label), resizeToParentWidth_WidgetFlag | + fixedHeight_WidgetFlag, iTrue); /* wrap changes height */ setTextColor_LabelWidget(label, uiTextAction_ColorId); } + updateSize_LabelWidget(label); /* drawKey was set */ } } if (deviceType_App() == phone_AppDeviceType) { @@ -1051,7 +1053,8 @@ void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, int menuOpenFlags) { #else const iRect rootRect = rect_Root(d->root); const iInt2 rootSize = rootRect.size; - const iBool isPortraitPhone = (deviceType_App() == phone_AppDeviceType && isPortrait_App()); + const iBool isPhone = (deviceType_App() == phone_AppDeviceType); + const iBool isPortraitPhone = (isPhone && isPortrait_App()); const iBool isSlidePanel = (flags_Widget(d) & horizontalOffset_WidgetFlag) != 0; if (postCommands) { postCommand_App("cancel"); /* dismiss any other menus */ @@ -1139,16 +1142,14 @@ void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, int menuOpenFlags) { raise_Widget(d); if (deviceType_App() != desktop_AppDeviceType) { setFlags_Widget(d, arrangeWidth_WidgetFlag | resizeChildrenToWidestChild_WidgetFlag, - !isPortraitPhone); + !isPhone); setFlags_Widget(d, resizeWidthOfChildren_WidgetFlag | drawBackgroundToBottom_WidgetFlag | drawBackgroundToVerticalSafeArea_WidgetFlag, - isPortraitPhone); - if (isPortraitPhone) { - if (!isSlidePanel) { - setFlags_Widget(d, borderTop_WidgetFlag, iTrue); - } - d->rect.size.x = rootSize.x; + isPhone); + if (isPhone) { + setFlags_Widget(d, borderTop_WidgetFlag, !isSlidePanel && isPortrait_App()); /* menu is otherwise frameless */ + setFixedSize_Widget(d, init_I2(iMin(rootSize.x, rootSize.y), -1)); } else { d->rect.size.x = 0; @@ -1156,12 +1157,18 @@ void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, int menuOpenFlags) { } updateMenuItemFonts_Widget_(d); arrange_Widget(d); - if (isPortraitPhone) { + if (!isSlidePanel) { + /* LAYOUT BUG: Height of wrapped menu items is incorrect with a single arrange! */ + arrange_Widget(d); + } + if (deviceType_App() == phone_AppDeviceType) { if (isSlidePanel) { d->rect.pos = zero_I2(); } else { - d->rect.pos = init_I2(0, rootSize.y); + d->rect.pos = windowToLocal_Widget(d, + init_I2(rootSize.x / 2 - d->rect.size.x / 2, + rootSize.y)); } } else if (menuOpenFlags & center_MenuOpenFlags) { @@ -1638,7 +1645,7 @@ static void acceptValueInput_(iWidget *dlg) { } } -static void updateValueInputWidth_(iWidget *dlg) { +static void updateValueInputSizing_(iWidget *dlg) { const iRect safeRoot = safeRect_Root(dlg->root); const iInt2 rootSize = safeRoot.size; iWidget * title = findChild_Widget(dlg, "valueinput.title"); @@ -1651,20 +1658,15 @@ static void updateValueInputWidth_(iWidget *dlg) { iMin(rootSize.x, iMaxi(iMaxi(100 * gap_UI, title->rect.size.x), prompt->rect.size.x)); } /* Adjust the maximum number of visible lines. */ - int footer = 6 * gap_UI + get_MainWindow()->keyboardHeight; -#if defined (iPlatformAppleMobile) - if (deviceType_App() == phone_AppDeviceType) { - footer -= 12 * gap_UI; /* A little suspect, this... Check the math? */ - } -#endif + int footer = 6 * gap_UI; iWidget *buttons = findChild_Widget(dlg, "dialogbuttons"); - if (buttons) { + if (buttons && deviceType_App() == desktop_AppDeviceType) { footer += height_Widget(buttons); } iInputWidget *input = findChild_Widget(dlg, "input"); setLineLimits_InputWidget(input, 1, - (bottom_Rect(safeRect_Root(dlg->root)) - footer - + (bottom_Rect(visibleRect_Root(dlg->root)) - footer - top_Rect(boundsWithoutVisualOffset_Widget(as_Widget(input)))) / lineHeight_Text(font_InputWidget(input))); } @@ -1673,7 +1675,7 @@ iBool valueInputHandler_(iWidget *dlg, const char *cmd) { iWidget *ptr = as_Widget(pointer_Command(cmd)); if (equal_Command(cmd, "window.resized") || equal_Command(cmd, "keyboard.changed")) { if (isVisible_Widget(dlg)) { - updateValueInputWidth_(dlg); + updateValueInputSizing_(dlg); arrange_Widget(dlg); } return iFalse; @@ -1847,7 +1849,7 @@ iWidget *makeValueInput_Widget(iWidget *parent, const iString *initialValue, con dlg->rect.pos.y -= delta; } } - updateValueInputWidth_(dlg); + updateValueInputSizing_(dlg); setupSheetTransition_Mobile(dlg, incoming_TransitionFlag | top_TransitionDir); return dlg; } @@ -1855,7 +1857,7 @@ iWidget *makeValueInput_Widget(iWidget *parent, const iString *initialValue, con void updateValueInput_Widget(iWidget *d, const char *title, const char *prompt) { setTextCStr_LabelWidget(findChild_Widget(d, "valueinput.title"), title); setTextCStr_LabelWidget(findChild_Widget(d, "valueinput.prompt"), prompt); - updateValueInputWidth_(d); + updateValueInputSizing_(d); } static void updateQuestionWidth_(iWidget *dlg) { diff --git a/src/ui/util.h b/src/ui/util.h index 023fa7ae..b9109aee 100644 --- a/src/ui/util.h +++ b/src/ui/util.h @@ -179,11 +179,17 @@ iDeclareType(SmoothScroll) typedef void (*iSmoothScrollNotifyFunc)(iAnyObject *, int offset, uint32_t span); +enum iSmoothScrollFlags { + pullDownAction_SmoothScrollFlag = iBit(1), + pullUpAction_SmoothScrollFlag = iBit(2), +}; + struct Impl_SmoothScroll { iAnim pos; int max; int overscroll; iWidget *widget; + int flags; int pullActionTriggered; iSmoothScrollNotifyFunc notify; }; -- cgit v1.2.3 From b744308198bc821db9e0ed5801b75740289ab385 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Wed, 8 Dec 2021 17:12:57 +0200 Subject: iOS: Fixed safe area inset issues --- src/ui/mobile.c | 2 +- src/ui/root.c | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ui/mobile.c b/src/ui/mobile.c index fdcc4248..5a787e7b 100644 --- a/src/ui/mobile.c +++ b/src/ui/mobile.c @@ -156,7 +156,7 @@ static iBool mainDetailSplitHandler_(iWidget *mainDetailSplit, const char *cmd) if (isSideBySide) { setVisualOffset_Widget(panel, 0, 0, 0); } - setPadding_Widget(panel, pad, 0, pad, pad); + setPadding_Widget(panel, pad, 0, pad, pad + bottomSafeInset_Mobile()); } arrange_Widget(mainDetailSplit); updateCertListHeight_(detailStack); diff --git a/src/ui/root.c b/src/ui/root.c index 2a7bf4ff..08e0eafd 100644 --- a/src/ui/root.c +++ b/src/ui/root.c @@ -1698,9 +1698,10 @@ iRect safeRect_Root(const iRoot *d) { iRect visibleRect_Root(const iRoot *d) { iRect visRect = rect_Root(d); + float bottom = 0.0f; #if defined (iPlatformAppleMobile) /* TODO: Check this on device... Maybe DisplayUsableBounds would be good here, too? */ - float left, top, right, bottom; + float left, top, right; safeAreaInsets_iOS(&left, &top, &right, &bottom); visRect.pos.x = (int) left; visRect.size.x -= (int) (left + right); @@ -1725,6 +1726,6 @@ iRect visibleRect_Root(const iRoot *d) { visRect = intersect_Rect(visRect, init_Rect(usable.x, usable.y, usable.w, usable.h)); } #endif - adjustEdges_Rect(&visRect, 0, 0, -get_MainWindow()->keyboardHeight, 0); + adjustEdges_Rect(&visRect, 0, 0, -get_MainWindow()->keyboardHeight + bottom, 0); return visRect; } -- cgit v1.2.3 From eadba95aa01cea517fc4fb221c19d081e9d8f26e Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Wed, 8 Dec 2021 17:13:14 +0200 Subject: DocumentWidget: Clear selection if document changes --- src/ui/documentwidget.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 87c86192..c24fcf27 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -1123,6 +1123,8 @@ static void showOrHidePinningIndicator_DocumentWidget_(iDocumentWidget *d) { } static void documentWasChanged_DocumentWidget_(iDocumentWidget *d) { + iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iFalse); + setFlags_Widget(as_Widget(d), touchDrag_WidgetFlag, iFalse); updateVisitedLinks_GmDocument(d->doc); documentRunsInvalidated_DocumentWidget_(d); updateWindowTitle_DocumentWidget_(d); -- cgit v1.2.3 From a8d64c19ee2a1015dbbaa019170940f6b7427485 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Wed, 8 Dec 2021 17:13:38 +0200 Subject: Mobile: Minor font/text sizing tweaks --- src/ui/documentwidget.c | 9 ++++----- src/ui/sidebarwidget.c | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index c24fcf27..3dadb911 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -2201,7 +2201,7 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) { } iWidget *counter = (iWidget *) new_LabelWidget("", NULL); setId_Widget(counter, "valueinput.counter"); - setFlags_Widget(counter, frameless_WidgetFlag, iTrue); + setFlags_Widget(counter, frameless_WidgetFlag | resizeToParentHeight_WidgetFlag, iTrue); if (deviceType_App() == desktop_AppDeviceType) { addChildPos_Widget(buttons, iClob(counter), front_WidgetAddPos); } @@ -3215,17 +3215,16 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) } return wasHandled; } - else if (equal_Command(cmd, "document.upload") && d == document_App()) { + else if (equal_Command(cmd, "document.upload") && d == document_App()) { if (findChild_Widget(root_Widget(w), "upload")) { return iTrue; /* already open */ } - if (equalCase_Rangecc(urlScheme_String(d->mod.url), "gemini") || - equalCase_Rangecc(urlScheme_String(d->mod.url), "titan")) { + const iBool isGemini = equalCase_Rangecc(urlScheme_String(d->mod.url), "gemini"); + if (isGemini || equalCase_Rangecc(urlScheme_String(d->mod.url), "titan")) { iUploadWidget *upload = new_UploadWidget(); setUrl_UploadWidget(upload, d->mod.url); setResponseViewer_UploadWidget(upload, d); addChild_Widget(get_Root()->widget, iClob(upload)); -// finalizeSheet_Mobile(as_Widget(upload)); setupSheetTransition_Mobile(as_Widget(upload), iTrue); postRefresh_App(); } diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index c5596b9f..15274987 100644 --- a/src/ui/sidebarwidget.c +++ b/src/ui/sidebarwidget.c @@ -212,7 +212,7 @@ static enum iFontId actionButtonFont_SidebarWidget_(const iSidebarWidget *d) { default: break; case phone_AppDeviceType: - return uiLabelBig_FontId; + return isPortrait_App() ? uiLabelBig_FontId : uiLabelMedium_FontId; case tablet_AppDeviceType: return uiLabelMedium_FontId; } -- cgit v1.2.3 From 1fd1ad1f5220c0a567ca3839b32393b9ce274589 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Wed, 8 Dec 2021 17:37:15 +0200 Subject: Mobile: Hide iOS text view background; Translation panel positioning Mobile panels should always be root-parented, but the source document relationship is currently not communicated in any other way. --- src/ios.m | 2 +- src/ui/documentwidget.c | 5 +++++ src/ui/util.c | 3 ++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/ios.m b/src/ios.m index 6a30c8a8..021e354d 100644 --- a/src/ios.m +++ b/src/ios.m @@ -759,7 +759,7 @@ void init_SystemTextInput(iSystemTextInput *d, iRect rect, int flags) { } else { UITextView *view = REF_d_view; - [view setBackgroundColor:backgroundColor]; + [view setBackgroundColor:[UIColor colorWithWhite:1.0f alpha:0.0f]]; [view setTextColor:textColor]; [view setTintColor:tintColor]; [view setEditable:YES]; diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 3dadb911..a9a0e07c 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -3204,6 +3204,11 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) else if (equal_Command(cmd, "document.translate") && d == document_App()) { if (!d->translation) { d->translation = new_Translation(d); + if (isUsingPanelLayout_Mobile()) { + const iRect safe = safeRect_Root(w->root); + d->translation->dlg->rect.pos = windowToLocal_Widget(w, zero_I2()); + d->translation->dlg->rect.size = safe.size; + } } return iTrue; } diff --git a/src/ui/util.c b/src/ui/util.c index 6e139856..21067bf4 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -1655,7 +1655,8 @@ static void updateValueInputSizing_(iWidget *dlg) { } else { dlg->rect.size.x = - iMin(rootSize.x, iMaxi(iMaxi(100 * gap_UI, title->rect.size.x), prompt->rect.size.x)); + iMin(rootSize.x, iMaxi(iMaxi(100 * gap_UI, title ? title->rect.size.x : 0), + prompt->rect.size.x)); } /* Adjust the maximum number of visible lines. */ int footer = 6 * gap_UI; -- cgit v1.2.3 From d08c4c427b2a6bb314ab8d8195b38efcd59db8ef Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Wed, 8 Dec 2021 17:41:56 +0200 Subject: Recompiled language strings --- res/lang/cs.bin | Bin 31216 -> 31255 bytes res/lang/de.bin | Bin 30223 -> 30262 bytes res/lang/en.bin | Bin 26320 -> 26359 bytes res/lang/eo.bin | Bin 25274 -> 25313 bytes res/lang/es.bin | Bin 30047 -> 30086 bytes res/lang/es_MX.bin | Bin 27379 -> 27418 bytes res/lang/fi.bin | Bin 29880 -> 29919 bytes res/lang/fr.bin | Bin 31027 -> 31066 bytes res/lang/gl.bin | Bin 29232 -> 29271 bytes res/lang/hu.bin | Bin 31052 -> 31091 bytes res/lang/ia.bin | Bin 28379 -> 28418 bytes res/lang/ie.bin | Bin 28967 -> 29006 bytes res/lang/isv.bin | Bin 25040 -> 25079 bytes res/lang/pl.bin | Bin 29655 -> 29694 bytes res/lang/ru.bin | Bin 44415 -> 44454 bytes res/lang/sk.bin | Bin 25376 -> 25415 bytes res/lang/sr.bin | Bin 43841 -> 43880 bytes res/lang/tok.bin | Bin 27089 -> 27128 bytes res/lang/tr.bin | Bin 29273 -> 29312 bytes res/lang/uk.bin | Bin 43760 -> 43799 bytes res/lang/zh_Hans.bin | Bin 25274 -> 25313 bytes res/lang/zh_Hant.bin | Bin 25472 -> 25511 bytes 22 files changed, 0 insertions(+), 0 deletions(-) diff --git a/res/lang/cs.bin b/res/lang/cs.bin index aacd5072..488dec0a 100644 Binary files a/res/lang/cs.bin and b/res/lang/cs.bin differ diff --git a/res/lang/de.bin b/res/lang/de.bin index 83039237..bc05cc18 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 03f69772..3998160c 100644 Binary files a/res/lang/en.bin and b/res/lang/en.bin differ diff --git a/res/lang/eo.bin b/res/lang/eo.bin index 638e3b7e..1bc766b1 100644 Binary files a/res/lang/eo.bin and b/res/lang/eo.bin differ diff --git a/res/lang/es.bin b/res/lang/es.bin index 11cdd187..937cfaf2 100644 Binary files a/res/lang/es.bin and b/res/lang/es.bin differ diff --git a/res/lang/es_MX.bin b/res/lang/es_MX.bin index 4725bc56..db886661 100644 Binary files a/res/lang/es_MX.bin and b/res/lang/es_MX.bin differ diff --git a/res/lang/fi.bin b/res/lang/fi.bin index 254ae4e9..370ddf15 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 d26e99db..a5785ffe 100644 Binary files a/res/lang/fr.bin and b/res/lang/fr.bin differ diff --git a/res/lang/gl.bin b/res/lang/gl.bin index f67655a4..8c8e7b84 100644 Binary files a/res/lang/gl.bin and b/res/lang/gl.bin differ diff --git a/res/lang/hu.bin b/res/lang/hu.bin index 6420c3c0..c9cc4704 100644 Binary files a/res/lang/hu.bin and b/res/lang/hu.bin differ diff --git a/res/lang/ia.bin b/res/lang/ia.bin index 0c55d980..a6465eeb 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 d95a1723..c1b7e6e6 100644 Binary files a/res/lang/ie.bin and b/res/lang/ie.bin differ diff --git a/res/lang/isv.bin b/res/lang/isv.bin index c61d9465..23a6ca82 100644 Binary files a/res/lang/isv.bin and b/res/lang/isv.bin differ diff --git a/res/lang/pl.bin b/res/lang/pl.bin index 370bbca2..eadd2cfe 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 c04db5fb..e196e341 100644 Binary files a/res/lang/ru.bin and b/res/lang/ru.bin differ diff --git a/res/lang/sk.bin b/res/lang/sk.bin index 36a1f1d1..49a06577 100644 Binary files a/res/lang/sk.bin and b/res/lang/sk.bin differ diff --git a/res/lang/sr.bin b/res/lang/sr.bin index 9d8c40de..24f0c616 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 c2432e42..b1cf8870 100644 Binary files a/res/lang/tok.bin and b/res/lang/tok.bin differ diff --git a/res/lang/tr.bin b/res/lang/tr.bin index 80497417..7a4e8592 100644 Binary files a/res/lang/tr.bin and b/res/lang/tr.bin differ diff --git a/res/lang/uk.bin b/res/lang/uk.bin index e77375da..cb485617 100644 Binary files a/res/lang/uk.bin and b/res/lang/uk.bin differ diff --git a/res/lang/zh_Hans.bin b/res/lang/zh_Hans.bin index 9c3a92b0..a45e5590 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 c5d2d7fa..b643c187 100644 Binary files a/res/lang/zh_Hant.bin and b/res/lang/zh_Hant.bin differ -- cgit v1.2.3 From 31d54820a6f8ecf02ca8e234543b0139047bdbda Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Wed, 8 Dec 2021 17:50:45 +0200 Subject: iOS: Bumped version number to 1.10 (2); updated release notes --- CMakeLists.txt | 4 ++-- res/about/ios-version.gmi | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 85a32b69..dba65757 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,8 +25,8 @@ project (Lagrange set (COPYRIGHT_YEAR 2021) if (IOS) set (PROJECT_VERSION 1.10) - set (IOS_BUNDLE_VERSION 1) - set (IOS_BUILD_DATE "2021-12-06") + set (IOS_BUNDLE_VERSION 2) + set (IOS_BUILD_DATE "2021-12-08") endif () # Defaults that depend on environment. diff --git a/res/about/ios-version.gmi b/res/about/ios-version.gmi index 45fde9e6..b1421bf9 100644 --- a/res/about/ios-version.gmi +++ b/res/about/ios-version.gmi @@ -6,6 +6,17 @@ ``` # Release notes +## 1.10 (2) +* Fixed pull-to-refresh on short pages. +* Fixed URL field contents not being clipped to widget bounds. +* Fixed major glitches in back/forward swipe navigation, e.g., top banner showing incorrect contents, incorrect theme colors, scroll position jumping. +* Fixed major and minor UI element positioning glitches, e.g., native text fields, maximum height of input fields, input length counter, Translate dialog. +* Fixed inappropriate font sizes, e.g., sidebar action labels. +* Fixed color issues: tint color of native input, and footer buttons not being prominent enough. +* Bookmarks: In Edit mode, folders still get (un)folded when tapped. +* Feeds: Added "No Unread Entries" message. +* Identities: Show the current page's URL in the context menu to make it clearer what "This Page" refers to. + ## 1.10 (1) * Use native iOS text input UI controls for entering text. * Pull-to-refresh on pages. -- cgit v1.2.3 From ef2387aa3564cb010307591a7b446807f14ff021 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 9 Dec 2021 08:30:54 +0200 Subject: iOS: Fixed backups in InputWidget --- src/ui/inputwidget.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index 3bee3d21..3be6a383 100644 --- a/src/ui/inputwidget.c +++ b/src/ui/inputwidget.c @@ -1150,6 +1150,7 @@ static void contentsWereChanged_InputWidget_(iInputWidget *); void systemInputChanged_InputWidget_(iSystemTextInput *sysCtrl, void *widget) { iInputWidget *d = widget; set_String(&d->text, text_SystemTextInput(sysCtrl)); + restartBackupTimer_InputWidget_(d); contentsWereChanged_InputWidget_(d); updateMetrics_InputWidget_(d); } -- cgit v1.2.3 From 8e445f3c0c35eae785e933d380142e2971639960 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 9 Dec 2021 08:31:05 +0200 Subject: iOS: Use accent color in input fields --- src/ios.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ios.m b/src/ios.m index 021e354d..2705a350 100644 --- a/src/ios.m +++ b/src/ios.m @@ -748,7 +748,7 @@ void init_SystemTextInput(iSystemTextInput *d, iRect rect, int flags) { } UIColor *textColor = makeUIColor_(uiInputTextFocused_ColorId); UIColor *backgroundColor = makeUIColor_(uiInputBackgroundFocused_ColorId); - UIColor *tintColor = makeUIColor_(uiInputCursor_ColorId); + UIColor *tintColor = makeUIColor_(uiInputFrameHover_ColorId); /* use the accent color */ //uiInputCursor_ColorId); [appState_ setSystemTextInput:d]; if (d->field) { UITextField *field = REF_d_field; -- cgit v1.2.3 From 1545ef0167fbfa91744e612adb22fa92cad38091 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 9 Dec 2021 08:31:34 +0200 Subject: Mobile: UploadWidget work-in-progress Added a context menu for text editing actions. --- src/ui/uploadwidget.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/ui/uploadwidget.c b/src/ui/uploadwidget.c index 89376633..ef407b6d 100644 --- a/src/ui/uploadwidget.c +++ b/src/ui/uploadwidget.c @@ -205,6 +205,15 @@ void init_UploadWidget(iUploadWidget *d) { if (isPortraitPhone_App()) { enableUploadButton_UploadWidget_(d, iFalse); } + iWidget *title = findChild_Widget(w, "heading.upload.text"); + iLabelWidget *menu = makeMenuButton_LabelWidget(midEllipsis_Icon, (iMenuItem[]){ + { export_Icon " ${upload.text.export}", 0, 0, "upload.text.export" }, + { "---" }, + { delete_Icon " " uiTextCaution_ColorEscape "${menu.delete}", 0, 0, "upload.text.delete" } + }, 3); + setTextColor_LabelWidget(menu, uiTextAction_ColorId); + setFont_LabelWidget(menu, uiLabelBigBold_FontId); + addChildFlags_Widget(title, iClob(menu), frameless_WidgetFlag | moveToParentRightEdge_WidgetFlag); } else { useSheetStyle_Widget(w); @@ -410,8 +419,15 @@ static iBool processEvent_UploadWidget_(iUploadWidget *d, const SDL_Event *ev) { } else if (equal_Command(cmd, "panel.changed")) { showOrHideUploadButton_UploadWidget_(d); + setFocus_Widget(NULL); return iFalse; } +#if defined (iPlatformAppleMobile) + else if (deviceType_App() != desktop_AppDeviceType && equal_Command(cmd, "menu.opened")) { + setFocus_Widget(NULL); /* overlaid text fields! */ + return iFalse; + } +#endif else if (equal_Command(cmd, "upload.cancel")) { setupSheetTransition_Mobile(w, iFalse); destroy_Widget(w); -- cgit v1.2.3 From f969ea2d93208bcc99eb9e513275686dfee5108a Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 9 Dec 2021 11:34:33 +0200 Subject: Avoid allocations in event processing The command string processing is done very often (e.g., whenever the mouse moves, potentially), so avoid allocating garbage-collected memory. --- src/ui/command.c | 77 ++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 53 insertions(+), 24 deletions(-) diff --git a/src/ui/command.c b/src/ui/command.c index 3ae0f0c9..d6c668db 100644 --- a/src/ui/command.c +++ b/src/ui/command.c @@ -26,6 +26,34 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include #include +iDeclareType(Token) + +#define maxLen_Token 64 + +struct Impl_Token { + char buf[64]; + size_t size; +}; + +static void init_Token(iToken *d, const char *label) { + const size_t len = strlen(label); + iAssert(len < sizeof(d->buf) - 3); + d->buf[0] = ' '; + memcpy(d->buf + 1, label, len); + d->buf[1 + len] = ':'; + d->buf[1 + len + 1] = 0; + d->size = len + 2; +} + +static iRangecc find_Token(const iToken *d, const char *cmd) { + iRangecc range = iNullRange; + range.start = strstr(cmd, d->buf); + if (range.start) { + range.end = range.start + d->size; + } + return range; +} + iBool equal_Command(const char *cmdWithArgs, const char *cmd) { if (strchr(cmdWithArgs, ':')) { return startsWith_CStr(cmdWithArgs, cmd) && cmdWithArgs[strlen(cmd)] == ' '; @@ -33,15 +61,12 @@ iBool equal_Command(const char *cmdWithArgs, const char *cmd) { return equal_CStr(cmdWithArgs, cmd); } -static const iString *tokenString_(const char *label) { - return collectNewFormat_String(" %s:", label); -} - int argLabel_Command(const char *cmd, const char *label) { - const iString *tok = tokenString_(label); - const char *ptr = strstr(cmd, cstr_String(tok)); - if (ptr) { - return atoi(ptr + size_String(tok)); + iToken tok; + init_Token(&tok, label); + iRangecc ptr = find_Token(&tok, cmd); + if (ptr.start) { + return atoi(ptr.end); } return 0; } @@ -51,19 +76,21 @@ int arg_Command(const char *cmd) { } uint32_t argU32Label_Command(const char *cmd, const char *label) { - const iString *tok = tokenString_(label); - const char *ptr = strstr(cmd, cstr_String(tok)); - if (ptr) { - return strtoul(ptr + size_String(tok), NULL, 10); + iToken tok; + init_Token(&tok, label); + const iRangecc ptr = find_Token(&tok, cmd); + if (ptr.start) { + return strtoul(ptr.end, NULL, 10); } return 0; } float argfLabel_Command(const char *cmd, const char *label) { - const iString *tok = tokenString_(label); - const char *ptr = strstr(cmd, cstr_String(tok)); - if (ptr) { - return strtof(ptr + size_String(tok), NULL); + iToken tok; + init_Token(&tok, label); + const iRangecc ptr = find_Token(&tok, cmd); + if (ptr.start) { + return strtof(ptr.end, NULL); } return 0.0f; } @@ -77,11 +104,12 @@ float argf_Command(const char *cmd) { } void *pointerLabel_Command(const char *cmd, const char *label) { - const iString *tok = tokenString_(label); - const char *ptr = strstr(cmd, cstr_String(tok)); - if (ptr) { + iToken tok; + init_Token(&tok, label); + const iRangecc ptr = find_Token(&tok, cmd); + if (ptr.start) { void *val = NULL; - sscanf(ptr + size_String(tok), "%p", &val); + sscanf(ptr.end, "%p", &val); return val; } return NULL; @@ -92,10 +120,11 @@ void *pointer_Command(const char *cmd) { } const char *suffixPtr_Command(const char *cmd, const char *label) { - const iString *tok = tokenString_(label); - const char *ptr = strstr(cmd, cstr_String(tok)); - if (ptr) { - return ptr + size_String(tok); + iToken tok; + init_Token(&tok, label); + const iRangecc ptr = find_Token(&tok, cmd); + if (ptr.start) { + return ptr.end; } return NULL; } -- cgit v1.2.3 From c17fe5ed17c906b0c0bc21e07d87d281d04e10e6 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 9 Dec 2021 11:35:03 +0200 Subject: UploadWidget: Improved Identity dropdown Better colors, keep the menu button fixed-height. --- src/ui/labelwidget.c | 17 +++++++---------- src/ui/uploadwidget.c | 25 +++++++++++++++---------- src/ui/util.c | 14 +++++++++++--- 3 files changed, 33 insertions(+), 23 deletions(-) diff --git a/src/ui/labelwidget.c b/src/ui/labelwidget.c index d00733e1..9e0d37e4 100644 --- a/src/ui/labelwidget.c +++ b/src/ui/labelwidget.c @@ -195,6 +195,7 @@ static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int int *icon, int *meta) { const iWidget *w = constAs_Widget(d); const int64_t flags = flags_Widget(w); + const iBool isHover = isHover_LabelWidget_(d); const iBool isFocus = (flags & focusable_WidgetFlag && isFocused_Widget(d)); const iBool isPress = (flags & pressed_WidgetFlag) != 0; const iBool isSel = (flags & selected_WidgetFlag) != 0; @@ -222,9 +223,6 @@ static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int if (isSel) { if (!d->flags.checkMark) { *bg = uiBackgroundSelected_ColorId; -// if (!isKeyRoot) { -// *bg = uiEmbossSelected1_ColorId; //uiBackgroundUnfocusedSelection_ColorId; -// } if (!isKeyRoot) { *bg = isDark_ColorTheme(colorTheme_App()) ? uiBackgroundUnfocusedSelection_ColorId : uiMarked_ColorId; @@ -249,7 +247,7 @@ static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int if (colorEscape == uiTextCaution_ColorId) { *icon = *meta = colorEscape; } - if (isHover_LabelWidget_(d)) { + if (isHover) { if (isFrameless) { *bg = uiBackgroundFramelessHover_ColorId; *fg = uiTextFramelessHover_ColorId; @@ -275,7 +273,7 @@ static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int } } if (d->forceFg >= 0) { - *fg = /* *icon = */ *meta = d->forceFg; + *fg = *meta = d->forceFg; } if (isPress) { if (colorEscape == uiTextAction_ColorId || colorEscape == uiTextCaution_ColorId) { @@ -290,14 +288,13 @@ static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int *frame1 = uiEmbossPressed1_ColorId; *frame2 = colorEscape != none_ColorId ? colorEscape : uiEmbossPressed2_ColorId; } - //if (colorEscape == none_ColorId || colorEscape == uiTextAction_ColorId) { *fg = *icon = *meta = uiTextPressed_ColorId | permanent_ColorId; - // } - // else { - // *fg = (isDark_ColorTheme(colorTheme_App()) ? white_ColorId : black_ColorId) | permanent_ColorId; - // } } } + if (((isSel || isHover) && isFrameless) || isPress) { + /* Ensure that the full label text remains readable. */ + *fg |= permanent_ColorId; + } } iLocalDef int iconPadding_LabelWidget_(const iLabelWidget *d) { diff --git a/src/ui/uploadwidget.c b/src/ui/uploadwidget.c index ef407b6d..e0bc3755 100644 --- a/src/ui/uploadwidget.c +++ b/src/ui/uploadwidget.c @@ -58,6 +58,7 @@ struct Impl_UploadWidget { iLabelWidget * info; iInputWidget * mime; iInputWidget * token; + iLabelWidget * ident; iInputWidget * input; iLabelWidget * filePathLabel; iLabelWidget * fileSizeLabel; @@ -124,9 +125,10 @@ static const iArray *makeIdentityItems_UploadWidget_(const iUploadWidget *d) { iConstForEach(PtrArray, i, listIdentities_GmCerts(certs_App(), NULL, NULL)) { const iGmIdentity *id = i.ptr; iString *str = collect_String(copy_String(name_GmIdentity(id))); + prependCStr_String(str, uiTextStrong_ColorEscape); if (!isEmpty_String(&id->notes)) { appendFormat_String( - str, "\n%s%s", escape_Color(uiAnnotation_ColorId), cstr_String(&id->notes)); + str, "\n%s%s", escape_Color(uiTextDim_ColorId), cstr_String(&id->notes)); } pushBack_Array( items, @@ -259,19 +261,20 @@ void init_UploadWidget(iUploadWidget *d) { /* Identity and Token. */ { addChild_Widget(w, iClob(makePadding_Widget(gap_UI))); iWidget *page = makeTwoColumns_Widget(&headings, &values); - /* Token. */ - d->token = addTwoColumnDialogInputField_Widget( - headings, values, "${upload.token}", "upload.token", iClob(new_InputWidget(0))); - setHint_InputWidget(d->token, "${hint.upload.token}"); - setFixedSize_Widget(as_Widget(d->token), init_I2(50 * gap_UI, -1)); /* Identity. */ const iArray * identItems = makeIdentityItems_UploadWidget_(d); const iMenuItem *items = constData_Array(identItems); const size_t numItems = size_Array(identItems); - iLabelWidget * ident = makeMenuButton_LabelWidget("${upload.id}", items, numItems); - setTextCStr_LabelWidget(ident, items[findWidestLabel_MenuItem(items, numItems)].label); + d->ident = makeMenuButton_LabelWidget("${upload.id}", items, numItems); + setTextCStr_LabelWidget(d->ident, items[findWidestLabel_MenuItem(items, numItems)].label); + //setFixedSize_Widget(as_Widget(d->ident), init_I2(50 * gap_UI, )); addChild_Widget(headings, iClob(makeHeading_Widget("${upload.id}"))); - setId_Widget(addChildFlags_Widget(values, iClob(ident), alignLeft_WidgetFlag), "upload.id"); + setId_Widget(addChildFlags_Widget(values, iClob(d->ident), alignLeft_WidgetFlag), "upload.id"); + /* Token. */ + d->token = addTwoColumnDialogInputField_Widget( + headings, values, "${upload.token}", "upload.token", iClob(new_InputWidget(0))); + setHint_InputWidget(d->token, "${hint.upload.token}"); + setFixedSize_Widget(as_Widget(d->token), init_I2(50 * gap_UI, -1)); addChild_Widget(w, iClob(page)); } /* Buttons. */ { @@ -287,6 +290,8 @@ void init_UploadWidget(iUploadWidget *d) { resizeToLargestPage_Widget(tabs); arrange_Widget(w); setFixedSize_Widget(as_Widget(d->token), init_I2(width_Widget(tabs) - left_Rect(parent_Widget(d->token)->rect), -1)); + setFixedSize_Widget(as_Widget(d->ident), init_I2(width_Widget(d->token), + lineHeight_Text(uiLabel_FontId) + 2 * gap_UI)); setFlags_Widget(as_Widget(d->token), expand_WidgetFlag, iTrue); setFocus_Widget(as_Widget(d->input)); } @@ -560,7 +565,7 @@ static iBool processEvent_UploadWidget_(iUploadWidget *d, const SDL_Event *ev) { destroy_Widget(w); return iTrue; } - else if (isCommand_Widget(w, ev, "input.resized")) { + else if (!isUsingPanelLayout_Mobile() && isCommand_Widget(w, ev, "input.resized")) { resizeToLargestPage_Widget(findChild_Widget(w, "upload.tabs")); arrange_Widget(w); refresh_Widget(w); diff --git a/src/ui/util.c b/src/ui/util.c index 21067bf4..73fafe84 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -1327,6 +1327,12 @@ const iString *removeMenuItemLabelPrefixes_String(const iString *d) { return collect_String(str); } +static const iString *replaceNewlinesWithDash_(const iString *str) { + iString *mod = copy_String(str); + replace_String(mod, "\n", " "); + return collect_String(mod); +} + void updateDropdownSelection_LabelWidget(iLabelWidget *dropButton, const char *selectedCommand) { if (!dropButton) { return; @@ -1337,8 +1343,9 @@ void updateDropdownSelection_LabelWidget(iLabelWidget *dropButton, const char *s iMenuItem *item = findNativeMenuItem_Widget(menu, selectedCommand); if (item) { setSelected_NativeMenuItem(item, iTrue); - updateText_LabelWidget( - dropButton, removeMenuItemLabelPrefixes_String(collectNewCStr_String(item->label))); + updateText_LabelWidget(dropButton, + replaceNewlinesWithDash_(removeMenuItemLabelPrefixes_String( + collectNewCStr_String(item->label)))); checkIcon_LabelWidget(dropButton); } return; @@ -1349,7 +1356,8 @@ void updateDropdownSelection_LabelWidget(iLabelWidget *dropButton, const char *s const iBool isSelected = endsWith_String(command_LabelWidget(item), selectedCommand); setFlags_Widget(as_Widget(item), selected_WidgetFlag, isSelected); if (isSelected) { - updateText_LabelWidget(dropButton, sourceText_LabelWidget(item)); + updateText_LabelWidget(dropButton, + replaceNewlinesWithDash_(text_LabelWidget(item))); checkIcon_LabelWidget(dropButton); } } -- cgit v1.2.3 From 38bc51a65be35488857249e5ed5949172f06ee89 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 9 Dec 2021 11:40:44 +0200 Subject: UploadWidget: Bold identity names --- src/ui/uploadwidget.c | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/ui/uploadwidget.c b/src/ui/uploadwidget.c index e0bc3755..e11970ec 100644 --- a/src/ui/uploadwidget.c +++ b/src/ui/uploadwidget.c @@ -125,10 +125,10 @@ static const iArray *makeIdentityItems_UploadWidget_(const iUploadWidget *d) { iConstForEach(PtrArray, i, listIdentities_GmCerts(certs_App(), NULL, NULL)) { const iGmIdentity *id = i.ptr; iString *str = collect_String(copy_String(name_GmIdentity(id))); - prependCStr_String(str, uiTextStrong_ColorEscape); + prependCStr_String(str, "\x1b[1m"); if (!isEmpty_String(&id->notes)) { appendFormat_String( - str, "\n%s%s", escape_Color(uiTextDim_ColorId), cstr_String(&id->notes)); + str, "\x1b[0m\n%s%s", escape_Color(uiTextDim_ColorId), cstr_String(&id->notes)); } pushBack_Array( items, @@ -220,12 +220,16 @@ void init_UploadWidget(iUploadWidget *d) { else { useSheetStyle_Widget(w); setFlags_Widget(w, overflowScrollable_WidgetFlag, iFalse); - addChildFlags_Widget(w, - iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.upload}", NULL)), - frameless_WidgetFlag); - d->info = addChildFlags_Widget(w, iClob(new_LabelWidget("", NULL)), + setAllCaps_LabelWidget( + addChildFlags_Widget( + w, + iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.upload}", NULL)), + frameless_WidgetFlag), + iTrue); + d->info = addChildFlags_Widget(w, + iClob(new_LabelWidget("", NULL)), frameless_WidgetFlag | resizeToParentWidth_WidgetFlag | - fixedHeight_WidgetFlag); + fixedHeight_WidgetFlag); setWrap_LabelWidget(d->info, iTrue); /* Tabs for input data. */ iWidget *tabs = makeTabs_Widget(w); -- cgit v1.2.3 From a265ab290cc9bce2f0c305a3eaf9d743380b967b Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 9 Dec 2021 15:06:02 +0200 Subject: iOS: Fixes and new edit menu for UploadWidget The sizing and behavior of the input field on the plain text upload page is much improved. --- po/en.po | 9 +++++ src/ios.h | 7 ++-- src/ios.m | 37 +++++++++++++++++++-- src/ui/inputwidget.c | 25 +++++++++----- src/ui/labelwidget.c | 4 +++ src/ui/uploadwidget.c | 92 +++++++++++++++++++++++++++++++++++++++------------ src/ui/util.c | 1 + 7 files changed, 139 insertions(+), 36 deletions(-) diff --git a/po/en.po b/po/en.po index 8e690649..89a05939 100644 --- a/po/en.po +++ b/po/en.po @@ -1017,6 +1017,15 @@ msgstr "Default" msgid "heading.upload.text" msgstr "Text" +msgid "menu.upload.export" +msgstr "Export Text" + +msgid "menu.upload.delete" +msgstr "Delete All" + +msgid "menu.upload.delete.confirm" +msgstr "Really Delete All (No Undo)" + msgid "hint.upload.text" msgstr "enter text to upload" diff --git a/src/ios.h b/src/ios.h index 6c5ec8d5..fbe7c2eb 100644 --- a/src/ios.h +++ b/src/ios.h @@ -38,6 +38,7 @@ void playHapticEffect_iOS (enum iHapticEffect effect); void exportDownloadedFile_iOS(const iString *path); void pickFileForOpening_iOS (void); void pickFile_iOS (const char *command); /* ` path:%s` will be appended */ +void openTextActivityView_iOS(const iString *text); iBool isPhone_iOS (void); void safeAreaInsets_iOS (float *left, float *top, float *right, float *bottom); @@ -79,9 +80,11 @@ iDeclareType(SystemTextInput) iDeclareTypeConstructionArgs(SystemTextInput, iRect rect, int flags) void setRect_SystemTextInput (iSystemTextInput *, iRect rect); -void setText_SystemTextInput (iSystemTextInput *, const iString *text); +void setText_SystemTextInput (iSystemTextInput *, const iString *text, iBool allowUndo); void setFont_SystemTextInput (iSystemTextInput *, int fontId); -void setTextChangedFunc_SystemTextInput (iSystemTextInput *, void (*textChangedFunc)(iSystemTextInput *, void *), void *); +void setTextChangedFunc_SystemTextInput + (iSystemTextInput *, void (*textChangedFunc)(iSystemTextInput *, void *), void *); +void selectAll_SystemTextInput(iSystemTextInput *); const iString * text_SystemTextInput (const iSystemTextInput *); int preferredHeight_SystemTextInput (const iSystemTextInput *); diff --git a/src/ios.m b/src/ios.m index 2705a350..b6fbdec0 100644 --- a/src/ios.m +++ b/src/ios.m @@ -536,6 +536,15 @@ void pickFile_iOS(const char *command) { [viewController_(get_Window()) presentViewController:picker animated:YES completion:nil]; } +void openTextActivityView_iOS(const iString *text) { + UIActivityViewController *actView = + [[UIActivityViewController alloc] + initWithActivityItems:@[ + [NSString stringWithUTF8String:cstr_String(text)]] + applicationActivities:nil]; + [viewController_(get_Window()) presentViewController:actView animated:YES completion:nil]; +} + /*----------------------------------------------------------------------------------------------*/ enum iAVFAudioPlayerState { @@ -784,7 +793,16 @@ void deinit_SystemTextInput(iSystemTextInput *d) { } } -void setText_SystemTextInput(iSystemTextInput *d, const iString *text) { +void selectAll_SystemTextInput(iSystemTextInput *d) { + if (d->field) { + [REF_d_field selectAll:nil]; + } + if (d->view) { + [REF_d_view selectAll:nil]; + } +} + +void setText_SystemTextInput(iSystemTextInput *d, const iString *text, iBool allowUndo) { NSString *str = [NSString stringWithUTF8String:cstr_String(text)]; if (d->field) { [REF_d_field setText:str]; @@ -793,9 +811,22 @@ void setText_SystemTextInput(iSystemTextInput *d, const iString *text) { } } else { - [REF_d_view setText:str]; + UITextView *view = REF_d_view; +// if (allowUndo) { +// [view selectAll:nil]; +// if ([view shouldChangeTextInRange:[view selectedTextRange] replacementText:@""]) { +// [[view textStorage] beginEditing]; +// [[view textStorage] replaceCharactersInRange:[view selectedRange] withString:@""]; +// [[view textStorage] endEditing]; +// } +// } +// else { + // TODO: How to implement `allowUndo`, given that UITextView does not exist when unfocused? + // Maybe keep the UITextStorage (if it has the undo?)? + [view setText:str]; +// } if (d->flags & selectAll_SystemTextInputFlags) { - [REF_d_view selectAll:nil]; + [view selectAll:nil]; } } } diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index 3be6a383..22f983eb 100644 --- a/src/ui/inputwidget.c +++ b/src/ui/inputwidget.c @@ -715,7 +715,8 @@ static void updateAllLinesAndResizeHeight_InputWidget_(iInputWidget *d) { const int height = measure_WrapText(&wt, d->font).bounds.size.y; /* We use this to store the number wrapped lines for determining widget height. */ d->visWrapLines.start = 0; - d->visWrapLines.end = iMin(d->maxWrapLines, height / lineHeight_Text(d->font)); + d->visWrapLines.end = iMax(d->minWrapLines, + iMin(d->maxWrapLines, height / lineHeight_Text(d->font))); updateMetrics_InputWidget_(d); } @@ -733,8 +734,10 @@ static int contentHeight_InputWidget_(const iInputWidget *d) { if (d->buffered && ~d->inFlags & needUpdateBuffer_InputWidgetFlag) { return iClamp(d->buffered->size.y, minHeight, maxHeight); } -#endif + return minHeight; +#else return (int) size_Range(&d->visWrapLines) * lineHeight; +#endif } static void updateTextInputRect_InputWidget_(const iInputWidget *d) { @@ -1111,7 +1114,7 @@ void setText_InputWidget(iInputWidget *d, const iString *text) { #else set_String(&d->text, nfcText); if (d->sysCtrl) { - setText_SystemTextInput(d->sysCtrl, nfcText); + setText_SystemTextInput(d->sysCtrl, nfcText, iTrue); } else { updateAllLinesAndResizeHeight_InputWidget_(d); /* need to know the new height */ @@ -1134,7 +1137,11 @@ void setTextCStr_InputWidget(iInputWidget *d, const char *cstr) { } void selectAll_InputWidget(iInputWidget *d) { -#if !LAGRANGE_USE_SYSTEM_TEXT_INPUT +#if LAGRANGE_USE_SYSTEM_TEXT_INPUT + if (d->sysCtrl) { + selectAll_SystemTextInput(d->sysCtrl); + } +#else d->mark = (iRanges){ 0, lastLine_InputWidget_(d)->range.end }; refresh_Widget(as_Widget(d)); #endif @@ -1177,7 +1184,7 @@ void begin_InputWidget(iInputWidget *d) { (isAllowedToInsertNewline_InputWidget_(d) ? insertNewlines_SystemTextInputFlag : 0) | (d->inFlags & selectAllOnFocus_InputWidgetFlag ? selectAll_SystemTextInputFlags : 0)); setFont_SystemTextInput(d->sysCtrl, d->font); - setText_SystemTextInput(d->sysCtrl, &d->oldText); + setText_SystemTextInput(d->sysCtrl, &d->oldText, iFalse); setTextChangedFunc_SystemTextInput(d->sysCtrl, systemInputChanged_InputWidget_, d); iConnect(Root, w->root, visualOffsetsChanged, d, updateAfterVisualOffsetChange_InputWidget_); updateTextInputRect_InputWidget_(d); @@ -2157,10 +2164,6 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { } return iTrue; } - else if (isCommand_UserEvent(ev, "input.selectall") && isEditing_InputWidget_(d)) { - selectAll_InputWidget(d); - return iTrue; - } else if (isCommand_UserEvent(ev, "text.insert")) { pushUndo_InputWidget_(d); deleteMarked_InputWidget_(d); @@ -2169,6 +2172,10 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { return iTrue; } #endif + else if (isCommand_UserEvent(ev, "input.selectall") && isEditing_InputWidget_(d)) { + selectAll_InputWidget(d); + return iTrue; + } else if (isCommand_UserEvent(ev, "theme.changed")) { if (d->buffered) { d->inFlags |= needUpdateBuffer_InputWidgetFlag; diff --git a/src/ui/labelwidget.c b/src/ui/labelwidget.c index 9e0d37e4..5ad43b0e 100644 --- a/src/ui/labelwidget.c +++ b/src/ui/labelwidget.c @@ -134,6 +134,10 @@ static iBool processEvent_LabelWidget_(iLabelWidget *d, const SDL_Event *ev) { refresh_Widget(d); return iFalse; } + else if (isCommand_Widget(w, ev, "trigger")) { + trigger_LabelWidget_(d); + return iTrue; + } if (!isEmpty_String(&d->command)) { #if 0 && defined (iPlatformAppleMobile) /* Touch allows activating any button on release. */ diff --git a/src/ui/uploadwidget.c b/src/ui/uploadwidget.c index e11970ec..58106dcb 100644 --- a/src/ui/uploadwidget.c +++ b/src/ui/uploadwidget.c @@ -92,17 +92,19 @@ static void updateProgress_UploadWidget_(iGmRequest *request, size_t current, si static void updateInputMaxHeight_UploadWidget_(iUploadWidget *d) { iWidget *w = as_Widget(d); /* Calculate how many lines fits vertically in the view. */ - const iInt2 inputPos = topLeft_Rect(bounds_Widget(as_Widget(d->input))); - const int footerHeight = isUsingPanelLayout_Mobile() ? 0 : - (height_Widget(d->token) + - height_Widget(findChild_Widget(w, "dialogbuttons")) + - 12 * gap_UI); - const int avail = bottom_Rect(safeRect_Root(w->root)) - footerHeight - - get_MainWindow()->keyboardHeight; - setLineLimits_InputWidget(d->input, - minLines_InputWidget(d->input), - iMaxi(minLines_InputWidget(d->input), - (avail - inputPos.y) / lineHeight_Text(font_InputWidget(d->input)))); + const iInt2 inputPos = topLeft_Rect(bounds_Widget(as_Widget(d->input))); + int footerHeight = 0; + if (!isUsingPanelLayout_Mobile()) { + footerHeight = (height_Widget(d->token) + + height_Widget(findChild_Widget(w, "dialogbuttons")) + + 12 * gap_UI); + } + const int avail = bottom_Rect(visibleRect_Root(w->root)) - footerHeight - inputPos.y; + /* On desktop, retain the previously set minLines value. */ + int minLines = isUsingPanelLayout_Mobile() ? 1 : minLines_InputWidget(d->input); + int maxLines = iMaxi(minLines, avail / lineHeight_Text(font_InputWidget(d->input))); + /* On mobile, the height is fixed to the available space. */ + setLineLimits_InputWidget(d->input, isUsingPanelLayout_Mobile() ? maxLines : minLines, maxLines); } static const iGmIdentity *titanIdentityForUrl_(const iString *url) { @@ -208,11 +210,7 @@ void init_UploadWidget(iUploadWidget *d) { enableUploadButton_UploadWidget_(d, iFalse); } iWidget *title = findChild_Widget(w, "heading.upload.text"); - iLabelWidget *menu = makeMenuButton_LabelWidget(midEllipsis_Icon, (iMenuItem[]){ - { export_Icon " ${upload.text.export}", 0, 0, "upload.text.export" }, - { "---" }, - { delete_Icon " " uiTextCaution_ColorEscape "${menu.delete}", 0, 0, "upload.text.delete" } - }, 3); + iLabelWidget *menu = new_LabelWidget(midEllipsis_Icon, "upload.editmenu.open"); setTextColor_LabelWidget(menu, uiTextAction_ColorId); setFont_LabelWidget(menu, uiLabelBigBold_FontId); addChildFlags_Widget(title, iClob(menu), frameless_WidgetFlag | moveToParentRightEdge_WidgetFlag); @@ -428,12 +426,19 @@ static iBool processEvent_UploadWidget_(iUploadWidget *d, const SDL_Event *ev) { } else if (equal_Command(cmd, "panel.changed")) { showOrHideUploadButton_UploadWidget_(d); - setFocus_Widget(NULL); + if (currentPanelIndex_Mobile(w) == 0) { + setFocus_Widget(as_Widget(d->input)); + } + else { + setFocus_Widget(NULL); + } + refresh_Widget(d->input); return iFalse; } #if defined (iPlatformAppleMobile) else if (deviceType_App() != desktop_AppDeviceType && equal_Command(cmd, "menu.opened")) { setFocus_Widget(NULL); /* overlaid text fields! */ + refresh_Widget(d->input); return iFalse; } #endif @@ -474,6 +479,44 @@ static iBool processEvent_UploadWidget_(iUploadWidget *d, const SDL_Event *ev) { updateIdentityDropdown_UploadWidget_(d); return iTrue; } + if (isCommand_Widget(w, ev, "upload.editmenu.open")) { + setFocus_Widget(NULL); + refresh_Widget(as_Widget(d->input)); + iWidget *editMenu = makeMenu_Widget(root_Widget(w), (iMenuItem[]){ + { select_Icon " ${menu.selectall}", 0, 0, "upload.text.selectall" }, + { export_Icon " ${menu.upload.export}", 0, 0, "upload.text.export" }, + { "---" }, + { delete_Icon " " uiTextCaution_ColorEscape "${menu.upload.delete}", 0, 0, "upload.text.delete" } + }, 4); + openMenu_Widget(editMenu, topLeft_Rect(bounds_Widget(as_Widget(d->input)))); + return iTrue; + } + if (isCommand_UserEvent(ev, "upload.text.export")) { +#if defined (iPlatformAppleMobile) + openTextActivityView_iOS(text_InputWidget(d->input)); +#endif + return iTrue; + } + if (isCommand_UserEvent(ev, "upload.text.delete")) { + if (argLabel_Command(command_UserEvent(ev), "confirmed")) { + setTextCStr_InputWidget(d->input, ""); + setFocus_Widget(as_Widget(d->input)); + } + else { + iWidget *confirm = makeMenu_Widget(root_Widget(w), (iMenuItem[]){ + { delete_Icon " " uiTextCaution_ColorEscape "${menu.upload.delete.confirm}", 0, 0, + "upload.text.delete confirmed:1" } + }, 1); + openMenu_Widget(confirm, zero_I2()); + } + return iTrue; + } + if (isCommand_UserEvent(ev, "upload.text.selectall")) { + setFocus_Widget(as_Widget(d->input)); + refresh_Widget(as_Widget(d->input)); + postCommand_Widget(d->input, "input.selectall"); + return iTrue; + } if (isCommand_Widget(w, ev, "upload.accept")) { iBool isText; iWidget *tabs = findChild_Widget(w, "upload.tabs"); @@ -569,11 +612,16 @@ static iBool processEvent_UploadWidget_(iUploadWidget *d, const SDL_Event *ev) { destroy_Widget(w); return iTrue; } - else if (!isUsingPanelLayout_Mobile() && isCommand_Widget(w, ev, "input.resized")) { - resizeToLargestPage_Widget(findChild_Widget(w, "upload.tabs")); - arrange_Widget(w); - refresh_Widget(w); - return iTrue; + else if (isCommand_Widget(w, ev, "input.resized")) { + if (!isUsingPanelLayout_Mobile()) { + resizeToLargestPage_Widget(findChild_Widget(w, "upload.tabs")); + arrange_Widget(w); + refresh_Widget(w); + return iTrue; + } + else { + refresh_Widget(as_Widget(d->input)); + } } else if (isCommand_Widget(w, ev, "upload.pickfile")) { #if defined (iPlatformAppleMobile) diff --git a/src/ui/util.c b/src/ui/util.c index 73fafe84..daefc279 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -671,6 +671,7 @@ static iBool isCommandIgnoredByMenus_(const char *cmd) { equal_Command(cmd, "window.reload.update") || equal_Command(cmd, "window.mouse.exited") || equal_Command(cmd, "window.mouse.entered") || + equal_Command(cmd, "input.backup") || (equal_Command(cmd, "mouse.clicked") && !arg_Command(cmd)); /* button released */ } -- cgit v1.2.3 From 812b770e2f63fa2864b88de6d7a0dbe37abf026a Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 9 Dec 2021 18:45:29 +0200 Subject: Mobile: UploadWidget allows editing URL path --- po/en.po | 3 +++ src/ios.m | 2 +- src/ui/uploadwidget.c | 32 +++++++++++++++++++++++++------- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/po/en.po b/po/en.po index 89a05939..43857d12 100644 --- a/po/en.po +++ b/po/en.po @@ -1047,6 +1047,9 @@ msgstr "MIME type:" msgid "upload.token" msgstr "Token:" +msgid "hint.upload.path" +msgstr "URL path" + msgid "hint.upload.token" msgstr "see server's instructions" diff --git a/src/ios.m b/src/ios.m index b6fbdec0..43d7ec69 100644 --- a/src/ios.m +++ b/src/ios.m @@ -850,7 +850,7 @@ void setFont_SystemTextInput(iSystemTextInput *d, int fontId) { // for (NSString *name in [UIFont fontNamesForFamilyName:@"Iosevka Term"]) { // printf("fontname: %s\n", [name cStringUsingEncoding:NSUTF8StringEncoding]); // } - font = [UIFont fontWithName:@"Iosevka-Term-Extended" size:height * 0.8f]; + font = [UIFont fontWithName:@"Iosevka-Term-Extended" size:height * 0.82f]; } else { // font = [UIFont systemFontOfSize:0.65f * height]; diff --git a/src/ui/uploadwidget.c b/src/ui/uploadwidget.c index 58106dcb..9e86741e 100644 --- a/src/ui/uploadwidget.c +++ b/src/ui/uploadwidget.c @@ -56,6 +56,7 @@ struct Impl_UploadWidget { iDocumentWidget *viewer; iGmRequest * request; iLabelWidget * info; + iInputWidget * path; iInputWidget * mime; iInputWidget * token; iLabelWidget * ident; @@ -192,14 +193,17 @@ void init_UploadWidget(iUploadWidget *d) { initPanels_Mobile(w, NULL, (iMenuItem[]){ { "title id:heading.upload" }, { "label id:upload.info" }, - { "panel id:dlg.upload.text icon:0x1f5b9 noscroll:1", 0, 0, (const void *) textItems }, - { "panel id:dlg.upload.file icon:0x1f4c1", 0, 0, (const void *) fileItems }, + { "input id:upload.path hint:hint.upload.path noheading:1 url:1 text:" }, { "heading text:${heading.upload.id}" }, { "dropdown id:upload.id icon:0x1f464 text:", 0, 0, constData_Array(makeIdentityItems_UploadWidget_(d)) }, { "input id:upload.token hint:hint.upload.token.long icon:0x1f516 text:" }, + { "padding" }, + { "panel id:dlg.upload.text icon:0x1f5b9 noscroll:1", 0, 0, (const void *) textItems }, + { "panel id:dlg.upload.file icon:0x1f4c1", 0, 0, (const void *) fileItems }, { NULL } }, actions, iElemCount(actions)); d->info = findChild_Widget(w, "upload.info"); + d->path = findChild_Widget(w, "upload.path"); d->input = findChild_Widget(w, "upload.text"); d->filePathLabel = findChild_Widget(w, "upload.filepathlabel"); d->fileSizeLabel = findChild_Widget(w, "upload.filesizelabel"); @@ -360,7 +364,10 @@ static void setUrlPort_UploadWidget_(iUploadWidget *d, const iString *url, uint1 appendRange_String(&d->url, (iRangecc){ parts.scheme.end, parts.host.end }); appendFormat_String(&d->url, ":%u", overridePort ? overridePort : titanPortForUrl_(url)); appendRange_String(&d->url, (iRangecc){ parts.path.start, constEnd_String(url) }); - setText_LabelWidget(d->info, &d->url); + const iRangecc siteRoot = urlRoot_String(&d->url); + setTextCStr_LabelWidget(d->info, cstr_Rangecc((iRangecc){ constBegin_String(&d->url), siteRoot.end })); + /* From root onwards, the URL is editable. */ + setTextCStr_InputWidget(d->path, cstr_Rangecc((iRangecc){ siteRoot.end, constEnd_String(&d->url) })); arrange_Widget(as_Widget(d)); } @@ -417,6 +424,18 @@ static void showOrHideUploadButton_UploadWidget_(iUploadWidget *d) { } } +static const iString *requestUrl_UploadWidget_(const iUploadWidget *d) { + const iRangecc siteRoot = urlRoot_String(&d->url); + iString *reqUrl = collectNew_String(); + setRange_String(reqUrl, (iRangecc){ constBegin_String(&d->url), siteRoot.end }); + const iString *path = text_InputWidget(d->path); + if (!startsWith_String(path, "/")) { + appendCStr_String(reqUrl, "/"); + } + append_String(reqUrl, path); + return reqUrl; +} + static iBool processEvent_UploadWidget_(iUploadWidget *d, const SDL_Event *ev) { iWidget *w = as_Widget(d); const char *cmd = command_UserEvent(ev); @@ -503,11 +522,10 @@ static iBool processEvent_UploadWidget_(iUploadWidget *d, const SDL_Event *ev) { setFocus_Widget(as_Widget(d->input)); } else { - iWidget *confirm = makeMenu_Widget(root_Widget(w), (iMenuItem[]){ + openMenu_Widget(makeMenu_Widget(root_Widget(w), (iMenuItem[]){ { delete_Icon " " uiTextCaution_ColorEscape "${menu.upload.delete.confirm}", 0, 0, "upload.text.delete confirmed:1" } - }, 1); - openMenu_Widget(confirm, zero_I2()); + }, 1), zero_I2()); } return iTrue; } @@ -537,7 +555,7 @@ static iBool processEvent_UploadWidget_(iUploadWidget *d, const SDL_Event *ev) { d->request = new_GmRequest(certs_App()); setSendProgressFunc_GmRequest(d->request, updateProgress_UploadWidget_); setUserData_Object(d->request, d); - setUrl_GmRequest(d->request, &d->url); + setUrl_GmRequest(d->request, requestUrl_UploadWidget_(d)); const iString *site = collectNewRange_String(urlRoot_String(&d->url)); switch (d->idMode) { case none_UploadIdentity: -- cgit v1.2.3 From 343e12429ec6b7e41edcaf9c9b05f2bb9ccb4e7d Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 9 Dec 2021 21:26:33 +0200 Subject: UploadWidget: Improved layout; path editing --- src/ui/root.c | 5 +++++ src/ui/uploadwidget.c | 43 +++++++++++++++++++++++++++++++++---------- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/src/ui/root.c b/src/ui/root.c index 08e0eafd..97879422 100644 --- a/src/ui/root.c +++ b/src/ui/root.c @@ -824,6 +824,11 @@ static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) { return iFalse; } else if (equal_Command(cmd, "navigate.focus")) { + /* The upload dialog has its own path field. */ + if (findWidget_App("upload")) { + postCommand_App("focus.set id:upload.path"); + return iTrue; + } iWidget *url = findChild_Widget(navBar, "url"); if (focus_Widget() != url) { setFocus_Widget(findChild_Widget(navBar, "url")); diff --git a/src/ui/uploadwidget.c b/src/ui/uploadwidget.c index 9e86741e..fc5cc190 100644 --- a/src/ui/uploadwidget.c +++ b/src/ui/uploadwidget.c @@ -228,11 +228,18 @@ void init_UploadWidget(iUploadWidget *d) { iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.upload}", NULL)), frameless_WidgetFlag), iTrue); - d->info = addChildFlags_Widget(w, - iClob(new_LabelWidget("", NULL)), - frameless_WidgetFlag | resizeToParentWidth_WidgetFlag | - fixedHeight_WidgetFlag); - setWrap_LabelWidget(d->info, iTrue); + iWidget *headings, *values; + /* URL path. */ { + iWidget *page = makeTwoColumns_Widget(&headings, &values); + d->path = new_InputWidget(0); + addTwoColumnDialogInputField_Widget( + headings, values, "", "upload.path", iClob(d->path)); + d->info = (iLabelWidget *) lastChild_Widget(headings); + setFont_LabelWidget(d->info, uiContent_FontId); + setTextColor_LabelWidget(d->info, uiInputTextFocused_ColorId); + addChild_Widget(w, iClob(page)); + addChild_Widget(w, iClob(makePadding_Widget(gap_UI))); + } /* Tabs for input data. */ iWidget *tabs = makeTabs_Widget(w); /* Make the tabs support vertical expansion based on content. */ { @@ -242,7 +249,6 @@ void init_UploadWidget(iUploadWidget *d) { setFlags_Widget(tabPages, resizeHeightOfChildren_WidgetFlag, iFalse); setFlags_Widget(tabPages, arrangeHeight_WidgetFlag, iTrue); } - iWidget *headings, *values; setBackgroundColor_Widget(findChild_Widget(tabs, "tabs.buttons"), uiBackgroundSidebar_ColorId); setId_Widget(tabs, "upload.tabs"); /* Text input. */ { @@ -255,7 +261,8 @@ void init_UploadWidget(iUploadWidget *d) { appendFramelessTabPage_Widget(tabs, iClob(page), "${heading.upload.text}", '1', 0); } /* File content. */ { - appendTwoColumnTabPage_Widget(tabs, "${heading.upload.file}", '2', &headings, &values); + iWidget *page = appendTwoColumnTabPage_Widget(tabs, "${heading.upload.file}", '2', &headings, &values); + setBackgroundColor_Widget(page, uiBackgroundSidebar_ColorId); addChildFlags_Widget(headings, iClob(new_LabelWidget("${upload.file.name}", NULL)), frameless_WidgetFlag); d->filePathLabel = addChildFlags_Widget(values, iClob(new_LabelWidget(uiTextAction_ColorEscape "${upload.file.drophere}", NULL)), frameless_WidgetFlag); addChildFlags_Widget(headings, iClob(new_LabelWidget("${upload.file.size}", NULL)), frameless_WidgetFlag); @@ -295,6 +302,9 @@ void init_UploadWidget(iUploadWidget *d) { } resizeToLargestPage_Widget(tabs); arrange_Widget(w); + setFixedSize_Widget(as_Widget(d->path), init_I2(width_Widget(tabs) - width_Widget(d->info), -1)); + setFixedSize_Widget(as_Widget(d->mime), init_I2(width_Widget(tabs) - 3 * gap_UI - + left_Rect(parent_Widget(d->mime)->rect), -1)); setFixedSize_Widget(as_Widget(d->token), init_I2(width_Widget(tabs) - left_Rect(parent_Widget(d->token)->rect), -1)); setFixedSize_Widget(as_Widget(d->ident), init_I2(width_Widget(d->token), lineHeight_Text(uiLabel_FontId) + 2 * gap_UI)); @@ -365,10 +375,23 @@ static void setUrlPort_UploadWidget_(iUploadWidget *d, const iString *url, uint1 appendFormat_String(&d->url, ":%u", overridePort ? overridePort : titanPortForUrl_(url)); appendRange_String(&d->url, (iRangecc){ parts.path.start, constEnd_String(url) }); const iRangecc siteRoot = urlRoot_String(&d->url); - setTextCStr_LabelWidget(d->info, cstr_Rangecc((iRangecc){ constBegin_String(&d->url), siteRoot.end })); + setTextCStr_LabelWidget(d->info, cstr_Rangecc((iRangecc){ urlHost_String(&d->url).start, + siteRoot.end })); /* From root onwards, the URL is editable. */ - setTextCStr_InputWidget(d->path, cstr_Rangecc((iRangecc){ siteRoot.end, constEnd_String(&d->url) })); - arrange_Widget(as_Widget(d)); + setTextCStr_InputWidget(d->path, + cstr_Rangecc((iRangecc){ siteRoot.end, constEnd_String(&d->url) })); + if (!cmp_String(text_InputWidget(d->path), "/")) { + setTextCStr_InputWidget(d->path, ""); /* might as well show the hint */ + } + if (isUsingPanelLayout_Mobile()) { + arrange_Widget(as_Widget(d)); /* a wrapped label */ + } + else { + setFixedSize_Widget(as_Widget(d->path), + init_I2(width_Widget(findChild_Widget(as_Widget(d), "upload.tabs")) - + width_Widget(d->info), + -1)); + } } void setUrl_UploadWidget(iUploadWidget *d, const iString *url) { -- cgit v1.2.3 From 76259953925d42568c9ac80d6bb7732f1a9475be Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 10 Dec 2021 06:40:21 +0200 Subject: Mobile: UploadWidget styling --- po/en.po | 8 ++++++++ src/ui/mobile.c | 3 +++ src/ui/uploadwidget.c | 6 ++++-- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/po/en.po b/po/en.po index 43857d12..33c86382 100644 --- a/po/en.po +++ b/po/en.po @@ -1047,6 +1047,14 @@ msgstr "MIME type:" msgid "upload.token" msgstr "Token:" +# Mobile subheading in the Upload dialog. +msgid "upload.url" +msgstr "URL" + +# Mobile subheading: buttons for entering uploaded data. +msgid "upload.content" +msgstr "Content" + msgid "hint.upload.path" msgstr "URL path" diff --git a/src/ui/mobile.c b/src/ui/mobile.c index 5a787e7b..64a5a7cc 100644 --- a/src/ui/mobile.c +++ b/src/ui/mobile.c @@ -653,6 +653,9 @@ void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) { fixedHeight_WidgetFlag | (!argLabel_Command(spec, "frame") ? frameless_WidgetFlag : 0), iTrue); + if (argLabel_Command(spec, "font")) { + setFont_LabelWidget(lab, argLabel_Command(spec, "font")); + } } else if (equal_Command(spec, "padding")) { float height = 1.5f; diff --git a/src/ui/uploadwidget.c b/src/ui/uploadwidget.c index fc5cc190..28a35668 100644 --- a/src/ui/uploadwidget.c +++ b/src/ui/uploadwidget.c @@ -192,12 +192,14 @@ void init_UploadWidget(iUploadWidget *d) { }; initPanels_Mobile(w, NULL, (iMenuItem[]){ { "title id:heading.upload" }, - { "label id:upload.info" }, + { "heading id:upload.url" }, + { format_CStr("label id:upload.info font:%d", + deviceType_App() == phone_AppDeviceType ? uiLabelBig_FontId : uiLabelMedium_FontId) }, { "input id:upload.path hint:hint.upload.path noheading:1 url:1 text:" }, { "heading text:${heading.upload.id}" }, { "dropdown id:upload.id icon:0x1f464 text:", 0, 0, constData_Array(makeIdentityItems_UploadWidget_(d)) }, { "input id:upload.token hint:hint.upload.token.long icon:0x1f516 text:" }, - { "padding" }, + { "heading id:upload.content" }, { "panel id:dlg.upload.text icon:0x1f5b9 noscroll:1", 0, 0, (const void *) textItems }, { "panel id:dlg.upload.file icon:0x1f4c1", 0, 0, (const void *) fileItems }, { NULL } -- cgit v1.2.3 From 9741611c08d0f481a3f0583419b68b36d8711d1d Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 10 Dec 2021 09:50:15 +0200 Subject: Fixed history with multiple items having the same URL If there were multiple instances of the same URL in history, only the latest one's content would be used when navigating back/forward. --- res/about/version.gmi | 3 +++ src/history.c | 32 +++++++++++++++++++------------- src/history.h | 2 +- src/ui/documentwidget.c | 30 ++++++++++++++++++++++-------- src/ui/documentwidget.h | 5 +---- 5 files changed, 46 insertions(+), 26 deletions(-) diff --git a/res/about/version.gmi b/res/about/version.gmi index 506e3ae0..625e4ccb 100644 --- a/res/about/version.gmi +++ b/res/about/version.gmi @@ -10,6 +10,9 @@ New features: * Identity toolbar menu can be used to switch between alternate identities. If you have used multiple identities on one site, this makes it more convenient to switch between them. +Fixes: +* Fixed a history caching issue: if there were multiple instances of the same URL in history, only the latest one's content would be used when navigating back/forward. + ## 1.9.2 * Windows: Use the correct version number for update checks. * Shorter label for "Mark All as Read" in Feeds sidebar actions. diff --git a/src/history.c b/src/history.c index d1a731fb..50db33dd 100644 --- a/src/history.c +++ b/src/history.c @@ -254,18 +254,26 @@ const iString *url_History(const iHistory *d, size_t pos) { return collectNew_String(); } -iRecentUrl *findUrl_History(iHistory *d, const iString *url) { +#if 0 +iRecentUrl *findUrl_History(iHistory *d, const iString *url, int timeDir) { url = canonicalUrl_String(url); +// if (!timeDir) { +// timeDir = -1; +// } lock_Mutex(d->mtx); - iReverseForEach(Array, i, &d->recent) { - if (cmpStringCase_String(url, &((iRecentUrl *) i.value)->url) == 0) { + for (size_t i = size_Array(&d->recent) - 1 - d->recentPos; i < size_Array(&d->recent); + i += timeDir) { + iRecentUrl *item = at_Array(&d->recent, i); + if (cmpStringCase_String(url, &item->url) == 0) { unlock_Mutex(d->mtx); - return i.value; + return item; /* FIXME: Returning an internal pointer; should remain locked. */ } + if (!timeDir) break; } unlock_Mutex(d->mtx); return NULL; } +#endif void replace_History(iHistory *d, const iString *url) { url = canonicalUrl_String(url); @@ -405,20 +413,18 @@ void setCachedDocument_History(iHistory *d, iGmDocument *doc, iBool openedFromSi lock_Mutex(d->mtx); iRecentUrl *item = mostRecentUrl_History(d); if (item) { - if (equal_String(url_GmDocument(doc), &item->url)) { - item->flags.openedFromSidebar = openedFromSidebar; - if (item->cachedDoc != doc) { - iRelease(item->cachedDoc); - item->cachedDoc = ref_Object(doc); - } - } #if !defined (NDEBUG) - else { - printf("[History] Not updating cached document; expecting {%s} but document URL is {%s}\n", + if (!equal_String(url_GmDocument(doc), &item->url)) { + printf("[History] Cache mismatch! Expecting data for item {%s} but document URL is {%s}\n", cstr_String(&item->url), cstr_String(url_GmDocument(doc))); } #endif + item->flags.openedFromSidebar = openedFromSidebar; + if (item->cachedDoc != doc) { + iRelease(item->cachedDoc); + item->cachedDoc = ref_Object(doc); + } } unlock_Mutex(d->mtx); } diff --git a/src/history.h b/src/history.h index 3bb3808e..bfb88cf4 100644 --- a/src/history.h +++ b/src/history.h @@ -71,7 +71,7 @@ iBool goForward_History (iHistory *); iRecentUrl *precedingLocked_History (iHistory *); /* requires manual lock/unlock! */ iRecentUrl *recentUrl_History (iHistory *, size_t pos); iRecentUrl *mostRecentUrl_History (iHistory *); -iRecentUrl *findUrl_History (iHistory *, const iString *url); +//iRecentUrl *findUrl_History (iHistory *, const iString *url, int timeDir); void clearCache_History (iHistory *); size_t pruneLeastImportant_History (iHistory *); diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index a9a0e07c..e68af4d8 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -231,6 +231,8 @@ enum iDocumentWidgetFlag { urlChanged_DocumentWidgetFlag = iBit(13), openedFromSidebar_DocumentWidgetFlag = iBit(14), drawDownloadCounter_DocumentWidgetFlag = iBit(15), + fromCache_DocumentWidgetFlag = iBit(16), /* don't write anything to cache */ + animationPlaceholder_DocumentWidgetFlag = iBit(17), /* avoid slow operations */ }; enum iDocumentLinkOrdinalMode { @@ -1142,9 +1144,11 @@ static void documentWasChanged_DocumentWidget_(iDocumentWidget *d) { } } showOrHidePinningIndicator_DocumentWidget_(d); - setCachedDocument_History(d->mod.history, - d->doc, /* keeps a ref */ - (d->flags & openedFromSidebar_DocumentWidgetFlag) != 0); + if (~d->flags & fromCache_DocumentWidgetFlag) { + setCachedDocument_History(d->mod.history, + d->doc, /* keeps a ref */ + (d->flags & openedFromSidebar_DocumentWidgetFlag) != 0); + } } void setSource_DocumentWidget(iDocumentWidget *d, const iString *source) { @@ -1743,6 +1747,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, } static void fetch_DocumentWidget_(iDocumentWidget *d) { + iAssert(~d->flags & animationPlaceholder_DocumentWidgetFlag); /* Forget the previous request. */ if (d->request) { iRelease(d->request); @@ -1756,6 +1761,7 @@ static void fetch_DocumentWidget_(iDocumentWidget *d) { d->certFlags = 0; setLinkNumberMode_DocumentWidget_(d, iFalse); d->flags &= ~drawDownloadCounter_DocumentWidgetFlag; + d->flags &= ~fromCache_DocumentWidgetFlag; d->state = fetching_RequestState; set_Atomic(&d->isRequestUpdated, iFalse); d->request = new_GmRequest(certs_App()); @@ -1805,7 +1811,8 @@ static void cacheRunGlyphs_(void *data, const iGmRun *run) { } static void cacheDocumentGlyphs_DocumentWidget_(const iDocumentWidget *d) { - if (isFinishedLaunching_App() && isExposed_Window(get_Window())) { + if (isFinishedLaunching_App() && isExposed_Window(get_Window()) && + ~d->flags & animationPlaceholder_DocumentWidgetFlag) { /* Just cache the top of the document, since this is what we usually need. */ int maxY = height_Widget(&d->widget) * 2; if (maxY == 0) { @@ -1885,6 +1892,7 @@ static void updateFromCachedResponse_DocumentWidget_(iDocumentWidget *d, float n d->doc = new_GmDocument(); resetWideRuns_DocumentWidget_(d); d->state = fetching_RequestState; + d->flags |= fromCache_DocumentWidgetFlag; /* Do the fetch. */ { d->initNormScrollY = normScrollY; /* Use the cached response data. */ @@ -1916,7 +1924,8 @@ static void updateFromCachedResponse_DocumentWidget_(iDocumentWidget *d, float n } static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) { - const iRecentUrl *recent = findUrl_History(d->mod.history, d->mod.url); + const iRecentUrl *recent = constMostRecentUrl_History(d->mod.history); + iAssert(equalCase_String(&recent->url, d->mod.url)); if (recent && recent->cachedResponse) { iChangeFlags(d->flags, openedFromSidebar_DocumentWidgetFlag, @@ -2677,6 +2686,7 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { iDocumentWidget *swipeIn = findChild_Widget(swipeParent, "swipein"); if (!swipeIn) { swipeIn = new_DocumentWidget(); + swipeIn->flags |= animationPlaceholder_DocumentWidgetFlag; setId_Widget(as_Widget(swipeIn), "swipein"); setFlags_Widget(as_Widget(swipeIn), disabled_WidgetFlag | refChildrenOffset_WidgetFlag | @@ -2718,6 +2728,7 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { /* Set up the swipe dummy. */ iWidget *swipeParent = swipeParent_DocumentWidget_(d); iDocumentWidget *target = new_DocumentWidget(); + target->flags |= animationPlaceholder_DocumentWidgetFlag; setId_Widget(as_Widget(target), "swipeout"); /* "swipeout" takes `d`'s document and goes underneath. */ target->widget.rect.pos = windowToInner_Widget(swipeParent, localToWindow_Widget(w, w->rect.pos)); @@ -2785,6 +2796,7 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { /* What was being shown in the `d` document is now being swapped to the outgoing page animation. */ iDocumentWidget *target = new_DocumentWidget(); + target->flags |= animationPlaceholder_DocumentWidgetFlag; addChildPos_Widget(swipeParent, iClob(target), back_WidgetAddPos); setId_Widget(as_Widget(target), "swipeout"); swap_DocumentWidget_(target, d->doc, d); @@ -2967,7 +2979,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) timeVerified_GmCertFlag); const iBool canTrust = ~d->certFlags & trusted_GmCertFlag && ((d->certFlags & requiredForTrust) == requiredForTrust); - const iRecentUrl *recent = findUrl_History(d->mod.history, d->mod.url); + const iRecentUrl *recent = constMostRecentUrl_History(d->mod.history); const iString *meta = &d->sourceMime; if (recent && recent->cachedResponse) { meta = &recent->cachedResponse->meta; @@ -3178,6 +3190,8 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) postProcessRequestContent_DocumentWidget_(d, iFalse); /* The response may be cached. */ if (d->request) { + iAssert(~d->flags & animationPlaceholder_DocumentWidgetFlag); + iAssert(~d->flags & fromCache_DocumentWidgetFlag); if (!equal_Rangecc(urlScheme_String(d->mod.url), "about") && (startsWithCase_String(meta_GmRequest(d->request), "text/") || !cmp_String(&d->sourceMime, mimeType_Gempub))) { @@ -5399,12 +5413,12 @@ void deserializeState_DocumentWidget(iDocumentWidget *d, iStream *ins) { void setUrlFlags_DocumentWidget(iDocumentWidget *d, const iString *url, int setUrlFlags) { iChangeFlags(d->flags, openedFromSidebar_DocumentWidgetFlag, (setUrlFlags & openedFromSidebar_DocumentWidgetSetUrlFlag) != 0); - const iBool isFromCache = (setUrlFlags & useCachedContentIfAvailable_DocumentWidgetSetUrlFlag) != 0; + const iBool allowCache = (setUrlFlags & useCachedContentIfAvailable_DocumentWidgetSetUrlFlag) != 0; setLinkNumberMode_DocumentWidget_(d, iFalse); setUrl_DocumentWidget_(d, urlFragmentStripped_String(url)); /* See if there a username in the URL. */ parseUser_DocumentWidget_(d); - if (!isFromCache || !updateFromHistory_DocumentWidget_(d)) { + if (!allowCache || !updateFromHistory_DocumentWidget_(d)) { fetch_DocumentWidget_(d); } } diff --git a/src/ui/documentwidget.h b/src/ui/documentwidget.h index 2df3392b..c97fa0ba 100644 --- a/src/ui/documentwidget.h +++ b/src/ui/documentwidget.h @@ -48,12 +48,9 @@ const iString * bookmarkTitle_DocumentWidget (const iDocumentWidget *); const iString * feedTitle_DocumentWidget (const iDocumentWidget *); int documentWidth_DocumentWidget (const iDocumentWidget *); -//iBool findCachedContent_DocumentWidget(const iDocumentWidget *, const iString *url, -// iString *mime_out, iBlock *data_out); - enum iDocumentWidgetSetUrlFlags { useCachedContentIfAvailable_DocumentWidgetSetUrlFlag = iBit(1), - openedFromSidebar_DocumentWidgetSetUrlFlag = iBit(2), + openedFromSidebar_DocumentWidgetSetUrlFlag = iBit(2), }; void setUrl_DocumentWidget (iDocumentWidget *, const iString *url); -- cgit v1.2.3 From 680b72de4185527baea76ee8ce3cc4c2145069a5 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 10 Dec 2021 10:06:46 +0200 Subject: Mobile: Minimum width for the top dialog panel The top panel should not be too narrow, and if it doesn't fit, fall back to a layered page view. --- src/ui/mobile.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/ui/mobile.c b/src/ui/mobile.c index 64a5a7cc..9bb55132 100644 --- a/src/ui/mobile.c +++ b/src/ui/mobile.c @@ -41,10 +41,18 @@ iBool isUsingPanelLayout_Mobile(void) { return deviceType_App() != desktop_AppDeviceType; } +#define sidebarMinWidth_Mobile (80 * gap_UI) + static iBool isSideBySideLayout_(void) { + /* Minimum is an even split. */ + const int safeWidth = safeRect_Root(get_Root()).size.x; + if (safeWidth / 2 < sidebarMinWidth_Mobile) { + return iFalse; + } if (deviceType_App() == phone_AppDeviceType) { return isLandscape_App(); } + /* Tablet may still be too narrow. */ return numRoots_Window(get_Window()) == 1; } @@ -128,8 +136,9 @@ static iBool mainDetailSplitHandler_(iWidget *mainDetailSplit, const char *cmd) const int pad = isPortrait ? 0 : 3 * gap_UI; if (isSideBySide) { iAssert(topPanel); - topPanel->rect.size.x = (deviceType_App() == phone_AppDeviceType ? - safeRoot.size.x * 2 / 5 : (safeRoot.size.x / 3)); + topPanel->rect.size.x = iMax(sidebarMinWidth_Mobile, + (deviceType_App() == phone_AppDeviceType ? + safeRoot.size.x * 2 / 5 : safeRoot.size.x / 3)); } if (deviceType_App() == tablet_AppDeviceType) { setPadding_Widget(topPanel, pad, 0, pad, pad); -- cgit v1.2.3 From 467498b549254041bde827b4a96fa4a1f2272764 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 10 Dec 2021 10:06:59 +0200 Subject: Cleanup --- src/ui/mobile.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/mobile.c b/src/ui/mobile.c index 9bb55132..bf3eb425 100644 --- a/src/ui/mobile.c +++ b/src/ui/mobile.c @@ -41,12 +41,12 @@ iBool isUsingPanelLayout_Mobile(void) { return deviceType_App() != desktop_AppDeviceType; } -#define sidebarMinWidth_Mobile (80 * gap_UI) +#define topPanelMinWidth_Mobile (80 * gap_UI) static iBool isSideBySideLayout_(void) { /* Minimum is an even split. */ const int safeWidth = safeRect_Root(get_Root()).size.x; - if (safeWidth / 2 < sidebarMinWidth_Mobile) { + if (safeWidth / 2 < topPanelMinWidth_Mobile) { return iFalse; } if (deviceType_App() == phone_AppDeviceType) { @@ -136,7 +136,7 @@ static iBool mainDetailSplitHandler_(iWidget *mainDetailSplit, const char *cmd) const int pad = isPortrait ? 0 : 3 * gap_UI; if (isSideBySide) { iAssert(topPanel); - topPanel->rect.size.x = iMax(sidebarMinWidth_Mobile, + topPanel->rect.size.x = iMax(topPanelMinWidth_Mobile, (deviceType_App() == phone_AppDeviceType ? safeRoot.size.x * 2 / 5 : safeRoot.size.x / 3)); } -- cgit v1.2.3 From fe3c7bb41fbb1d1db7c244e9ef0489ebd1d8e7f2 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 10 Dec 2021 11:01:22 +0200 Subject: iOS: Bumped version number; updated release notes --- CMakeLists.txt | 4 ++-- res/about/ios-version.gmi | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index dba65757..3d55057c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,8 +25,8 @@ project (Lagrange set (COPYRIGHT_YEAR 2021) if (IOS) set (PROJECT_VERSION 1.10) - set (IOS_BUNDLE_VERSION 2) - set (IOS_BUILD_DATE "2021-12-08") + set (IOS_BUNDLE_VERSION 3) + set (IOS_BUILD_DATE "2021-12-10") endif () # Defaults that depend on environment. diff --git a/res/about/ios-version.gmi b/res/about/ios-version.gmi index b1421bf9..7b0428bf 100644 --- a/res/about/ios-version.gmi +++ b/res/about/ios-version.gmi @@ -6,6 +6,16 @@ ``` # Release notes +## 1.10 (3) +* Revised layout of the Upload dialog. There is a new edit field that lets you modify the file path of the URL. Identity names are in bold. +* Added an edit actions menu in the Upload text editor (only in portrait phone layout for now). +* Fixed persistent storage of the Upload text field contents. +* Fixed input widget positioning and behavior in the Upload dialog. +* Fixed minimum width of main panel in dialogs. +* Fixed use of cached content when the same URL was found multiple times in history. Previously, only the latest page was used. +* Changed input field tint color to match chosen UI accent color. +* Optimized memory use during UI event processing. + ## 1.10 (2) * Fixed pull-to-refresh on short pages. * Fixed URL field contents not being clipped to widget bounds. -- cgit v1.2.3 From 2c20faea44bdf38c6bf5dc3b62c6ad2ecd323a74 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 10 Dec 2021 11:21:30 +0200 Subject: Mobile: Added missing Glyph Warnings option --- src/ui/util.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ui/util.c b/src/ui/util.c index daefc279..4ef50ec5 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -2480,7 +2480,8 @@ iWidget *makePreferences_Widget(void) { { "buttons id:prefs.mono", 0, 0, (const void *) monoFontItems }, { "padding" }, { "dropdown id:prefs.font.monodoc", 0, 0, (const void *) constData_Array(makeFontItems_("monodoc")) }, -// { "padding" }, + { "padding" }, + { "toggle id:prefs.font.warnmissing" }, { "heading id:prefs.gemtext.ansi" }, { "toggle id:prefs.gemtext.ansi.fg" }, { "toggle id:prefs.gemtext.ansi.bg" }, -- cgit v1.2.3 From 52518cc268c06552245852b9b60783df5ba0314d Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 10 Dec 2021 11:38:04 +0200 Subject: Mobile: Use the "Settings" label on tablet, too --- src/ui/root.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/root.c b/src/ui/root.c index 97879422..1295371d 100644 --- a/src/ui/root.c +++ b/src/ui/root.c @@ -104,7 +104,7 @@ static const iMenuItem tabletNavMenuItems_[] = { { "${menu.feeds.entrylist}", 0, 0, "!open url:about:feeds" }, //{ "${menu.downloads}", 0, 0, "downloads.open" }, { "---" }, - { gear_Icon " ${menu.preferences}", SDLK_COMMA, KMOD_PRIMARY, "preferences" }, + { gear_Icon " ${menu.settings}", SDLK_COMMA, KMOD_PRIMARY, "preferences" }, { "${menu.help}", SDLK_F1, 0, "!open url:about:help" }, { "${menu.releasenotes}", 0, 0, "!open url:about:version" }, }; -- cgit v1.2.3 From b857d5e996bde015686bc2bf19d16671c1d90f87 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 10 Dec 2021 16:09:32 +0200 Subject: Mobile: Return key behavior is not configurable --- src/ui/util.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/util.c b/src/ui/util.c index 4ef50ec5..8446416d 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -2450,7 +2450,7 @@ iWidget *makePreferences_Widget(void) { }; const iMenuItem uiPanelItems[] = { { "title id:heading.prefs.interface" }, - { "dropdown device:1 id:prefs.returnkey", 0, 0, (const void *) returnKeyBehaviors }, + { "dropdown device:0 id:prefs.returnkey", 0, 0, (const void *) returnKeyBehaviors }, { "padding device:1" }, { "toggle id:prefs.hoverlink" }, { "toggle device:2 id:prefs.hidetoolbarscroll" }, -- cgit v1.2.3 From ec88d55f71663ef711531c002434d9f55fe885f8 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 10 Dec 2021 16:09:51 +0200 Subject: LookupWidget: Minimum width for the results list --- src/ui/lookupwidget.c | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/ui/lookupwidget.c b/src/ui/lookupwidget.c index da0113ce..c654e3cf 100644 --- a/src/ui/lookupwidget.c +++ b/src/ui/lookupwidget.c @@ -658,13 +658,25 @@ static iBool processEvent_LookupWidget_(iLookupWidget *d, const SDL_Event *ev) { (equal_Command(cmd, "layout.changed") && equal_Rangecc(range_Command(cmd, "id"), "navbar"))) { /* Position the lookup popup under the URL bar. */ { - iRoot *root = w->root; + iRoot *root = w->root; + iWidget *url = findChild_Widget(root->widget, "url"); + const int minWidth = iMin(120 * gap_UI, width_Rect(safeRect_Root(root))); + const int urlWidth = width_Widget(url); + int extraWidth = 0; + if (urlWidth < minWidth) { + extraWidth = minWidth - urlWidth; + } const iRect navBarBounds = bounds_Widget(findChild_Widget(root->widget, "navbar")); - iWidget *url = findChild_Widget(root->widget, "url"); - setFixedSize_Widget(w, init_I2(width_Widget(url), - (bottom_Rect(rect_Root(root)) - bottom_Rect(navBarBounds)) / 2)); - setPos_Widget(w, windowToLocal_Widget(w, bottomLeft_Rect(bounds_Widget(url)))); -#if defined (iPlatformAppleMobile) + setFixedSize_Widget( + w, + init_I2(width_Widget(url) + extraWidth, + (bottom_Rect(rect_Root(root)) - bottom_Rect(navBarBounds)) / 2)); + setPos_Widget(w, + windowToLocal_Widget(w, + max_I2(zero_I2(), + addX_I2(bottomLeft_Rect(bounds_Widget(url)), + -extraWidth / 2)))); +#if defined(iPlatformAppleMobile) /* TODO: Check this again. */ /* Adjust height based on keyboard size. */ { w->rect.size.y = bottom_Rect(visibleRect_Root(root)) - top_Rect(bounds_Widget(w)); -- cgit v1.2.3 From 97a3dc5ef8829f744c6191d7c77681c1b2f1a07b Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 10 Dec 2021 16:23:24 +0200 Subject: iOS: Extra padding at bottom of upload text editor Long multiline editors should have a fair bit of overscroll at the bottom, as long as it doesn't affect the normal expanding editors. --- src/ios.h | 1 + src/ios.m | 3 +++ src/ui/inputwidget.c | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/ios.h b/src/ios.h index fbe7c2eb..bf742f1f 100644 --- a/src/ios.h +++ b/src/ios.h @@ -74,6 +74,7 @@ enum iSystemTextInputFlags { disableAutocapitalize_SystemTextInputFlag = iBit(6), alignRight_SystemTextInputFlag = iBit(7), insertNewlines_SystemTextInputFlag = iBit(8), + extraPadding_SystemTextInputFlag = iBit(9), }; iDeclareType(SystemTextInput) diff --git a/src/ios.m b/src/ios.m index 43d7ec69..c9e843e4 100644 --- a/src/ios.m +++ b/src/ios.m @@ -771,6 +771,9 @@ void init_SystemTextInput(iSystemTextInput *d, iRect rect, int flags) { [view setBackgroundColor:[UIColor colorWithWhite:1.0f alpha:0.0f]]; [view setTextColor:textColor]; [view setTintColor:tintColor]; + if (flags & extraPadding_SystemTextInputFlag) { + [view setContentInset:(UIEdgeInsets){ 0, 0, 3 * gap_UI / get_Window()->pixelRatio, 0}]; + } [view setEditable:YES]; [view setDelegate:appState_]; [view becomeFirstResponder]; diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index 22f983eb..18c0c20c 100644 --- a/src/ui/inputwidget.c +++ b/src/ui/inputwidget.c @@ -1179,7 +1179,9 @@ void begin_InputWidget(iInputWidget *d) { (d->maxWrapLines > 1 ? multiLine_SystemTextInputFlags : 0) | (d->inFlags & isUrl_InputWidgetFlag ? (disableAutocorrect_SystemTextInputFlag | disableAutocapitalize_SystemTextInputFlag) : 0) | + /* widget-specific tweaks (hacks) */ (!cmp_String(id_Widget(w), "url") ? returnGo_SystemTextInputFlags : 0) | + (!cmp_String(id_Widget(w), "upload.text") ? extraPadding_SystemTextInputFlag : 0) | (flags_Widget(w) & alignRight_WidgetFlag ? alignRight_SystemTextInputFlag : 0) | (isAllowedToInsertNewline_InputWidget_(d) ? insertNewlines_SystemTextInputFlag : 0) | (d->inFlags & selectAllOnFocus_InputWidgetFlag ? selectAll_SystemTextInputFlags : 0)); -- cgit v1.2.3 From 7fcbd335481a22233f63de1311bc26f8d2298930 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 11 Dec 2021 19:18:00 +0200 Subject: Cleanup --- src/ui/inputwidget.c | 29 +++++++++++++++++------------ src/ui/sidebarwidget.c | 2 -- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index 18c0c20c..00ade351 100644 --- a/src/ui/inputwidget.c +++ b/src/ui/inputwidget.c @@ -1175,16 +1175,18 @@ void begin_InputWidget(iInputWidget *d) { d->inFlags &= ~enterPressed_InputWidgetFlag; #if LAGRANGE_USE_SYSTEM_TEXT_INPUT set_String(&d->oldText, &d->text); - d->sysCtrl = new_SystemTextInput(contentBounds_InputWidget_(d), - (d->maxWrapLines > 1 ? multiLine_SystemTextInputFlags : 0) | - (d->inFlags & isUrl_InputWidgetFlag ? (disableAutocorrect_SystemTextInputFlag | - disableAutocapitalize_SystemTextInputFlag) : 0) | - /* widget-specific tweaks (hacks) */ - (!cmp_String(id_Widget(w), "url") ? returnGo_SystemTextInputFlags : 0) | - (!cmp_String(id_Widget(w), "upload.text") ? extraPadding_SystemTextInputFlag : 0) | - (flags_Widget(w) & alignRight_WidgetFlag ? alignRight_SystemTextInputFlag : 0) | - (isAllowedToInsertNewline_InputWidget_(d) ? insertNewlines_SystemTextInputFlag : 0) | - (d->inFlags & selectAllOnFocus_InputWidgetFlag ? selectAll_SystemTextInputFlags : 0)); + d->sysCtrl = new_SystemTextInput( + contentBounds_InputWidget_(d), + (d->maxWrapLines > 1 ? multiLine_SystemTextInputFlags : 0) | + (d->inFlags & isUrl_InputWidgetFlag ? (disableAutocorrect_SystemTextInputFlag | + disableAutocapitalize_SystemTextInputFlag) + : 0) | + /* widget-specific tweaks (hacks) */ + (!cmp_String(id_Widget(w), "url") ? returnGo_SystemTextInputFlags : 0) | + (!cmp_String(id_Widget(w), "upload.text") ? extraPadding_SystemTextInputFlag : 0) | + (flags_Widget(w) & alignRight_WidgetFlag ? alignRight_SystemTextInputFlag : 0) | + (isAllowedToInsertNewline_InputWidget_(d) ? insertNewlines_SystemTextInputFlag : 0) | + (d->inFlags & selectAllOnFocus_InputWidgetFlag ? selectAll_SystemTextInputFlags : 0)); setFont_SystemTextInput(d->sysCtrl, d->font); setText_SystemTextInput(d->sysCtrl, &d->oldText, iFalse); setTextChangedFunc_SystemTextInput(d->sysCtrl, systemInputChanged_InputWidget_, d); @@ -2589,8 +2591,11 @@ static void draw_InputWidget_(const iInputWidget *d) { : isFocused /*&& !isEmpty_Array(&d->lines)*/ ? uiInputTextFocused_ColorId : uiInputText_ColorId; #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT - setClip_Paint(&p, adjusted_Rect(bounds, init_I2(d->leftPadding, 0), - init_I2(-d->rightPadding, w->flags & extraPadding_WidgetFlag ? -gap_UI / 2 : 0))); + setClip_Paint(&p, + adjusted_Rect(bounds, + init_I2(d->leftPadding, 0), + init_I2(-d->rightPadding, + w->flags & extraPadding_WidgetFlag ? -gap_UI / 2 : 0))); iWrapText wrapText = { .maxWidth = d->maxLen == 0 ? width_Rect(contentBounds) : unlimitedWidth_InputWidget_, .mode = (d->inFlags & isUrl_InputWidgetFlag ? anyCharacter_WrapTextMode diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index 15274987..fa0e16ea 100644 --- a/src/ui/sidebarwidget.c +++ b/src/ui/sidebarwidget.c @@ -223,8 +223,6 @@ static iLabelWidget *addActionButton_SidebarWidget_(iSidebarWidget *d, const cha const char *command, int64_t flags) { iLabelWidget *btn = addChildFlags_Widget(d->actions, iClob(new_LabelWidget(label, command)), - //(deviceType_App() != desktop_AppDeviceType ? - // extraPadding_WidgetFlag : 0) | flags); setFont_LabelWidget(btn, actionButtonFont_SidebarWidget_(d)); checkIcon_LabelWidget(btn); -- cgit v1.2.3 From c391ce4fccf6de52b884a438329d12e76bd1a160 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 11 Dec 2021 19:18:06 +0200 Subject: Updated release notes --- res/about/version.gmi | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/res/about/version.gmi b/res/about/version.gmi index 625e4ccb..90fa2bcd 100644 --- a/res/about/version.gmi +++ b/res/about/version.gmi @@ -10,8 +10,13 @@ New features: * Identity toolbar menu can be used to switch between alternate identities. If you have used multiple identities on one site, this makes it more convenient to switch between them. +Changes and enhancements: +* Revised layout of the Upload dialog. There is a new edit field that lets you modify the file path of the URL. Identity names are in bold. +* Optimized memory use during UI event processing. + Fixes: * Fixed a history caching issue: if there were multiple instances of the same URL in history, only the latest one's content would be used when navigating back/forward. +* Fixed lookup results list becoming too narrow. ## 1.9.2 * Windows: Use the correct version number for update checks. -- cgit v1.2.3 From 8bd055049a7878f4509dff5bcd0e152890665976 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 12 Dec 2021 13:52:57 +0200 Subject: Mobile: Crash when dismissing sidebar during Edit mode --- src/ui/labelwidget.c | 21 +++++++++++++-------- src/ui/sidebarwidget.c | 17 +++++++++++------ 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/ui/labelwidget.c b/src/ui/labelwidget.c index 5ad43b0e..4ace6204 100644 --- a/src/ui/labelwidget.c +++ b/src/ui/labelwidget.c @@ -492,6 +492,7 @@ int font_LabelWidget(const iLabelWidget *d) { } void updateSize_LabelWidget(iLabelWidget *d) { + if (!d) return; iWidget *w = as_Widget(d); const int64_t flags = flags_Widget(w); const iInt2 size = defaultSize_LabelWidget(d); @@ -562,18 +563,22 @@ void setTextColor_LabelWidget(iLabelWidget *d, int color) { } void setText_LabelWidget(iLabelWidget *d, const iString *text) { - updateText_LabelWidget(d, text); - updateSize_LabelWidget(d); - if (isWrapped_LabelWidget(d)) { - sizeChanged_LabelWidget_(d); + if (d) { + updateText_LabelWidget(d, text); + updateSize_LabelWidget(d); + if (isWrapped_LabelWidget(d)) { + sizeChanged_LabelWidget_(d); + } } } void setTextCStr_LabelWidget(iLabelWidget *d, const char *text) { - updateTextCStr_LabelWidget(d, text); - updateSize_LabelWidget(d); - if (isWrapped_LabelWidget(d)) { - sizeChanged_LabelWidget_(d); + if (d) { + updateTextCStr_LabelWidget(d, text); + updateSize_LabelWidget(d); + if (isWrapped_LabelWidget(d)) { + sizeChanged_LabelWidget_(d); + } } } diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index fa0e16ea..07f1908c 100644 --- a/src/ui/sidebarwidget.c +++ b/src/ui/sidebarwidget.c @@ -251,12 +251,14 @@ static iBool isSlidingSheet_SidebarWidget_(const iSidebarWidget *d) { static void setMobileEditMode_SidebarWidget_(iSidebarWidget *d, iBool editing) { iWidget *w = as_Widget(d); d->isEditing = editing; - setFlags_Widget(findChild_Widget(w, "sidebar.close"), hidden_WidgetFlag, editing); - setFlags_Widget(child_Widget(d->actions, 0), hidden_WidgetFlag, !editing); - setTextCStr_LabelWidget(child_Widget(as_Widget(d->actions), 2), - editing ? "${sidebar.close}" : "${sidebar.action.bookmarks.edit}"); - setDragHandleWidth_ListWidget(d->list, editing ? itemHeight_ListWidget(d->list) * 3 / 2 : 0); - arrange_Widget(d->actions); + if (d->actions) { + setFlags_Widget(findChild_Widget(w, "sidebar.close"), hidden_WidgetFlag, editing); + setFlags_Widget(child_Widget(d->actions, 0), hidden_WidgetFlag, !editing); + setTextCStr_LabelWidget(child_Widget(as_Widget(d->actions), 2), + editing ? "${sidebar.close}" : "${sidebar.action.bookmarks.edit}"); + setDragHandleWidth_ListWidget(d->list, editing ? itemHeight_ListWidget(d->list) * 3 / 2 : 0); + arrange_Widget(d->actions); + } } static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepActions) { @@ -698,6 +700,9 @@ iBool setMode_SidebarWidget(iSidebarWidget *d, enum iSidebarMode mode) { d->mode == documentOutline_SidebarMode ? tmBannerBackground_ColorId : uiBackgroundSidebar_ColorId); updateItemHeight_SidebarWidget_(d); + if (deviceType_App() != desktop_AppDeviceType && mode != bookmarks_SidebarMode) { + setMobileEditMode_SidebarWidget_(d, iFalse); + } /* Restore previous scroll position. */ setScrollPos_ListWidget(list_SidebarWidget_(d), d->modeScroll[mode]); /* Title of the mobile sliding sheet. */ -- cgit v1.2.3 From d548c24208233c1db0a464270d40772359ace28b Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Mon, 13 Dec 2021 15:19:40 +0200 Subject: Mobile: Fixed initial InputWidget sizing --- src/ui/inputwidget.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index 00ade351..66419a16 100644 --- a/src/ui/inputwidget.c +++ b/src/ui/inputwidget.c @@ -734,10 +734,8 @@ static int contentHeight_InputWidget_(const iInputWidget *d) { if (d->buffered && ~d->inFlags & needUpdateBuffer_InputWidgetFlag) { return iClamp(d->buffered->size.y, minHeight, maxHeight); } - return minHeight; -#else - return (int) size_Range(&d->visWrapLines) * lineHeight; #endif + return (int) size_Range(&d->visWrapLines) * lineHeight; } static void updateTextInputRect_InputWidget_(const iInputWidget *d) { -- cgit v1.2.3 From 282ddd8d1101befb25a1c057696a2cb1743bc6b5 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Mon, 13 Dec 2021 15:20:26 +0200 Subject: DocumentWidget: Keeping a cached document When restoring a page from history, ensure the cached document is stored in the memory cache. Previously this only occurred when a page load finished. --- src/history.c | 1 + src/ui/documentwidget.c | 13 +++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/history.c b/src/history.c index 50db33dd..91416020 100644 --- a/src/history.c +++ b/src/history.c @@ -412,6 +412,7 @@ void setCachedResponse_History(iHistory *d, const iGmResponse *response) { void setCachedDocument_History(iHistory *d, iGmDocument *doc, iBool openedFromSidebar) { lock_Mutex(d->mtx); iRecentUrl *item = mostRecentUrl_History(d); + iAssert(size_GmDocument(doc).x > 0); if (item) { #if !defined (NDEBUG) if (!equal_String(url_GmDocument(doc), &item->url)) { diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index e68af4d8..bd3fe7ad 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -1881,6 +1881,7 @@ static void addBannerWarnings_DocumentWidget_(iDocumentWidget *d) { static void updateFromCachedResponse_DocumentWidget_(iDocumentWidget *d, float normScrollY, const iGmResponse *resp, iGmDocument *cachedDoc) { + iAssert(width_Widget(d) > 0); /* must be laid out by now */ setLinkNumberMode_DocumentWidget_(d, iFalse); clear_ObjectList(d->media); delete_Gempub(d->sourceGempub); @@ -1901,9 +1902,13 @@ static void updateFromCachedResponse_DocumentWidget_(iDocumentWidget *d, float n d->sourceStatus = success_GmStatusCode; format_String(&d->sourceHeader, cstr_Lang("pageinfo.header.cached")); set_Block(&d->sourceContent, &resp->body); + if (!cachedDoc) { + setWidth_GmDocument(d->doc, documentWidth_DocumentWidget_(d), width_Widget(d)); + } updateDocument_DocumentWidget_(d, resp, cachedDoc, iTrue); -// setCachedDocument_History(d->mod.history, d->doc, -// (d->flags & openedFromSidebar_DocumentWidgetFlag) != 0); +// if (!cachedDoc) { +// setCachedDocument_History(d->mod.history, d->doc, iFalse); +// } clear_Banner(d->banner); updateBanner_DocumentWidget_(d); addBannerWarnings_DocumentWidget_(d); @@ -1932,6 +1937,10 @@ static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) { recent->flags.openedFromSidebar); updateFromCachedResponse_DocumentWidget_( d, recent->normScrollY, recent->cachedResponse, recent->cachedDoc); + if (!recent->cachedDoc) { + /* We have a cached copy now. */ + setCachedDocument_History(d->mod.history, d->doc, iFalse); + } return iTrue; } else if (!isEmpty_String(d->mod.url)) { -- cgit v1.2.3 From 96c074f2eaba2cee55a7c820a5e898107ccdd49b Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 14 Dec 2021 13:21:56 +0200 Subject: Fixed missing document width updates --- src/gmdocument.c | 1 + src/ui/documentwidget.c | 1 + 2 files changed, 2 insertions(+) diff --git a/src/gmdocument.c b/src/gmdocument.c index e3c06d49..56b2f06d 100644 --- a/src/gmdocument.c +++ b/src/gmdocument.c @@ -2102,6 +2102,7 @@ void setSource_GmDocument(iGmDocument *d, const iString *source, int width, int if (size_String(source) == size_String(&d->unormSource)) { iAssert(equal_String(source, &d->unormSource)); // printf("[GmDocument] source is unchanged!\n"); + updateWidth_GmDocument(d, width, canvasWidth); return; /* Nothing to do. */ } /* Normalize and convert to Gemtext if needed. */ diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index bd3fe7ad..b610b4c5 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -1307,6 +1307,7 @@ static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode } /* Make a new document for the error page.*/ iGmDocument *errorDoc = new_GmDocument(); + setWidth_GmDocument(errorDoc, documentWidth_DocumentWidget_(d), width_Widget(d)); setUrl_GmDocument(errorDoc, d->mod.url); setFormat_GmDocument(errorDoc, gemini_SourceFormat); replaceDocument_DocumentWidget_(d, errorDoc); -- cgit v1.2.3 From e62c7408550be839a9275d026eb375fd8794225f Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 14 Dec 2021 14:19:14 +0200 Subject: Mobile: Folders in bookmark editor; menus vs. input focus --- src/ui/inputwidget.c | 4 ++++ src/ui/util.c | 25 ++++++++++++++++++++----- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index 66419a16..fe897ed4 100644 --- a/src/ui/inputwidget.c +++ b/src/ui/inputwidget.c @@ -2121,6 +2121,10 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { updateAfterVisualOffsetChange_InputWidget_(d, w->root); } } + else if (d->sysCtrl && isCommand_UserEvent(ev, "menu.opened")) { + setFocus_Widget(NULL); + return iFalse; + } #endif if (isCommand_Widget(w, ev, "focus.gained")) { begin_InputWidget(d); diff --git a/src/ui/util.c b/src/ui/util.c index 8446416d..cc11db57 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -651,7 +651,7 @@ static iBool isCommandIgnoredByMenus_(const char *cmd) { if (equal_Command(cmd, "window.focus.lost") || equal_Command(cmd, "window.focus.gained")) return iTrue; /* TODO: Perhaps a common way of indicating which commands are notifications and should not - be reacted to by menus? */ + be reacted to by menus?! */ return equal_Command(cmd, "media.updated") || equal_Command(cmd, "media.player.update") || startsWith_CStr(cmd, "feeds.update.") || @@ -672,6 +672,8 @@ static iBool isCommandIgnoredByMenus_(const char *cmd) { equal_Command(cmd, "window.mouse.exited") || equal_Command(cmd, "window.mouse.entered") || equal_Command(cmd, "input.backup") || + equal_Command(cmd, "input.ended") || + equal_Command(cmd, "focus.lost") || (equal_Command(cmd, "mouse.clicked") && !arg_Command(cmd)); /* button released */ } @@ -703,6 +705,13 @@ static iBool menuHandler_(iWidget *menu, const char *cmd) { closeMenu_Widget(menu); return iTrue; } + if (deviceType_App() == phone_AppDeviceType && equal_Command(cmd, "keyboard.changed") && + arg_Command(cmd) == 0) { + /* May need to reposition the menu. */ + menu->rect.pos = windowToLocal_Widget(menu, init_I2(left_Rect(bounds_Widget(menu)), + bottom_Rect(safeRect_Root(menu->root)) - menu->rect.size.y)); + return iFalse; + } if (!isCommandIgnoredByMenus_(cmd)) { closeMenu_Widget(menu); } @@ -2894,7 +2903,7 @@ static iBool isBookmarkFolder_(void *context, const iBookmark *bm) { return isFolder_Bookmark(bm); } -static const iArray *makeBookmarkFolderItems_(void) { +static const iArray *makeBookmarkFolderItems_(iBool withNullTerminator) { iArray *folders = new_Array(sizeof(iMenuItem)); pushBack_Array(folders, &(iMenuItem){ "\u2014", 0, 0, "dlg.bookmark.setfolder arg:0" }); iConstForEach( @@ -2915,6 +2924,9 @@ static const iArray *makeBookmarkFolderItems_(void) { 0, format_CStr("dlg.bookmark.setfolder arg:%u", id_Bookmark(bm)) }); } + if (withNullTerminator) { + pushBack_Array(folders, &(iMenuItem){ NULL }); + } return collect_Array(folders); } @@ -2924,6 +2936,7 @@ iWidget *makeBookmarkEditor_Widget(void) { { uiTextCaution_ColorEscape "${dlg.bookmark.save}", SDLK_RETURN, KMOD_PRIMARY, "bmed.accept" } }; if (isUsingPanelLayout_Mobile()) { + const iArray *folderItems = makeBookmarkFolderItems_(iTrue); const iMenuItem items[] = { { "title id:bmed.heading text:${heading.bookmark.edit}" }, { "heading id:dlg.bookmark.url" }, @@ -2932,6 +2945,8 @@ iWidget *makeBookmarkEditor_Widget(void) { { "input id:bmed.title text:${dlg.bookmark.title}" }, { "input id:bmed.tags text:${dlg.bookmark.tags}" }, { "input id:bmed.icon maxlen:1 text:${dlg.bookmark.icon}" }, + { "padding" }, + { "dropdown id:bmed.folder text:${dlg.bookmark.folder}", 0, 0, (const void *) constData_Array(folderItems) }, { "heading text:${heading.bookmark.tags}" }, { "toggle id:bmed.tag.home text:${bookmark.tag.home}" }, { "toggle id:bmed.tag.remote text:${bookmark.tag.remote}" }, @@ -2956,7 +2971,7 @@ iWidget *makeBookmarkEditor_Widget(void) { setUrlContent_InputWidget(inputs[1], iTrue); /* Folder to add to. */ { addChild_Widget(headings, iClob(makeHeading_Widget("${dlg.bookmark.folder}"))); - const iArray *folderItems = makeBookmarkFolderItems_(); + const iArray *folderItems = makeBookmarkFolderItems_(iFalse); iLabelWidget *folderButton; setId_Widget(addChildFlags_Widget(values, iClob(folderButton = makeMenuButton_LabelWidget( @@ -3153,13 +3168,13 @@ iWidget *makeFeedSettings_Widget(uint32_t bookmarkId) { setText_InputWidget(findChild_Widget(dlg, "feedcfg.title"), bm ? &bm->title : feedTitle_DocumentWidget(document_App())); setFlags_Widget(findChild_Widget(dlg, - bm->flags & headings_BookmarkFlag + bm && bm->flags & headings_BookmarkFlag ? "feedcfg.type.headings" : "feedcfg.type.gemini"), selected_WidgetFlag, iTrue); setToggle_Widget(findChild_Widget(dlg, "feedcfg.ignoreweb"), - bm->flags & ignoreWeb_BookmarkFlag); + bm && bm->flags & ignoreWeb_BookmarkFlag); setCommandHandler_Widget(dlg, handleFeedSettingCommands_); } setupSheetTransition_Mobile(dlg, incoming_TransitionFlag); -- cgit v1.2.3 From c6dc83b3496edf5617ee3ce29f56b84b32c9cc14 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 14 Dec 2021 14:19:51 +0200 Subject: Text: Warning about unchecked FriBidi return value --- src/ui/text.c | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/ui/text.c b/src/ui/text.c index 94569a6a..7367e6c0 100644 --- a/src/ui/text.c +++ b/src/ui/text.c @@ -790,6 +790,7 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh pushBack_Array(&d->logicalToSourceOffset, &(int){ ch - d->source.start }); ch += len; } + iBool bidiOk = iFalse; #if defined (LAGRANGE_ENABLE_FRIBIDI) /* Use FriBidi to reorder the codepoints. */ resize_Array(&d->visual, length); @@ -797,25 +798,25 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh resize_Array(&d->visualToLogical, length); d->bidiLevels = length ? malloc(length) : NULL; FriBidiParType baseDir = (FriBidiParType) FRIBIDI_TYPE_ON; - /* TODO: If this returns zero (error occurred), act like everything is LTR. */ - fribidi_log2vis(constData_Array(&d->logical), - length, - &baseDir, - data_Array(&d->visual), - data_Array(&d->logicalToVisual), - data_Array(&d->visualToLogical), - (FriBidiLevel *) d->bidiLevels); + bidiOk = fribidi_log2vis(constData_Array(&d->logical), + (FriBidiStrIndex) length, + &baseDir, + data_Array(&d->visual), + data_Array(&d->logicalToVisual), + data_Array(&d->visualToLogical), + (FriBidiLevel *) d->bidiLevels) > 0; d->isBaseRTL = (overrideBaseDir == 0 ? FRIBIDI_IS_RTL(baseDir) : (overrideBaseDir < 0)); -#else - /* 1:1 mapping. */ - setCopy_Array(&d->visual, &d->logical); - resize_Array(&d->logicalToVisual, length); - for (size_t i = 0; i < length; i++) { - set_Array(&d->logicalToVisual, i, &(int){ i }); - } - setCopy_Array(&d->visualToLogical, &d->logicalToVisual); - d->isBaseRTL = iFalse; #endif + if (!bidiOk) { + /* 1:1 mapping. */ + setCopy_Array(&d->visual, &d->logical); + resize_Array(&d->logicalToVisual, length); + for (size_t i = 0; i < length; i++) { + set_Array(&d->logicalToVisual, i, &(int){ i }); + } + setCopy_Array(&d->visualToLogical, &d->logicalToVisual); + d->isBaseRTL = iFalse; + } } /* The mapping needs to include the terminating NULL position. */ { pushBack_Array(&d->logicalToSourceOffset, &(int){ d->source.end - d->source.start }); -- cgit v1.2.3 From 9069f757dead4096d75b42e8090313c101b57da2 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 14 Dec 2021 15:06:30 +0200 Subject: Touch: Notify about speed at end of swipe --- src/ui/touch.c | 12 ++++++++++-- src/ui/touch.h | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/ui/touch.c b/src/ui/touch.c index aee5a383..0749bc7c 100644 --- a/src/ui/touch.c +++ b/src/ui/touch.c @@ -256,6 +256,11 @@ static iFloat3 gestureVector_Touch_(const iTouch *d) { return sub_F3(d->pos[0], d->pos[lastIndex]); } +static uint32_t gestureSpan_Touch_(const iTouch *d) { + const size_t lastIndex = iMin(d->posCount - 1, lastIndex_Touch_); + return d->posTime[0] - d->posTime[lastIndex]; +} + static void update_TouchState_(void *ptr) { iWindow *win = get_Window(); const iWidget *oldHover = win->hover; @@ -668,11 +673,14 @@ iBool processEvent_Touch(const SDL_Event *ev) { #endif if (touch->edge && !isStationary_Touch_(touch)) { const iFloat3 gesture = gestureVector_Touch_(touch); + const uint32_t duration = gestureSpan_Touch_(touch); const float pixel = window->pixelRatio; const int moveDir = x_F3(gesture) < -pixel ? -1 : x_F3(gesture) > pixel ? +1 : 0; const int didAbort = (touch->edge == left_TouchEdge && moveDir < 0) || (touch->edge == right_TouchEdge && moveDir > 0); - postCommandf_App("edgeswipe.ended abort:%d side:%d id:%llu", didAbort, touch->edge, touch->id); + postCommandf_App("edgeswipe.ended abort:%d side:%d id:%llu speed:%d", didAbort, + touch->edge, touch->id, + (int) (duration > 0 ? length_F3(gesture) / (duration / 1000.0f) : 0)); remove_ArrayIterator(&i); continue; } @@ -696,8 +704,8 @@ iBool processEvent_Touch(const SDL_Event *ev) { } /* Edge swipes do not generate momentum. */ const size_t lastIndex = iMin(touch->posCount - 1, lastIndex_Touch_); - const uint32_t duration = nowTime - touch->startTime; const iFloat3 gestureVector = sub_F3(pos, touch->pos[lastIndex]); + const uint32_t duration = nowTime - touch->startTime; iFloat3 velocity = zero_F3(); #if 0 if (touch->edge && fabsf(2 * x_F3(gestureVector)) > fabsf(y_F3(gestureVector)) && diff --git a/src/ui/touch.h b/src/ui/touch.h index c9c76d86..9c45fcb1 100644 --- a/src/ui/touch.h +++ b/src/ui/touch.h @@ -36,7 +36,7 @@ enum iWidgetTouchMode { iBool processEvent_Touch (const SDL_Event *); void update_Touch (void); -float stopWidgetMomentum_Touch (const iWidget *widget); +float stopWidgetMomentum_Touch (const iWidget *widget); /* pixels per second */ enum iWidgetTouchMode widgetMode_Touch (const iWidget *widget); void widgetDestroyed_Touch (iWidget *widget); void transferAffinity_Touch (iWidget *src, iWidget *dst); -- cgit v1.2.3 From d3595f9c51adf498e9d3b3cec4e9a3849bf56311 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 14 Dec 2021 15:06:38 +0200 Subject: Widget: Checking if visual offset applies --- src/ui/widget.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ui/widget.c b/src/ui/widget.c index 9f1c8640..0c3b1c8a 100644 --- a/src/ui/widget.c +++ b/src/ui/widget.c @@ -1980,6 +1980,9 @@ iBool isAffectedByVisualOffset_Widget(const iWidget *d) { if (w->flags & visualOffset_WidgetFlag) { return iTrue; } + if (visualOffsetByReference_Widget(w) != 0) { + return iTrue; + } } return iFalse; } -- cgit v1.2.3 From 51aaadbf5918abf6b0c05976f0cd5e1211a708ab Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 14 Dec 2021 15:07:11 +0200 Subject: SidebarWidget: Phone needs no document resize --- src/ui/sidebarwidget.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index 07f1908c..dcce724a 100644 --- a/src/ui/sidebarwidget.c +++ b/src/ui/sidebarwidget.c @@ -1231,7 +1231,9 @@ static iBool handleSidebarCommand_SidebarWidget_(iSidebarWidget *d, const char * arrange_Widget(w->parent); /* BUG: Rearranging because the arrange above didn't fully resolve the height. */ arrange_Widget(w); - updateSize_DocumentWidget(document_App()); + if (!isPortraitPhone_App()) { + updateSize_DocumentWidget(document_App()); + } if (isVisible_Widget(w)) { updateItems_SidebarWidget_(d); scrollOffset_ListWidget(d->list, 0); -- cgit v1.2.3 From 649986b3832403f119a0a615c74c31a158bec646 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 14 Dec 2021 15:07:25 +0200 Subject: Mobile: Tuning document swipe animations --- src/ui/documentwidget.c | 40 +++++++++++++++++++++++++++++++++++++--- src/ui/sidebarwidget.c | 6 +++--- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index b610b4c5..1aadb6bb 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -233,6 +233,7 @@ enum iDocumentWidgetFlag { drawDownloadCounter_DocumentWidgetFlag = iBit(15), fromCache_DocumentWidgetFlag = iBit(16), /* don't write anything to cache */ animationPlaceholder_DocumentWidgetFlag = iBit(17), /* avoid slow operations */ + invalidationPending_DocumentWidgetFlag = iBit(18), /* invalidate as soon as convenient */ }; enum iDocumentLinkOrdinalMode { @@ -261,6 +262,7 @@ struct Impl_DocumentWidget { iInt2 contextPos; /* coordinates of latest right click */ int pinchZoomInitial; int pinchZoomPosted; + float swipeSpeed; /* points/sec */ iString pendingGotoHeading; /* Network request: */ @@ -1081,8 +1083,17 @@ static void invalidate_DocumentWidget_(iDocumentWidget *d) { if (flags_Widget(as_Widget(d)) & destroyPending_WidgetFlag) { return; } + if (d->flags & invalidationPending_DocumentWidgetFlag) { + return; + } + if (isAffectedByVisualOffset_Widget(as_Widget(d))) { + d->flags |= invalidationPending_DocumentWidgetFlag; + return; + } + d->flags &= ~invalidationPending_DocumentWidgetFlag; invalidate_VisBuf(d->visBuf); clear_PtrSet(d->invalidRuns); +// printf("[%p] '%s' invalidated\n", d, cstr_String(id_Widget(as_Widget(d)))); } static iRangecc siteText_DocumentWidget_(const iDocumentWidget *d) { @@ -2157,6 +2168,7 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) { iGmResponse *resp = lockResponse_GmRequest(d->request); if (d->state == fetching_RequestState) { d->state = receivedPartialResponse_RequestState; + d->flags &= ~fromCache_DocumentWidgetFlag; updateTrust_DocumentWidget_(d, resp); if (isSuccess_GmStatusCode(statusCode)) { clear_Banner(d->banner); @@ -2309,6 +2321,7 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) { } } else if (d->state == receivedPartialResponse_RequestState) { + d->flags &= ~fromCache_DocumentWidgetFlag; switch (category_GmStatusCode(statusCode)) { case categorySuccess_GmStatusCode: /* More content available. */ @@ -2664,8 +2677,13 @@ static void setupSwipeOverlay_DocumentWidget_(iDocumentWidget *d, iWidget *overl setFlags_Widget(as_Widget(d), refChildrenOffset_WidgetFlag, iTrue); as_Widget(d)->offsetRef = swipeParent; /* `overlay` animates off the screen to the right. */ - setVisualOffset_Widget(overlay, value_Anim(&w->visualOffset), 0, 0); - setVisualOffset_Widget(overlay, width_Widget(overlay), 150, 0); + const int fromPos = value_Anim(&w->visualOffset); + const int toPos = width_Widget(overlay); + setVisualOffset_Widget(overlay, fromPos, 0, 0); + float swipe = iClamp(d->swipeSpeed, 400, 1000) * gap_UI; + uint32_t span = ((toPos - fromPos) / swipe) * 1000; +// printf("from:%d to:%d swipe:%f span:%u\n", fromPos, toPos, d->swipeSpeed, span); + setVisualOffset_Widget(overlay, toPos, span, 0); setVisualOffset_Widget(w, 0, 0, 0); } @@ -2791,12 +2809,13 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { return iTrue; } setFlags_Widget(w, dragged_WidgetFlag, iFalse); - setVisualOffset_Widget(w, 0, 150, 0); + setVisualOffset_Widget(w, 0, 250, easeOut_AnimFlag | softer_AnimFlag); return iTrue; } if (equal_Command(cmd, "edgeswipe.ended") && argLabel_Command(cmd, "side") == 1) { iWidget *swipeParent = swipeParent_DocumentWidget_(d); iDocumentWidget *swipeIn = findChild_Widget(swipeParent, "swipein"); + d->swipeSpeed = argLabel_Command(cmd, "speed") / gap_UI; /* "swipe.back" will soon follow. The `d` document will do the actual back navigation, switching immediately to a cached page. However, if one is not available, we'll need to show a blank page for a while. */ @@ -2858,6 +2877,9 @@ static iBool cancelRequest_DocumentWidget_(iDocumentWidget *d, iBool postBack) { static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) { iWidget *w = as_Widget(d); if (equal_Command(cmd, "document.openurls.changed")) { + if (d->flags & animationPlaceholder_DocumentWidgetFlag) { + return iFalse; + } /* When any tab changes its document URL, update the open link indicators. */ if (updateOpenURLs_GmDocument(d->doc)) { invalidate_DocumentWidget_(d); @@ -3179,6 +3201,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) } else if (equalWidget_Command(cmd, w, "document.request.finished") && id_GmRequest(d->request) == argU32Label_Command(cmd, "reqid")) { + d->flags &= ~fromCache_DocumentWidgetFlag; set_Block(&d->sourceContent, body_GmRequest(d->request)); if (!isSuccess_GmStatusCode(status_GmRequest(d->request))) { /* TODO: Why is this here? Can it be removed? */ @@ -5149,6 +5172,16 @@ static void prerender_DocumentWidget_(iAny *context) { } } +static void checkPendingInvalidation_DocumentWidget_(const iDocumentWidget *d) { + if (d->flags & invalidationPending_DocumentWidgetFlag && + !isAffectedByVisualOffset_Widget(constAs_Widget(d))) { +// printf("%p visoff: %d\n", d, left_Rect(bounds_Widget(w)) - left_Rect(boundsWithoutVisualOffset_Widget(w))); + iDocumentWidget *m = (iDocumentWidget *) d; /* Hrrm, not const... */ + m->flags &= ~invalidationPending_DocumentWidgetFlag; + invalidate_DocumentWidget_(m); + } +} + static void draw_DocumentWidget_(const iDocumentWidget *d) { const iWidget *w = constAs_Widget(d); const iRect bounds = bounds_Widget(w); @@ -5157,6 +5190,7 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) { if (width_Rect(bounds) <= 0) { return; } + checkPendingInvalidation_DocumentWidget_(d); /* Each document has its own palette, but the drawing routines rely on a global one. As we're now drawing a document, ensure that the right palette is in effect. Document theme colors can be used elsewhere, too, but first a document's palette diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index dcce724a..26473322 100644 --- a/src/ui/sidebarwidget.c +++ b/src/ui/sidebarwidget.c @@ -1809,14 +1809,14 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) enum iWidgetTouchMode touchMode = widgetMode_Touch(w); if (touchMode == momentum_WidgetTouchMode) { /* We don't do momentum. */ - float swipe = stopWidgetMomentum_Touch(w); + float swipe = stopWidgetMomentum_Touch(w) / gap_UI; // printf("swipe: %f\n", swipe); const iRangei midRegion = SlidingSheetMiddleRegion_SidebarWidget_(d); const int pos = top_Rect(w->rect); - if (swipe < 500) { + if (swipe < 170) { gotoNearestSlidingSheetPos_SidebarWidget_(d); } - else if (swipe > 6500 && ev->wheel.y > 0) { + else if (swipe > 500 && ev->wheel.y > 0) { /* Fast swipe down will dismiss. */ setSlidingSheetPos_SidebarWidget_(d, bottom_SlidingSheetPos); } -- cgit v1.2.3 From 0ea78766ba6d189cb70c94acc798c4f4c74be935 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 14 Dec 2021 16:43:10 +0200 Subject: Widget: More efficient size change notifications Only notify once, after the arrangement is done. --- src/ui/widget.c | 26 ++++++++++++++++++++------ src/ui/widget.h | 1 + 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/ui/widget.c b/src/ui/widget.c index 0c3b1c8a..8cb2cf02 100644 --- a/src/ui/widget.c +++ b/src/ui/widget.c @@ -126,6 +126,7 @@ void init_Widget(iWidget *d) { d->flags = 0; d->flags2 = 0; d->rect = zero_Rect(); + d->oldSize = zero_I2(); d->minSize = zero_I2(); d->sizeRef = NULL; d->offsetRef = NULL; @@ -420,9 +421,10 @@ static iBool setWidth_Widget_(iWidget *d, int width) { if (d->rect.size.x != width) { d->rect.size.x = width; TRACE(d, "width has changed to %d", width); - if (class_Widget(d)->sizeChanged) { - class_Widget(d)->sizeChanged(d); - } +// if (~d->flags2 & undefinedWidth_WidgetFlag2 && class_Widget(d)->sizeChanged) { +// class_Widget(d)->sizeChanged(d); +// } +// d->flags2 &= ~undefinedWidth_WidgetFlag2; return iTrue; } } @@ -443,9 +445,10 @@ static iBool setHeight_Widget_(iWidget *d, int height) { if (d->rect.size.y != height) { d->rect.size.y = height; TRACE(d, "height has changed to %d", height); - if (class_Widget(d)->sizeChanged) { - class_Widget(d)->sizeChanged(d); - } +// if (~d->flags2 & undefinedHeight_WidgetFlag2 && class_Widget(d)->sizeChanged) { +// class_Widget(d)->sizeChanged(d); +// } +// d->flags2 &= ~undefinedHeight_WidgetFlag2; return iTrue; } } @@ -842,6 +845,7 @@ static void arrange_Widget_(iWidget *d) { } static void resetArrangement_Widget_(iWidget *d) { + d->oldSize = d->rect.size; if (d->flags & resizeToParentWidth_WidgetFlag) { d->rect.size.x = 0; } @@ -878,6 +882,15 @@ static void resetArrangement_Widget_(iWidget *d) { } } +static void notifySizeChanged_Widget_(iWidget *d) { + if (class_Widget(d)->sizeChanged && !isEqual_I2(d->rect.size, d->oldSize)) { + class_Widget(d)->sizeChanged(d); + } + iForEach(ObjectList, child, d->children) { + notifySizeChanged_Widget_(child.object); + } +} + void arrange_Widget(iWidget *d) { if (d) { #if !defined (NDEBUG) @@ -887,6 +900,7 @@ void arrange_Widget(iWidget *d) { #endif resetArrangement_Widget_(d); /* back to initial default sizes */ arrange_Widget_(d); + notifySizeChanged_Widget_(d); } } diff --git a/src/ui/widget.h b/src/ui/widget.h index 9c4c44fb..4ab8d602 100644 --- a/src/ui/widget.h +++ b/src/ui/widget.h @@ -146,6 +146,7 @@ struct Impl_Widget { int64_t flags; int flags2; iRect rect; + iInt2 oldSize; /* in previous arrangement; for notification */ iInt2 minSize; iWidget * sizeRef; iWidget * offsetRef; -- cgit v1.2.3 From 282acaeadd5a9c003b0ce6a55fceb61e915ad43d Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 14 Dec 2021 16:43:51 +0200 Subject: SidebarWidget: Removed unnecessary update No need to update sidebar items when the Preferences tab changes. --- src/ui/sidebarwidget.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index 26473322..fcc1d807 100644 --- a/src/ui/sidebarwidget.c +++ b/src/ui/sidebarwidget.c @@ -1350,7 +1350,7 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) } else if (ev->type == SDL_USEREVENT && ev->user.code == command_UserEventCode) { const char *cmd = command_UserEvent(ev); - if (equal_Command(cmd, "tabs.changed") || equal_Command(cmd, "document.changed")) { + if (startsWith_CStr(cmd, "tabs.changed id:doc") || equal_Command(cmd, "document.changed")) { updateItems_SidebarWidget_(d); scrollOffset_ListWidget(d->list, 0); } -- cgit v1.2.3 From b65c60d2419095c2010b6aeea16887967d7c9a8c Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 14 Dec 2021 16:45:31 +0200 Subject: Root: More efficient input size updates Input widgets may resize themselves, and Preferences sends a ton of these notifications. Only rearrange a single time afterwards, not individually after each notification. --- src/app.c | 7 +++++-- src/ui/root.c | 8 ++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/app.c b/src/app.c index 8cdb4903..28e320fe 100644 --- a/src/app.c +++ b/src/app.c @@ -1949,8 +1949,11 @@ static iBool handlePrefsCommands_(iWidget *d, const char *cmd) { } } else if (equalWidget_Command(cmd, d, "input.resized")) { - updatePreferencesLayout_Widget(d); - return iFalse; + if (!d->root->pendingArrange) { + d->root->pendingArrange = iTrue; + postCommand_Root(d->root, "root.arrange"); + } + return iTrue; } return iFalse; } diff --git a/src/ui/root.c b/src/ui/root.c index 1295371d..a9bc7feb 100644 --- a/src/ui/root.c +++ b/src/ui/root.c @@ -538,6 +538,14 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) { #endif return iFalse; } + else if (equal_Command(cmd, "root.arrange")) { + iWidget *prefs = findWidget_Root("prefs"); + if (prefs) { + updatePreferencesLayout_Widget(prefs); + } + root->root->pendingArrange = iFalse; + return iTrue; + } else if (handleCommand_App(cmd)) { return iTrue; } -- cgit v1.2.3 From 5a6f7804d9a486ee0dd1e04df18cd40c4639c1ce Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 14 Dec 2021 17:00:51 +0200 Subject: Audio: Initialize SDL audio only when actually playing Playing audio is somewhat uncommon, so there is no need to have it set up always. --- src/audio/player.c | 21 +++++++++++++++++++++ src/main.c | 3 --- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/audio/player.c b/src/audio/player.c index 94bcd065..bf853e3f 100644 --- a/src/audio/player.c +++ b/src/audio/player.c @@ -31,6 +31,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include #include #include +#include #if defined (LAGRANGE_ENABLE_MPG123) # include @@ -739,6 +740,22 @@ size_t sourceDataSize_Player(const iPlayer *d) { return size; } +static iBool setupSDLAudio_(iBool init) { + static iBool isAudioInited_ = iFalse; + if (init) { + if (SDL_InitSubSystem(SDL_INIT_AUDIO)) { + fprintf(stderr, "[SDL] audio init failed: %s\n", SDL_GetError()); + return iFalse; + } + isAudioInited_ = iTrue; + } + else if (isAudioInited_) { + SDL_QuitSubSystem(SDL_INIT_AUDIO); + isAudioInited_ = iFalse; + } + return isAudioInited_; +} + iBool start_Player(iPlayer *d) { if (isStarted_Player(d)) { return iFalse; @@ -757,6 +774,9 @@ iBool start_Player(iPlayer *d) { } content.output.callback = writeOutputSamples_Player_; content.output.userdata = d; + if (!setupSDLAudio_(iTrue)) { + return iFalse; + } d->device = SDL_OpenAudioDevice(NULL, SDL_FALSE /* playback */, &content.output, &d->spec, 0); if (!d->device) { return iFalse; @@ -796,6 +816,7 @@ void stop_Player(iPlayer *d) { d->device = 0; delete_Decoder(d->decoder); d->decoder = NULL; + setupSDLAudio_(iFalse); } } diff --git a/src/main.c b/src/main.c index 6e5e99e9..cb5e060b 100644 --- a/src/main.c +++ b/src/main.c @@ -80,9 +80,6 @@ int main(int argc, char **argv) { fprintf(stderr, "[SDL] init failed: %s\n", SDL_GetError()); return -1; } - if (SDL_Init(SDL_INIT_AUDIO)) { - fprintf(stderr, "[SDL] audio init failed: %s\n", SDL_GetError()); - } init_Updater(); run_App(argc, argv); SDL_Quit(); -- cgit v1.2.3 From bb561bcec3d386801c6b05e5565d76acc7786f3d Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 14 Dec 2021 17:13:05 +0200 Subject: App: Prevent state corruption due to interrupted file write --- src/app.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/app.c b/src/app.c index 28e320fe..828f5097 100644 --- a/src/app.c +++ b/src/app.c @@ -105,6 +105,7 @@ static const char *defaultDataDir_App_ = "~/config/settings/lagrange"; static const char *prefsFileName_App_ = "prefs.cfg"; static const char *oldStateFileName_App_ = "state.binary"; static const char *stateFileName_App_ = "state.lgr"; +static const char *tempStateFileName_App_ = "state.lgr.tmp"; static const char *defaultDownloadDir_App_ = "~/Downloads"; static const int idleThreshold_App_ = 1000; /* ms */ @@ -554,7 +555,7 @@ static void saveState_App_(const iApp *d) { navigation history, cached content) and depends closely on the widget tree. The data is largely not reorderable and should not be modified by the user manually. */ - iFile *f = newCStr_File(concatPath_CStr(dataDir_App_(), stateFileName_App_)); + iFile *f = newCStr_File(concatPath_CStr(dataDir_App_(), tempStateFileName_App_)); if (open_File(f, writeOnly_FileMode)) { writeData_File(f, magicState_App_, 4); writeU32_File(f, latest_FileVersion); /* version */ @@ -596,11 +597,19 @@ static void saveState_App_(const iApp *d) { write8_File(f, flags); serializeState_DocumentWidget(i.object, stream_File(f)); } + iRelease(f); } else { + iRelease(f); fprintf(stderr, "[App] failed to save state: %s\n", strerror(errno)); + return; } - iRelease(f); + /* Copy it over to the real file. This avoids truncation if the app for any reason crashes + before the state file is fully written. */ + const char *tempName = concatPath_CStr(dataDir_App_(), tempStateFileName_App_); + const char *finalName = concatPath_CStr(dataDir_App_(), stateFileName_App_); + remove(finalName); + rename(tempName, finalName); } #if defined (LAGRANGE_ENABLE_IDLE_SLEEP) -- cgit v1.2.3 From b7f3aaca606e7e80f9b3b55fd5c4fe73fd050426 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 14 Dec 2021 17:13:37 +0200 Subject: Recompiled language strings --- res/lang/cs.bin | Bin 31255 -> 31435 bytes res/lang/de.bin | Bin 30262 -> 30442 bytes res/lang/en.bin | Bin 26359 -> 26539 bytes res/lang/eo.bin | Bin 25313 -> 25493 bytes res/lang/es.bin | Bin 30086 -> 30266 bytes res/lang/es_MX.bin | Bin 27418 -> 27598 bytes res/lang/fi.bin | Bin 29919 -> 30099 bytes res/lang/fr.bin | Bin 31066 -> 31246 bytes res/lang/gl.bin | Bin 29271 -> 29451 bytes res/lang/hu.bin | Bin 31091 -> 31271 bytes res/lang/ia.bin | Bin 28418 -> 28598 bytes res/lang/ie.bin | Bin 29006 -> 29186 bytes res/lang/isv.bin | Bin 25079 -> 25259 bytes res/lang/pl.bin | Bin 29694 -> 29874 bytes res/lang/ru.bin | Bin 44454 -> 44634 bytes res/lang/sk.bin | Bin 25415 -> 25595 bytes res/lang/sr.bin | Bin 43880 -> 44060 bytes res/lang/tok.bin | Bin 27128 -> 27308 bytes res/lang/tr.bin | Bin 29312 -> 29492 bytes res/lang/uk.bin | Bin 43799 -> 43979 bytes res/lang/zh_Hans.bin | Bin 25313 -> 25493 bytes res/lang/zh_Hant.bin | Bin 25511 -> 25691 bytes 22 files changed, 0 insertions(+), 0 deletions(-) diff --git a/res/lang/cs.bin b/res/lang/cs.bin index 488dec0a..3a5b5820 100644 Binary files a/res/lang/cs.bin and b/res/lang/cs.bin differ diff --git a/res/lang/de.bin b/res/lang/de.bin index bc05cc18..57a6c945 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 3998160c..6c921d9b 100644 Binary files a/res/lang/en.bin and b/res/lang/en.bin differ diff --git a/res/lang/eo.bin b/res/lang/eo.bin index 1bc766b1..b0ad3a58 100644 Binary files a/res/lang/eo.bin and b/res/lang/eo.bin differ diff --git a/res/lang/es.bin b/res/lang/es.bin index 937cfaf2..61a5b870 100644 Binary files a/res/lang/es.bin and b/res/lang/es.bin differ diff --git a/res/lang/es_MX.bin b/res/lang/es_MX.bin index db886661..6658bbcb 100644 Binary files a/res/lang/es_MX.bin and b/res/lang/es_MX.bin differ diff --git a/res/lang/fi.bin b/res/lang/fi.bin index 370ddf15..287de822 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 a5785ffe..495a1b30 100644 Binary files a/res/lang/fr.bin and b/res/lang/fr.bin differ diff --git a/res/lang/gl.bin b/res/lang/gl.bin index 8c8e7b84..efafd3a1 100644 Binary files a/res/lang/gl.bin and b/res/lang/gl.bin differ diff --git a/res/lang/hu.bin b/res/lang/hu.bin index c9cc4704..e0095569 100644 Binary files a/res/lang/hu.bin and b/res/lang/hu.bin differ diff --git a/res/lang/ia.bin b/res/lang/ia.bin index a6465eeb..fb4f482c 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 c1b7e6e6..d397cdea 100644 Binary files a/res/lang/ie.bin and b/res/lang/ie.bin differ diff --git a/res/lang/isv.bin b/res/lang/isv.bin index 23a6ca82..012e624f 100644 Binary files a/res/lang/isv.bin and b/res/lang/isv.bin differ diff --git a/res/lang/pl.bin b/res/lang/pl.bin index eadd2cfe..60b5d77c 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 e196e341..8cda6497 100644 Binary files a/res/lang/ru.bin and b/res/lang/ru.bin differ diff --git a/res/lang/sk.bin b/res/lang/sk.bin index 49a06577..9a5355d9 100644 Binary files a/res/lang/sk.bin and b/res/lang/sk.bin differ diff --git a/res/lang/sr.bin b/res/lang/sr.bin index 24f0c616..7d5183e5 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 b1cf8870..e21e8201 100644 Binary files a/res/lang/tok.bin and b/res/lang/tok.bin differ diff --git a/res/lang/tr.bin b/res/lang/tr.bin index 7a4e8592..9dd4764e 100644 Binary files a/res/lang/tr.bin and b/res/lang/tr.bin differ diff --git a/res/lang/uk.bin b/res/lang/uk.bin index cb485617..a48258b6 100644 Binary files a/res/lang/uk.bin and b/res/lang/uk.bin differ diff --git a/res/lang/zh_Hans.bin b/res/lang/zh_Hans.bin index a45e5590..1ad2db27 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 b643c187..f4263b3b 100644 Binary files a/res/lang/zh_Hant.bin and b/res/lang/zh_Hant.bin differ -- cgit v1.2.3 From d9e23c28fa8d0e06a90394605a3d20bd4c4f45cc Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 14 Dec 2021 17:24:11 +0200 Subject: iOS: Bumped version number; updated release notes --- CMakeLists.txt | 4 ++-- res/about/ios-version.gmi | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3d55057c..c2a00c01 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,8 +25,8 @@ project (Lagrange set (COPYRIGHT_YEAR 2021) if (IOS) set (PROJECT_VERSION 1.10) - set (IOS_BUNDLE_VERSION 3) - set (IOS_BUILD_DATE "2021-12-10") + set (IOS_BUNDLE_VERSION 4) + set (IOS_BUILD_DATE "2021-12-14") endif () # Defaults that depend on environment. diff --git a/res/about/ios-version.gmi b/res/about/ios-version.gmi index 7b0428bf..90febe89 100644 --- a/res/about/ios-version.gmi +++ b/res/about/ios-version.gmi @@ -6,6 +6,20 @@ ``` # Release notes +## 1.10 (4) +* Fixed crash when closing sidebar in bookmark edit mode. +* Fixed incorrect initial height of an unfocused multiline input widget. +* Fixed lookup results having too narrow a width. +* Fixed padding at bottom of the upload text editor. +* Fixed missing "Folder" field in the bookmark editor. Edited bookmarks would be moved to the root folder. +* Fixed missing "Glyph Warnings" toggle in Settings. +* Removed "Return Key Behavior" from Settings since it has no effect. +* Minor improvements in page caching. +* Tuned page swipe animations. +* Optimized UI layout updates. +* Audio subsystem is only initialized when actually needed. +* Prevent state file corruption if the app happens to get killed while state is being saved. + ## 1.10 (3) * Revised layout of the Upload dialog. There is a new edit field that lets you modify the file path of the URL. Identity names are in bold. * Added an edit actions menu in the Upload text editor (only in portrait phone layout for now). -- cgit v1.2.3 From 960a7e7f1591c8c8dc7956488578b36117033d76 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 14 Dec 2021 21:42:05 +0200 Subject: Mobile: Adjusted back swipe on tablets --- src/ui/documentwidget.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 1aadb6bb..384c51b5 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -2680,10 +2680,13 @@ static void setupSwipeOverlay_DocumentWidget_(iDocumentWidget *d, iWidget *overl const int fromPos = value_Anim(&w->visualOffset); const int toPos = width_Widget(overlay); setVisualOffset_Widget(overlay, fromPos, 0, 0); - float swipe = iClamp(d->swipeSpeed, 400, 1000) * gap_UI; + /* Bigger screen, faster swipes. */ + const float devFactor = (deviceType_App() == tablet_AppDeviceType ? 2.0f : 1.0f); + float swipe = iClamp(d->swipeSpeed, devFactor * 400, devFactor * 1000) * gap_UI; uint32_t span = ((toPos - fromPos) / swipe) * 1000; // printf("from:%d to:%d swipe:%f span:%u\n", fromPos, toPos, d->swipeSpeed, span); - setVisualOffset_Widget(overlay, toPos, span, 0); + setVisualOffset_Widget(overlay, toPos, span, deviceType_App() == tablet_AppDeviceType ? + easeOut_AnimFlag : 0); setVisualOffset_Widget(w, 0, 0, 0); } -- cgit v1.2.3 From 4dee43aa5c460850268c1acea263d4ff510e7c15 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 14 Dec 2021 21:43:08 +0200 Subject: iOS: Fixed input positioning (non-animated widget) If there were no entry animation, the native UI control would not be correctly placed. --- src/ui/root.c | 2 +- src/ui/root.h | 2 +- src/ui/widget.c | 3 ++- src/ui/window.c | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/ui/root.c b/src/ui/root.c index a9bc7feb..04586bac 100644 --- a/src/ui/root.c +++ b/src/ui/root.c @@ -688,7 +688,7 @@ void updateToolbarColors_Root(iRoot *d) { } void notifyVisualOffsetChange_Root(iRoot *d) { - if (d && (d->didAnimateVisualOffsets || d->didOverflowScroll)) { + if (d && (d->didAnimateVisualOffsets || d->didChangeArrangement)) { iNotifyAudience(d, visualOffsetsChanged, RootVisualOffsetsChanged); } } diff --git a/src/ui/root.h b/src/ui/root.h index 2f0d72c9..7e831be3 100644 --- a/src/ui/root.h +++ b/src/ui/root.h @@ -19,7 +19,7 @@ struct Impl_Root { iBool pendingArrange; int loadAnimTimer; iBool didAnimateVisualOffsets; - iBool didOverflowScroll; + iBool didChangeArrangement; iAudience *visualOffsetsChanged; /* called after running tickers */ iColor tmPalette[tmMax_ColorId]; /* theme-specific palette */ }; diff --git a/src/ui/widget.c b/src/ui/widget.c index 8cb2cf02..df74a744 100644 --- a/src/ui/widget.c +++ b/src/ui/widget.c @@ -901,6 +901,7 @@ void arrange_Widget(iWidget *d) { resetArrangement_Widget_(d); /* back to initial default sizes */ arrange_Widget_(d); notifySizeChanged_Widget_(d); + d->root->didChangeArrangement = iTrue; } } @@ -1218,7 +1219,7 @@ iBool scrollOverflow_Widget(iWidget *d, int delta) { } // printf("range: %d ... %d\n", range.start, range.end); if (delta) { - d->root->didOverflowScroll = iTrue; /* ensure that widgets update if needed */ + d->root->didChangeArrangement = iTrue; /* ensure that widgets update if needed */ } } else { diff --git a/src/ui/window.c b/src/ui/window.c index 76dd1105..a4929f51 100644 --- a/src/ui/window.c +++ b/src/ui/window.c @@ -1265,7 +1265,7 @@ void draw_MainWindow(iMainWindow *d) { if (root) { /* Some widgets may need a just-in-time visual update. */ notifyVisualOffsetChange_Root(root); - root->didOverflowScroll = iFalse; + root->didChangeArrangement = iFalse; } } if (isExposed_Window(w)) { -- cgit v1.2.3 From bc8cc04a7aabad391df2978f39dfeee4c01c505e Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Wed, 15 Dec 2021 06:26:14 +0200 Subject: Mobile: Dismissing keyboard unfocuses InputWidget --- src/ui/inputwidget.c | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index fe897ed4..c60cd99e 100644 --- a/src/ui/inputwidget.c +++ b/src/ui/inputwidget.c @@ -2189,15 +2189,16 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { return iFalse; } else if (isCommand_UserEvent(ev, "keyboard.changed")) { - /* Scroll to keep widget visible when keyboard appears. */ - if (isFocused_Widget(d) && arg_Command(command_UserEvent(ev))) { - d->lastOverflowScrollTime = SDL_GetTicks(); - overflowScrollToKeepVisible_InputWidget_(d); -// rect.pos.y -= value_Anim(&get_Window()->rootOffset); - //const iInt2 visRoot = visibleSize_Root(w->root); - //if (bottom_Rect(rect) > visRoot.y) { - //setValue_Anim(&get_Window()->rootOffset, -(bottom_Rect(rect) - visRoot.y), 250); - //} + const iBool isKeyboardVisible = (arg_Command(command_UserEvent(ev)) != 0); + /* Scroll to keep widget visible when keyboard appears. */ + if (isFocused_Widget(d)) { + if (isKeyboardVisible) { + d->lastOverflowScrollTime = SDL_GetTicks(); + overflowScrollToKeepVisible_InputWidget_(d); + } + else { + setFocus_Widget(NULL); /* stop editing */ + } } return iFalse; } -- cgit v1.2.3 From efcd356c447094089a147ba3cfffd4c433a81e24 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Wed, 15 Dec 2021 06:31:32 +0200 Subject: iOS: Bumped version number; updated release notes --- CMakeLists.txt | 4 ++-- res/about/ios-version.gmi | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c2a00c01..edf58863 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,8 +25,8 @@ project (Lagrange set (COPYRIGHT_YEAR 2021) if (IOS) set (PROJECT_VERSION 1.10) - set (IOS_BUNDLE_VERSION 4) - set (IOS_BUILD_DATE "2021-12-14") + set (IOS_BUNDLE_VERSION 5) + set (IOS_BUILD_DATE "2021-12-15") endif () # Defaults that depend on environment. diff --git a/res/about/ios-version.gmi b/res/about/ios-version.gmi index 90febe89..352c7e36 100644 --- a/res/about/ios-version.gmi +++ b/res/about/ios-version.gmi @@ -6,6 +6,11 @@ ``` # Release notes +## 1.10 (5) +* Fixed positioning of native UI controls in non-animated input widgets. +* Fixed input widgets not reacting to keyboard being dismissed by system. +* iPad: Faster back swipe animation. + ## 1.10 (4) * Fixed crash when closing sidebar in bookmark edit mode. * Fixed incorrect initial height of an unfocused multiline input widget. -- cgit v1.2.3 From 2619a97a74eb1b758263b7eca6e3968e9b05888b Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Wed, 15 Dec 2021 12:38:21 +0200 Subject: macOS: Newlines in native menus Other formatting besides line breaks is ignored for now, although attributed strings could be used here. --- src/gmdocument.c | 6 +++--- src/gmutil.h | 6 ++++++ src/macos.m | 44 ++++++++++++++++++++++++++++++++++++-------- src/ui/text.c | 6 +++++- src/ui/text.h | 4 ++++ 5 files changed, 54 insertions(+), 12 deletions(-) diff --git a/src/gmdocument.c b/src/gmdocument.c index 56b2f06d..bec89ca0 100644 --- a/src/gmdocument.c +++ b/src/gmdocument.c @@ -1899,9 +1899,9 @@ void setUrl_GmDocument(iGmDocument *d, const iString *url) { } } -static int replaceRegExp_String(iString *d, const iRegExp *regexp, const char *replacement, - void (*matchHandler)(void *, const iRegExpMatch *), - void *context) { +int replaceRegExp_String(iString *d, const iRegExp *regexp, const char *replacement, + void (*matchHandler)(void *, const iRegExpMatch *), + void *context) { iRegExpMatch m; iString result; int numMatches = 0; diff --git a/src/gmutil.h b/src/gmutil.h index 35a7ee0d..6d337eeb 100644 --- a/src/gmutil.h +++ b/src/gmutil.h @@ -27,6 +27,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ iDeclareType(GmError) iDeclareType(RegExp) +iDeclareType(RegExpMatch) iDeclareType(Url) /* Response status codes. */ @@ -145,3 +146,8 @@ const iString * findContainerArchive_Path (const iString *path); const iString * feedEntryOpenCommand_String (const iString *url, int newTab); /* checks fragment */ + +/* TODO: Consider adding this to the_Foundation. */ +int replaceRegExp_String (iString *, const iRegExp *regexp, const char *replacement, + void (*matchHandler)(void *, const iRegExpMatch *), + void *context); diff --git a/src/macos.m b/src/macos.m index cfbca488..5fd76bb9 100644 --- a/src/macos.m +++ b/src/macos.m @@ -110,7 +110,7 @@ static void ignoreImmediateKeyDownEvents_(void) { - (id)initWithIdentifier:(NSTouchBarItemIdentifier)identifier title:(NSString *)title command:(NSString *)cmd { - [super initWithIdentifier:identifier]; + self = [super initWithIdentifier:identifier]; self.view = [NSButton buttonWithTitle:title target:self action:@selector(buttonPressed)]; command = cmd; return self; @@ -120,7 +120,7 @@ static void ignoreImmediateKeyDownEvents_(void) { image:(NSImage *)image widget:(iWidget *)widget command:(NSString *)cmd { - [super initWithIdentifier:identifier]; + self = [super initWithIdentifier:identifier]; self.view = [NSButton buttonWithImage:image target:self action:@selector(buttonPressed)]; command = cmd; return self; @@ -163,12 +163,13 @@ static void ignoreImmediateKeyDownEvents_(void) { @implementation MenuCommands - (id)init { + self = [super init]; commands = [[NSMutableDictionary alloc] init]; source = NULL; return self; } -- (void)setCommand:(NSString *)command forMenuItem:(NSMenuItem *)menuItem { +- (void)setCommand:(NSString * __nonnull)command forMenuItem:(NSMenuItem * __nonnull)menuItem { [commands setObject:command forKey:[menuItem title]]; } @@ -220,7 +221,7 @@ static void ignoreImmediateKeyDownEvents_(void) { @implementation MyDelegate - (id)initWithSDLDelegate:(NSObject *)sdl { - [super init]; + self = [super init]; currentAppearanceName = nil; menuCommands = [[MenuCommands alloc] init]; touchBarVariant = default_TouchBarVariant; @@ -541,6 +542,29 @@ enum iColorId removeColorEscapes_String(iString *d) { return color; } +static NSString *cleanString_(const iString *ansiEscapedText) { + iString mod; + initCopy_String(&mod, ansiEscapedText); + iRegExp *ansi = makeAnsiEscapePattern_Text(); + replaceRegExp_String(&mod, ansi, "", NULL, NULL); + iRelease(ansi); + NSString *clean = [NSString stringWithUTF8String:cstr_String(&mod)]; + deinit_String(&mod); + return clean; +} + +#if 0 +static NSAttributedString *makeAttributedString_(const iString *ansiEscapedText) { + iString mod; + initCopy_String(&mod, ansiEscapedText); + NSData *data = [NSData dataWithBytesNoCopy:data_Block(&mod.chars) length:size_String(&mod)]; + NSAttributedString *as = [[NSAttributedString alloc] initWithHTML:data + documentAttributes:nil]; + deinit_String(&mod); + return as; +} +#endif + /* returns the selected item, if any */ static NSMenuItem *makeMenuItems_(NSMenu *menu, MenuCommands *commands, const iMenuItem *items, size_t n) { NSMenuItem *selectedItem = nil; @@ -557,7 +581,7 @@ static NSMenuItem *makeMenuItems_(NSMenu *menu, MenuCommands *commands, const iM isChecked = iTrue; label += 3; } - else if (startsWith_CStr(label, "///")) { + else if (startsWith_CStr(label, "///") || startsWith_CStr(label, "```")) { isDisabled = iTrue; label += 3; } @@ -567,9 +591,13 @@ static NSMenuItem *makeMenuItems_(NSMenu *menu, MenuCommands *commands, const iM if (removeColorEscapes_String(&itemTitle) == uiTextCaution_ColorId) { // prependCStr_String(&itemTitle, "\u26a0\ufe0f "); } - NSMenuItem *item = [menu addItemWithTitle:[NSString stringWithUTF8String:cstr_String(&itemTitle)] - action:(hasCommand ? @selector(postMenuItemCommand:) : nil) - keyEquivalent:@""]; + NSMenuItem *item = [[NSMenuItem alloc] init]; + /* Use attributed string to allow newlines. */ + NSAttributedString *title = [[NSAttributedString alloc] initWithString:cleanString_(&itemTitle)]; + item.attributedTitle = title; + [title release]; + item.action = (hasCommand ? @selector(postMenuItemCommand:) : nil); + [menu addItem:item]; deinit_String(&itemTitle); [item setTarget:commands]; if (isChecked) { diff --git a/src/ui/text.c b/src/ui/text.c index 7367e6c0..f3d945e4 100644 --- a/src/ui/text.c +++ b/src/ui/text.c @@ -390,12 +390,16 @@ static void deinitCache_Text_(iText *d) { SDL_DestroyTexture(d->cache); } +iRegExp *makeAnsiEscapePattern_Text(void) { + return new_RegExp("[[()][?]?([0-9;AB]*?)([ABCDEFGHJKSTfhilmn])", 0); +} + void init_Text(iText *d, SDL_Renderer *render) { iText *oldActive = activeText_; activeText_ = d; init_Array(&d->fonts, sizeof(iFont)); d->contentFontSize = contentScale_Text_; - d->ansiEscape = new_RegExp("[[()][?]?([0-9;AB]*?)([ABCDEFGHJKSTfhilmn])", 0); + d->ansiEscape = makeAnsiEscapePattern_Text(); d->baseFontId = -1; d->baseFgColorId = -1; d->missingGlyphs = iFalse; diff --git a/src/ui/text.h b/src/ui/text.h index b7934855..cb29adad 100644 --- a/src/ui/text.h +++ b/src/ui/text.h @@ -29,6 +29,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "fontpack.h" +iDeclareType(RegExp) + /* Content sizes: regular (1x) -> medium (1.2x) -> big (1.33x) -> large (1.67x) -> huge (2x) */ #define FONT_ID(name, style, size) ((name) + ((style) * max_FontSize) + (size)) @@ -230,6 +232,8 @@ enum iTextBlockMode { quadrants_TextBlockMode, shading_TextBlockMode }; iString * renderBlockChars_Text (const iBlock *fontData, int height, enum iTextBlockMode, const iString *text); +iRegExp * makeAnsiEscapePattern_Text (void); + /*-----------------------------------------------------------------------------------------------*/ iDeclareType(TextBuf) -- cgit v1.2.3 From bc3d069519b3cfd832a6a6499f2f4b2ef4a70329 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Wed, 15 Dec 2021 12:48:06 +0200 Subject: InputWidget: Dealing with very narrow width The input widget is not usable if the width is too narrow. --- src/ui/inputwidget.c | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index c60cd99e..8d58eac9 100644 --- a/src/ui/inputwidget.c +++ b/src/ui/inputwidget.c @@ -374,6 +374,8 @@ static iRect contentBounds_InputWidget_(const iInputWidget *d) { return bounds; } +#define minWidth_InputWidget_ (3 * gap_UI) + static iWrapText wrap_InputWidget_(const iInputWidget *d, int y) { #if LAGRANGE_USE_SYSTEM_TEXT_INPUT iUnused(y); /* full text is wrapped always */ @@ -383,7 +385,8 @@ static iWrapText wrap_InputWidget_(const iInputWidget *d, int y) { #endif return (iWrapText){ .text = text, - .maxWidth = d->maxLen == 0 ? width_Rect(contentBounds_InputWidget_(d)) + .maxWidth = d->maxLen == 0 ? iMaxi(minWidth_InputWidget_, + width_Rect(contentBounds_InputWidget_(d))) : unlimitedWidth_InputWidget_, .mode = (d->inFlags & isUrl_InputWidgetFlag ? anyCharacter_WrapTextMode : word_WrapTextMode), @@ -647,7 +650,7 @@ static size_t length_InputWidget_(const iInputWidget *d) { static void updateLine_InputWidget_(iInputWidget *d, iInputLine *line) { iAssert(endsWith_String(&line->text, "\n") || isLastLine_InputWidget_(d, line)); iWrapText wrapText = wrap_InputWidget_(d, indexOf_Array(&d->lines, line)); - if (wrapText.maxWidth <= 0) { + if (wrapText.maxWidth <= minWidth_InputWidget_) { line->wrapLines.end = line->wrapLines.start + 1; return; } @@ -1524,7 +1527,7 @@ static iInt2 coordCursor_InputWidget_(const iInputWidget *d, iInt2 coord) { // return cursorMax_InputWidget_(d); // } iWrapText wrapText = { - .maxWidth = d->maxLen == 0 ? width_Rect(bounds) : unlimitedWidth_InputWidget_, + .maxWidth = d->maxLen == 0 ? iMaxi(minWidth_InputWidget_, width_Rect(bounds)) : unlimitedWidth_InputWidget_, .mode = (d->inFlags & isUrl_InputWidgetFlag ? anyCharacter_WrapTextMode : word_WrapTextMode), .hitPoint = relCoord, .overrideChar = (d->inFlags & isSensitive_InputWidgetFlag ? sensitiveChar_ : 0), @@ -2108,6 +2111,10 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { /* Resize according to width immediately. */ if (d->lastUpdateWidth != w->rect.size.x) { d->inFlags |= needUpdateBuffer_InputWidgetFlag; + if (contentBounds_InputWidget_(d).size.x < minWidth_InputWidget_) { + setFocus_Widget(NULL); + return iFalse; + } if (d->inFlags & isUrl_InputWidgetFlag) { /* Restore/omit the default scheme if necessary. */ setText_InputWidget(d, text_InputWidget(d)); @@ -2127,7 +2134,12 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { } #endif if (isCommand_Widget(w, ev, "focus.gained")) { - begin_InputWidget(d); + if (contentBounds_InputWidget_(d).size.x < minWidth_InputWidget_) { + setFocus_Widget(NULL); + } + else { + begin_InputWidget(d); + } return iFalse; } else if (isCommand_UserEvent(ev, "keyroot.changed")) { -- cgit v1.2.3 From c8fcbe53279d9537895e17e15047b0142d24e4a1 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Wed, 15 Dec 2021 13:40:48 +0200 Subject: InputWidget: Initial scroll position out of bounds --- src/ui/inputwidget.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index 8d58eac9..49cc0a1e 100644 --- a/src/ui/inputwidget.c +++ b/src/ui/inputwidget.c @@ -558,6 +558,9 @@ static iInt2 relativeCursorCoord_InputWidget_(const iInputWidget *d) { } static void updateVisible_InputWidget_(iInputWidget *d) { + if (width_Widget(d) == 0) { + return; /* Nothing to do yet. */ + } const int totalWraps = numWrapLines_InputWidget_(d); const int visWraps = iClamp(totalWraps, d->minWrapLines, d->maxWrapLines); /* Resize the height of the editor. */ @@ -575,6 +578,14 @@ static void updateVisible_InputWidget_(iInputWidget *d) { else if (cursorY < d->visWrapLines.start) { delta = cursorY - d->visWrapLines.start; } + if (d->visWrapLines.end + delta > totalWraps) { + /* Don't scroll past the bottom. */ + delta = totalWraps - d->visWrapLines.end; + } + if (d->visWrapLines.start + delta < 0) { + /* Don't ever scroll above the top. */ + delta = -d->visWrapLines.start; + } d->visWrapLines.start += delta; d->visWrapLines.end += delta; iAssert(contains_Range(&d->visWrapLines, cursorY)); @@ -584,6 +595,7 @@ static void updateVisible_InputWidget_(iInputWidget *d) { } // printf("[InputWidget %p] total:%d viswrp:%d cur:%d vis:%d..%d\n", // d, totalWraps, visWraps, d->cursor.y, d->visWrapLines.start, d->visWrapLines.end); +// fflush(stdout); } static void showCursor_InputWidget_(iInputWidget *d) { -- cgit v1.2.3 From f8dac44a38ee4dc6472f5d01f66ca9cd7ac1adec Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Wed, 15 Dec 2021 13:41:02 +0200 Subject: Workaround for layout issue in value input dialogs --- src/ui/util.c | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/ui/util.c b/src/ui/util.c index cc11db57..db24b6fc 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -1699,6 +1699,12 @@ iBool valueInputHandler_(iWidget *dlg, const char *cmd) { } return iFalse; } + if (equal_Command(cmd, "input.resized")) { + /* BUG: A single arrange here is not sufficient, leaving a big gap between prompt and input. Why? */ + arrange_Widget(dlg); + arrange_Widget(dlg); + return iTrue; + } if (equal_Command(cmd, "input.ended")) { if (argLabel_Command(cmd, "enter") && hasParent_Widget(ptr, dlg)) { if (arg_Command(cmd)) { @@ -1861,8 +1867,9 @@ iWidget *makeValueInput_Widget(iWidget *parent, const iString *initialValue, con if (parent) { setFocus_Widget(as_Widget(input)); } - /* Check that the top is in the safe area. */ { - int top = top_Rect(bounds_Widget(dlg)); + /* Check that the top is in the safe area. */ + if (deviceType_App() != desktop_AppDeviceType) { + int top = top_Rect(boundsWithoutVisualOffset_Widget(dlg)); int delta = top - top_Rect(safeRect_Root(dlg->root)); if (delta < 0) { dlg->rect.pos.y -= delta; @@ -2943,10 +2950,10 @@ iWidget *makeBookmarkEditor_Widget(void) { { "input id:bmed.url url:1 noheading:1" }, { "padding" }, { "input id:bmed.title text:${dlg.bookmark.title}" }, - { "input id:bmed.tags text:${dlg.bookmark.tags}" }, - { "input id:bmed.icon maxlen:1 text:${dlg.bookmark.icon}" }, - { "padding" }, { "dropdown id:bmed.folder text:${dlg.bookmark.folder}", 0, 0, (const void *) constData_Array(folderItems) }, + { "padding" }, + { "input id:bmed.icon maxlen:1 text:${dlg.bookmark.icon}" }, + { "input id:bmed.tags text:${dlg.bookmark.tags}" }, { "heading text:${heading.bookmark.tags}" }, { "toggle id:bmed.tag.home text:${bookmark.tag.home}" }, { "toggle id:bmed.tag.remote text:${bookmark.tag.remote}" }, @@ -2966,24 +2973,24 @@ iWidget *makeBookmarkEditor_Widget(void) { iWidget *headings, *values; addChild_Widget(dlg, iClob(makeTwoColumns_Widget(&headings, &values))); iInputWidget *inputs[4]; - addDialogInputWithHeading_(headings, values, "${dlg.bookmark.title}", "bmed.title", iClob(inputs[0] = new_InputWidget(0))); - addDialogInputWithHeading_(headings, values, "${dlg.bookmark.url}", "bmed.url", iClob(inputs[1] = new_InputWidget(0))); - setUrlContent_InputWidget(inputs[1], iTrue); /* Folder to add to. */ { addChild_Widget(headings, iClob(makeHeading_Widget("${dlg.bookmark.folder}"))); const iArray *folderItems = makeBookmarkFolderItems_(iFalse); iLabelWidget *folderButton; setId_Widget(addChildFlags_Widget(values, - iClob(folderButton = makeMenuButton_LabelWidget( - widestLabel_MenuItemArray(folderItems), - constData_Array(folderItems), - size_Array(folderItems))), alignLeft_WidgetFlag), + iClob(folderButton = makeMenuButton_LabelWidget( + widestLabel_MenuItemArray(folderItems), + constData_Array(folderItems), + size_Array(folderItems))), alignLeft_WidgetFlag), "bmed.folder"); const uint32_t recentFolderId = recentFolder_Bookmarks(bookmarks_App()); updateDropdownSelection_LabelWidget( folderButton, format_CStr(" arg:%u", recentFolderId)); setUserData_Object(folderButton, get_Bookmarks(bookmarks_App(), recentFolderId)); } + addDialogInputWithHeading_(headings, values, "${dlg.bookmark.title}", "bmed.title", iClob(inputs[0] = new_InputWidget(0))); + addDialogInputWithHeading_(headings, values, "${dlg.bookmark.url}", "bmed.url", iClob(inputs[1] = new_InputWidget(0))); + setUrlContent_InputWidget(inputs[1], iTrue); addDialogInputWithHeading_(headings, values, "${dlg.bookmark.tags}", "bmed.tags", iClob(inputs[2] = new_InputWidget(0))); addDialogInputWithHeading_(headings, values, "${dlg.bookmark.icon}", "bmed.icon", iClob(inputs[3] = new_InputWidget(1))); /* Buttons for special tags. */ -- cgit v1.2.3 From 01c298520d7b6d9ad268d59a6d2a0b99c5cded78 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Wed, 15 Dec 2021 18:56:31 +0200 Subject: Preferences: Option for cursor blinking --- po/en.po | 3 +++ src/app.c | 16 ++++++++-------- src/prefs.c | 1 + src/prefs.h | 1 + src/ui/inputwidget.c | 24 +++++++++++++++++------- src/ui/util.c | 9 +++++---- 6 files changed, 35 insertions(+), 19 deletions(-) diff --git a/po/en.po b/po/en.po index 33c86382..3fc5423d 100644 --- a/po/en.po +++ b/po/en.po @@ -1441,6 +1441,9 @@ msgstr "UI scale factor:" msgid "prefs.customframe" msgstr "Custom window frame:" +msgid "prefs.blink" +msgstr "Blinking cursor:" + msgid "prefs.returnkey" msgstr "Return key behavior:" diff --git a/src/app.c b/src/app.c index 828f5097..e807f09d 100644 --- a/src/app.c +++ b/src/app.c @@ -258,6 +258,7 @@ static iString *serializePrefs_App_(const iApp *d) { { "prefs.bookmarks.addbottom", &d->prefs.addBookmarksToBottom }, { "prefs.archive.openindex", &d->prefs.openArchiveIndexPages }, { "prefs.font.warnmissing", &d->prefs.warnAboutMissingGlyphs }, + { "prefs.blink", &d->prefs.blinkingCursor }, }; iForIndices(i, boolPrefs) { appendFormat_String(str, "%s.changed arg:%d\n", boolPrefs[i].id, *boolPrefs[i].value); @@ -2600,6 +2601,10 @@ iBool handleCommand_App(const char *cmd) { d->prefs.uiAnimations = arg_Command(cmd) != 0; return iTrue; } + else if (equal_Command(cmd, "prefs.blink.changed")) { + d->prefs.blinkingCursor = arg_Command(cmd) != 0; + return iTrue; + } else if (equal_Command(cmd, "prefs.time.24h.changed")) { d->prefs.time24h = arg_Command(cmd) != 0; return iTrue; @@ -2911,6 +2916,8 @@ iBool handleCommand_App(const char *cmd) { iWidget *dlg = makePreferences_Widget(); updatePrefsThemeButtons_(dlg); setText_InputWidget(findChild_Widget(dlg, "prefs.downloads"), &d->prefs.strings[downloadDir_PrefsString]); + /* TODO: Use a common table in Prefs to do this more conviently. + Also see `serializePrefs_App_()`. */ setToggle_Widget(findChild_Widget(dlg, "prefs.hoverlink"), d->prefs.hoverLink); setToggle_Widget(findChild_Widget(dlg, "prefs.smoothscroll"), d->prefs.smoothScrolling); setToggle_Widget(findChild_Widget(dlg, "prefs.imageloadscroll"), d->prefs.loadImageInsteadOfScrolling); @@ -2921,7 +2928,7 @@ iBool handleCommand_App(const char *cmd) { setToggle_Widget(findChild_Widget(dlg, "prefs.ostheme"), d->prefs.useSystemTheme); setToggle_Widget(findChild_Widget(dlg, "prefs.customframe"), d->prefs.customFrame); setToggle_Widget(findChild_Widget(dlg, "prefs.animate"), d->prefs.uiAnimations); -// setText_InputWidget(findChild_Widget(dlg, "prefs.userfont"), &d->prefs.symbolFontPath); + setToggle_Widget(findChild_Widget(dlg, "prefs.blink"), d->prefs.blinkingCursor); updatePrefsPinSplitButtons_(dlg, d->prefs.pinSplit); updateScrollSpeedButtons_(dlg, mouse_ScrollType, d->prefs.smoothScrollSpeed[mouse_ScrollType]); updateScrollSpeedButtons_(dlg, keyboard_ScrollType, d->prefs.smoothScrollSpeed[keyboard_ScrollType]); @@ -2933,13 +2940,6 @@ iBool handleCommand_App(const char *cmd) { setToggle_Widget(findChild_Widget(dlg, "prefs.retainwindow"), d->prefs.retainWindowSize); setText_InputWidget(findChild_Widget(dlg, "prefs.uiscale"), collectNewFormat_String("%g", uiScale_Window(as_Window(d->window)))); -// setFlags_Widget(findChild_Widget(dlg, format_CStr("prefs.font.%d", d->prefs.font)), -// selected_WidgetFlag, -// iTrue); -// setFlags_Widget( -// findChild_Widget(dlg, format_CStr("prefs.headingfont.%d", d->prefs.headingFont)), -// selected_WidgetFlag, -// iTrue); setFlags_Widget(findChild_Widget(dlg, "prefs.mono.gemini"), selected_WidgetFlag, d->prefs.monospaceGemini); diff --git a/src/prefs.c b/src/prefs.c index 10df9ade..5d85f195 100644 --- a/src/prefs.c +++ b/src/prefs.c @@ -42,6 +42,7 @@ void init_Prefs(iPrefs *d) { d->zoomPercent = 100; d->sideIcon = iTrue; d->hideToolbarOnScroll = iTrue; + d->blinkingCursor = iTrue; d->pinSplit = 1; d->time24h = iTrue; d->returnKey = default_ReturnKeyBehavior; diff --git a/src/prefs.h b/src/prefs.h index c9abfc8e..f22953c5 100644 --- a/src/prefs.h +++ b/src/prefs.h @@ -72,6 +72,7 @@ struct Impl_Prefs { iBool uiAnimations; float uiScale; iBool hideToolbarOnScroll; + iBool blinkingCursor; /* Document presentation */ int zoomPercent; iBool sideIcon; diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index 49cc0a1e..b910f905 100644 --- a/src/ui/inputwidget.c +++ b/src/ui/inputwidget.c @@ -588,7 +588,7 @@ static void updateVisible_InputWidget_(iInputWidget *d) { } d->visWrapLines.start += delta; d->visWrapLines.end += delta; - iAssert(contains_Range(&d->visWrapLines, cursorY)); +// iAssert(contains_Range(&d->visWrapLines, cursorY)); if (!isFocused_Widget(d) && d->maxWrapLines == 1) { d->visWrapLines.start = 0; d->visWrapLines.end = 1; @@ -711,7 +711,10 @@ static uint32_t cursorTimer_(uint32_t interval, void *w) { return interval; } -static void startOrStopCursorTimer_InputWidget_(iInputWidget *d, iBool doStart) { +static void startOrStopCursorTimer_InputWidget_(iInputWidget *d, int doStart) { + if (!prefs_App()->blinkingCursor && doStart == 1) { + doStart = iFalse; + } if (doStart && !d->timer) { d->timer = SDL_AddTimer(refreshInterval_InputWidget_, cursorTimer_, d); } @@ -2167,6 +2170,12 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { return iFalse; } #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT + else if (isCommand_UserEvent(ev, "prefs.blink.changed")) { + if (isEditing_InputWidget_(d) && arg_Command(command_UserEvent(ev))) { + startOrStopCursorTimer_InputWidget_(d, 2); + } + return iFalse; + } else if (isEditing_InputWidget_(d) && (isCommand_UserEvent(ev, "window.focus.lost") || isCommand_UserEvent(ev, "window.focus.gained"))) { startOrStopCursorTimer_InputWidget_(d, isCommand_UserEvent(ev, "window.focus.gained")); @@ -2613,10 +2622,10 @@ static void draw_InputWidget_(const iInputWidget *d) { return; } const iRect contentBounds = contentBounds_InputWidget_(d); - iInt2 drawPos = topLeft_Rect(contentBounds); - const int fg = isHint ? uiAnnotation_ColorId - : isFocused /*&& !isEmpty_Array(&d->lines)*/ ? uiInputTextFocused_ColorId - : uiInputText_ColorId; + iInt2 drawPos = topLeft_Rect(contentBounds); + const int fg = isHint ? uiAnnotation_ColorId + : isFocused ? uiInputTextFocused_ColorId + : uiInputText_ColorId; #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT setClip_Paint(&p, adjusted_Rect(bounds, @@ -2685,7 +2694,8 @@ static void draw_InputWidget_(const iInputWidget *d) { wrapText.context = NULL; } /* Draw the insertion point. */ - if (isFocused && d->cursorVis && contains_Range(&visLines, d->cursor.y) && + if (isFocused && (d->cursorVis || !prefs_App()->blinkingCursor) && + contains_Range(&visLines, d->cursor.y) && (deviceType_App() == desktop_AppDeviceType || isEmpty_Range(&d->mark))) { iInt2 curSize; iRangecc cursorChar = iNullRange; diff --git a/src/ui/util.c b/src/ui/util.c index db24b6fc..a46c7f80 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -2629,9 +2629,8 @@ iWidget *makePreferences_Widget(void) { } /* User Interface. */ { appendTwoColumnTabPage_Widget(tabs, "${heading.prefs.interface}", '2', &headings, &values); -#if defined (LAGRANGE_ENABLE_CUSTOM_FRAME) - addDialogToggle_(headings, values, "${prefs.customframe}", "prefs.customframe"); -#endif + addDialogToggle_(headings, values, "${prefs.animate}", "prefs.animate"); + addDialogToggle_(headings, values, "${prefs.blink}", "prefs.blink"); addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.returnkey}"))); /* Return key behaviors. */ { iLabelWidget *returnKey = makeMenuButton_LabelWidget( @@ -2645,7 +2644,9 @@ iWidget *makePreferences_Widget(void) { setId_Widget(addChildFlags_Widget(values, iClob(returnKey), alignLeft_WidgetFlag), "prefs.returnkey"); } - addDialogToggle_(headings, values, "${prefs.animate}", "prefs.animate"); +#if defined (LAGRANGE_ENABLE_CUSTOM_FRAME) + addDialogToggle_(headings, values, "${prefs.customframe}", "prefs.customframe"); +#endif makeTwoColumnHeading_("${heading.prefs.scrolling}", headings, values); addDialogToggle_(headings, values, "${prefs.smoothscroll}", "prefs.smoothscroll"); /* Scroll speeds. */ { -- cgit v1.2.3 From a5a59eb664cdfb5fe37fb7d8c82d11c7008cbac7 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 16 Dec 2021 11:09:35 +0200 Subject: DocumentWidget: Paste preceding line Added a special actions menu in the input prompt to allow the user to paste the line preceding the latest clicked link from the originating document. One use case for this is user-editable lines. --- po/en.po | 4 ++ res/lang/cs.bin | Bin 31435 -> 31510 bytes res/lang/de.bin | Bin 30442 -> 30517 bytes res/lang/en.bin | Bin 26539 -> 26614 bytes res/lang/eo.bin | Bin 25493 -> 25568 bytes res/lang/es.bin | Bin 30266 -> 30341 bytes res/lang/es_MX.bin | Bin 27598 -> 27673 bytes res/lang/fi.bin | Bin 30099 -> 30174 bytes res/lang/fr.bin | Bin 31246 -> 31321 bytes res/lang/gl.bin | Bin 29451 -> 29526 bytes res/lang/hu.bin | Bin 31271 -> 31346 bytes res/lang/ia.bin | Bin 28598 -> 28673 bytes res/lang/ie.bin | Bin 29186 -> 29261 bytes res/lang/isv.bin | Bin 25259 -> 25334 bytes res/lang/pl.bin | Bin 29874 -> 29949 bytes res/lang/ru.bin | Bin 44634 -> 44709 bytes res/lang/sk.bin | Bin 25595 -> 25670 bytes res/lang/sr.bin | Bin 44060 -> 44135 bytes res/lang/tok.bin | Bin 27308 -> 27383 bytes res/lang/tr.bin | Bin 29492 -> 29567 bytes res/lang/uk.bin | Bin 43979 -> 44054 bytes res/lang/zh_Hans.bin | Bin 25493 -> 25568 bytes res/lang/zh_Hant.bin | Bin 25691 -> 25766 bytes src/app.c | 21 +++++++++-- src/ui/documentwidget.c | 98 +++++++++++++++++++++++++++++++++++++----------- src/ui/documentwidget.h | 1 + src/ui/inputwidget.c | 10 +++-- src/ui/inputwidget.h | 1 + src/ui/mobile.c | 12 +++--- src/ui/root.c | 2 +- src/ui/util.c | 12 ++++-- src/ui/window.c | 6 ++- src/ui/window.h | 1 + 33 files changed, 128 insertions(+), 40 deletions(-) diff --git a/po/en.po b/po/en.po index 3fc5423d..481545cf 100644 --- a/po/en.po +++ b/po/en.po @@ -760,6 +760,10 @@ msgstr "Line break" msgid "dlg.input.send" msgstr "Send" +# Paste the line preceding the clicked link into the input prompt. +msgid "menu.input.precedingline" +msgstr "Paste Preceding Line" + msgid "heading.save" msgstr "FILE SAVED" diff --git a/res/lang/cs.bin b/res/lang/cs.bin index 3a5b5820..97197051 100644 Binary files a/res/lang/cs.bin and b/res/lang/cs.bin differ diff --git a/res/lang/de.bin b/res/lang/de.bin index 57a6c945..d2dceda6 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 6c921d9b..28c11997 100644 Binary files a/res/lang/en.bin and b/res/lang/en.bin differ diff --git a/res/lang/eo.bin b/res/lang/eo.bin index b0ad3a58..d148e2f0 100644 Binary files a/res/lang/eo.bin and b/res/lang/eo.bin differ diff --git a/res/lang/es.bin b/res/lang/es.bin index 61a5b870..92037aea 100644 Binary files a/res/lang/es.bin and b/res/lang/es.bin differ diff --git a/res/lang/es_MX.bin b/res/lang/es_MX.bin index 6658bbcb..7b7e1219 100644 Binary files a/res/lang/es_MX.bin and b/res/lang/es_MX.bin differ diff --git a/res/lang/fi.bin b/res/lang/fi.bin index 287de822..243e9740 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 495a1b30..507cbb4c 100644 Binary files a/res/lang/fr.bin and b/res/lang/fr.bin differ diff --git a/res/lang/gl.bin b/res/lang/gl.bin index efafd3a1..447ed72d 100644 Binary files a/res/lang/gl.bin and b/res/lang/gl.bin differ diff --git a/res/lang/hu.bin b/res/lang/hu.bin index e0095569..c2a085ec 100644 Binary files a/res/lang/hu.bin and b/res/lang/hu.bin differ diff --git a/res/lang/ia.bin b/res/lang/ia.bin index fb4f482c..5305304c 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 d397cdea..69ac42d4 100644 Binary files a/res/lang/ie.bin and b/res/lang/ie.bin differ diff --git a/res/lang/isv.bin b/res/lang/isv.bin index 012e624f..56ed525c 100644 Binary files a/res/lang/isv.bin and b/res/lang/isv.bin differ diff --git a/res/lang/pl.bin b/res/lang/pl.bin index 60b5d77c..1ad99e5d 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 8cda6497..03092acf 100644 Binary files a/res/lang/ru.bin and b/res/lang/ru.bin differ diff --git a/res/lang/sk.bin b/res/lang/sk.bin index 9a5355d9..bf13f594 100644 Binary files a/res/lang/sk.bin and b/res/lang/sk.bin differ diff --git a/res/lang/sr.bin b/res/lang/sr.bin index 7d5183e5..823818e2 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 e21e8201..50743783 100644 Binary files a/res/lang/tok.bin and b/res/lang/tok.bin differ diff --git a/res/lang/tr.bin b/res/lang/tr.bin index 9dd4764e..543739cd 100644 Binary files a/res/lang/tr.bin and b/res/lang/tr.bin differ diff --git a/res/lang/uk.bin b/res/lang/uk.bin index a48258b6..f866f86f 100644 Binary files a/res/lang/uk.bin and b/res/lang/uk.bin differ diff --git a/res/lang/zh_Hans.bin b/res/lang/zh_Hans.bin index 1ad2db27..179d87db 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 f4263b3b..aad28410 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 e807f09d..accdd991 100644 --- a/src/app.c +++ b/src/app.c @@ -356,7 +356,7 @@ static void loadPrefs_App_(iApp *d) { setUiScale_Window(get_Window(), argf_Command(cmd)); } else if (equal_Command(cmd, "uilang")) { - const char *id = cstr_Rangecc(range_Command(cmd, "id")); + const char *id = cstr_Command(cmd, "id"); setCStr_String(&d->prefs.strings[uiLanguage_PrefsString], id); setCurrent_Lang(id); } @@ -2190,6 +2190,7 @@ iBool handleCommand_App(const char *cmd) { return iTrue; } else if (equal_Command(cmd, "fontpack.suggest.classic")) { + /* TODO: Don't use this when system fonts are accessible. */ if (!isInstalled_Fonts("classic-set") && !isInstalled_Fonts("cjk")) { makeQuestion_Widget( uiHeading_ColorEscape "${heading.fontpack.classic}", @@ -2243,6 +2244,9 @@ iBool handleCommand_App(const char *cmd) { (argLabel_Command(cmd, "axis") ? vertical_WindowSplit : 0) | (arg_Command(cmd) << 1); const char *url = suffixPtr_Command(cmd, "url"); setCStr_String(d->window->pendingSplitUrl, url ? url : ""); + if (hasLabel_Command(cmd, "origin")) { + set_String(d->window->pendingSplitOrigin, string_Command(cmd, "origin")); + } postRefresh_App(); return iTrue; } @@ -2732,12 +2736,21 @@ iBool handleCommand_App(const char *cmd) { openInDefaultBrowser_App(url); return iTrue; } + iDocumentWidget *doc = document_Command(cmd); + iDocumentWidget *origin = doc; + if (hasLabel_Command(cmd, "origin")) { + iDocumentWidget *cmdOrig = findWidget_App(cstr_Command(cmd, "origin")); + if (cmdOrig) { + origin = cmdOrig; + } + } const int newTab = argLabel_Command(cmd, "newtab"); if (newTab & otherRoot_OpenTabFlag && numRoots_Window(get_Window()) == 1) { /* Need to split first. */ const iInt2 winSize = get_Window()->size; - postCommandf_App("ui.split arg:3 axis:%d newtab:%d url:%s", + postCommandf_App("ui.split arg:3 axis:%d origin:%s newtab:%d url:%s", (float) winSize.x / (float) winSize.y < 0.7f ? 1 : 0, + cstr_String(id_Widget(as_Widget(origin))), newTab & ~otherRoot_OpenTabFlag, cstr_String(url)); return iTrue; @@ -2749,7 +2762,6 @@ iBool handleCommand_App(const char *cmd) { setKeyRoot_Window(as_Window(d->window), root); setCurrent_Root(root); /* need to change for widget creation */ } - iDocumentWidget *doc = document_Command(cmd); if (newTab & (new_OpenTabFlag | newBackground_OpenTabFlag)) { doc = newTab_App(NULL, (newTab & new_OpenTabFlag) != 0); /* `newtab:2` to open in background */ } @@ -2766,13 +2778,14 @@ iBool handleCommand_App(const char *cmd) { } setInitialScroll_DocumentWidget(doc, argfLabel_Command(cmd, "scroll")); setRedirectCount_DocumentWidget(doc, redirectCount); + setOrigin_DocumentWidget(doc, origin); showCollapsed_Widget(findWidget_App("document.progress"), iFalse); if (prefs_App()->decodeUserVisibleURLs) { urlDecodePath_String(url); } else { urlEncodePath_String(url); - } + } setUrlFlags_DocumentWidget(doc, url, (isHistory ? useCachedContentIfAvailable_DocumentWidgetSetUrlFlag : 0) | (fromSidebar ? openedFromSidebar_DocumentWidgetSetUrlFlag : 0)); diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 384c51b5..50df05f5 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -264,6 +264,7 @@ struct Impl_DocumentWidget { int pinchZoomPosted; float swipeSpeed; /* points/sec */ iString pendingGotoHeading; + iString linePrecedingLink; /* Network request: */ enum iRequestState state; @@ -391,6 +392,7 @@ void init_DocumentWidget(iDocumentWidget *d) { d->grabbedPlayer = NULL; d->mediaTimer = 0; init_String(&d->pendingGotoHeading); + init_String(&d->linePrecedingLink); init_Click(&d->click, d, SDL_BUTTON_LEFT); addChild_Widget(w, iClob(d->scroll = new_ScrollWidget())); d->menu = NULL; /* created when clicking */ @@ -437,6 +439,7 @@ void deinit_DocumentWidget(iDocumentWidget *d) { iRelease(d->media); iRelease(d->request); delete_Gempub(d->sourceGempub); + deinit_String(&d->linePrecedingLink); deinit_String(&d->pendingGotoHeading); deinit_Block(&d->sourceContent); deinit_String(&d->sourceMime); @@ -1893,7 +1896,7 @@ static void addBannerWarnings_DocumentWidget_(iDocumentWidget *d) { static void updateFromCachedResponse_DocumentWidget_(iDocumentWidget *d, float normScrollY, const iGmResponse *resp, iGmDocument *cachedDoc) { - iAssert(width_Widget(d) > 0); /* must be laid out by now */ +// iAssert(width_Widget(d) > 0); /* must be laid out by now */ setLinkNumberMode_DocumentWidget_(d, iFalse); clear_ObjectList(d->media); delete_Gempub(d->sourceGempub); @@ -1918,9 +1921,6 @@ static void updateFromCachedResponse_DocumentWidget_(iDocumentWidget *d, float n setWidth_GmDocument(d->doc, documentWidth_DocumentWidget_(d), width_Widget(d)); } updateDocument_DocumentWidget_(d, resp, cachedDoc, iTrue); -// if (!cachedDoc) { -// setCachedDocument_History(d->mod.history, d->doc, iFalse); -// } clear_Banner(d->banner); updateBanner_DocumentWidget_(d); addBannerWarnings_DocumentWidget_(d); @@ -2242,6 +2242,16 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) { if (lineBreak && deviceType_App() != desktop_AppDeviceType) { addChildPos_Widget(buttons, iClob(lineBreak), front_WidgetAddPos); } + /* Menu for additional actions, past entries. */ { + iMenuItem items[] = { { "${menu.input.precedingline}", + SDLK_v, + KMOD_PRIMARY | KMOD_SHIFT, + format_CStr("!valueinput.set ptr:%p text:%s", + buttons, + cstr_String(&d->linePrecedingLink)) } }; + iLabelWidget *menu = makeMenuButton_LabelWidget(midEllipsis_Icon, items, 1); + addChildPos_Widget(buttons, iClob(menu), front_WidgetAddPos); + } setValidator_InputWidget(findChild_Widget(dlg, "input"), inputQueryValidator_, d); setSensitiveContent_InputWidget(findChild_Widget(dlg, "input"), statusCode == sensitiveInput_GmStatusCode); @@ -3836,6 +3846,30 @@ static void beginMarkingSelection_DocumentWidget_(iDocumentWidget *d, iInt2 pos) refresh_Widget(as_Widget(d)); } +static void linkWasTriggered_DocumentWidget_(iDocumentWidget *d, iGmLinkId id) { + iRangecc loc = linkUrlRange_GmDocument(d->doc, id); + if (!loc.start) { + clear_String(&d->linePrecedingLink); + return; + } + const char *start = range_String(source_GmDocument(d->doc)).start; + /* Find the preceding line. This is offered as a prefill option for a possible input query. */ + while (loc.start > start && *loc.start != '\n') { + loc.start--; + } + loc.end = loc.start; /* End of the preceding line. */ + if (loc.start > start) { + loc.start--; + } + while (loc.start > start && *loc.start != '\n') { + loc.start--; + } + if (*loc.start == '\n') { + loc.start++; /* Start of the preceding line. */ + } + setRange_String(&d->linePrecedingLink, loc); +} + static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) { iWidget *w = as_Widget(d); if (isMetricsChange_UserEvent(ev)) { @@ -3878,6 +3912,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e : (d->flags & newTabViaHomeKeys_DocumentWidgetFlag ? 1 : 0)), cstr_String(absoluteUrl_String( d->mod.url, linkUrl_GmDocument(d->doc, run->linkId)))); + linkWasTriggered_DocumentWidget_(d, run->linkId); } setLinkNumberMode_DocumentWidget_(d, iFalse); invalidateVisibleLinks_DocumentWidget_(d); @@ -3995,6 +4030,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e return iTrue; } if (ev->button.button == SDL_BUTTON_MIDDLE && d->hoverLink) { + linkWasTriggered_DocumentWidget_(d, d->hoverLink->linkId); postCommandf_Root(w->root, "open newtab:%d url:%s", (isPinned_DocumentWidget_(d) ? otherRoot_OpenTabFlag : 0) | (modState_Keys() & KMOD_SHIFT ? new_OpenTabFlag : newBackground_OpenTabFlag), @@ -4015,6 +4051,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e init_Array(&items, sizeof(iMenuItem)); if (d->contextLink) { /* Context menu for a link. */ + linkWasTriggered_DocumentWidget_(d, d->contextLink->linkId); /* perhaps will be triggered */ const iString *linkUrl = linkUrl_GmDocument(d->doc, d->contextLink->linkId); // const int linkFlags = linkFlags_GmDocument(d->doc, d->contextLink->linkId); const iRangecc scheme = urlScheme_String(linkUrl); @@ -4034,23 +4071,30 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e /* Regular links that we can open. */ pushBackN_Array( &items, - (iMenuItem[]){ - { openTab_Icon " ${link.newtab}", - 0, - 0, - format_CStr("!open newtab:1 url:%s", cstr_String(linkUrl)) }, - { openTabBg_Icon " ${link.newtab.background}", - 0, - 0, - format_CStr("!open newtab:2 url:%s", cstr_String(linkUrl)) }, - { "${link.side}", - 0, - 0, - format_CStr("!open newtab:4 url:%s", cstr_String(linkUrl)) }, - { "${link.side.newtab}", - 0, - 0, - format_CStr("!open newtab:5 url:%s", cstr_String(linkUrl)) } }, + (iMenuItem[]){ { openTab_Icon " ${link.newtab}", + 0, + 0, + format_CStr("!open newtab:1 origin:%s url:%s", + cstr_String(id_Widget(w)), + cstr_String(linkUrl)) }, + { openTabBg_Icon " ${link.newtab.background}", + 0, + 0, + format_CStr("!open newtab:2 origin:%s url:%s", + cstr_String(id_Widget(w)), + cstr_String(linkUrl)) }, + { "${link.side}", + 0, + 0, + format_CStr("!open newtab:4 origin:%s url:%s", + cstr_String(id_Widget(w)), + cstr_String(linkUrl)) }, + { "${link.side.newtab}", + 0, + 0, + format_CStr("!open newtab:5 origin:%s url:%s", + cstr_String(id_Widget(w)), + cstr_String(linkUrl)) } }, 4); if (deviceType_App() == phone_AppDeviceType) { removeN_Array(&items, size_Array(&items) - 2, iInvalidSize); @@ -4072,7 +4116,9 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e { isGemini ? "${link.noproxy}" : openExt_Icon " ${link.browser}", 0, 0, - format_CStr("!open noproxy:1 url:%s", cstr_String(linkUrl)) } }, + format_CStr("!open origin:%s noproxy:1 url:%s", + cstr_String(id_Widget(w)), + cstr_String(linkUrl)) } }, 2); } iString *linkLabel = collectNewRange_String( @@ -4414,6 +4460,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e if (isPinned_DocumentWidget_(d)) { tabMode ^= otherRoot_OpenTabFlag; } + linkWasTriggered_DocumentWidget_(d, linkId); postCommandf_Root(w->root, "open newtab:%d url:%s", tabMode, cstr_String(absoluteUrl_String( @@ -5495,6 +5542,13 @@ iDocumentWidget *duplicate_DocumentWidget(const iDocumentWidget *orig) { return d; } +void setOrigin_DocumentWidget(iDocumentWidget *d, const iDocumentWidget *other) { + if (d != other) { + /* TODO: Could remember the other's ID? */ + set_String(&d->linePrecedingLink, &other->linePrecedingLink); + } +} + void setUrl_DocumentWidget(iDocumentWidget *d, const iString *url) { setUrlFlags_DocumentWidget(d, url, 0); } diff --git a/src/ui/documentwidget.h b/src/ui/documentwidget.h index c97fa0ba..1405f19d 100644 --- a/src/ui/documentwidget.h +++ b/src/ui/documentwidget.h @@ -53,6 +53,7 @@ enum iDocumentWidgetSetUrlFlags { openedFromSidebar_DocumentWidgetSetUrlFlag = iBit(2), }; +void setOrigin_DocumentWidget (iDocumentWidget *, const iDocumentWidget *other); void setUrl_DocumentWidget (iDocumentWidget *, const iString *url); void setUrlFlags_DocumentWidget (iDocumentWidget *, const iString *url, int setUrlFlags); void setUrlAndSource_DocumentWidget (iDocumentWidget *, const iString *url, const iString *mime, const iBlock *source); diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index b910f905..b94e0c27 100644 --- a/src/ui/inputwidget.c +++ b/src/ui/inputwidget.c @@ -1163,6 +1163,12 @@ void selectAll_InputWidget(iInputWidget *d) { #endif } +void validate_InputWidget(iInputWidget *d) { + if (d->validator) { + d->validator(d, d->validatorContext); /* this may change the contents */ + } +} + iLocalDef iBool isEditing_InputWidget_(const iInputWidget *d) { return (flags_Widget(constAs_Widget(d)) & selected_WidgetFlag) != 0; } @@ -1653,9 +1659,7 @@ void setEatEscape_InputWidget(iInputWidget *d, iBool eatEscape) { } static void contentsWereChanged_InputWidget_(iInputWidget *d) { - if (d->validator) { - d->validator(d, d->validatorContext); /* this may change the contents */ - } + validate_InputWidget(d); if (d->inFlags & notifyEdits_InputWidgetFlag) { postCommand_Widget(d, "input.edited id:%s", cstr_String(id_Widget(constAs_Widget(d)))); } diff --git a/src/ui/inputwidget.h b/src/ui/inputwidget.h index f70c81af..5a61ec22 100644 --- a/src/ui/inputwidget.h +++ b/src/ui/inputwidget.h @@ -57,6 +57,7 @@ void setBackupFileName_InputWidget (iInputWidget *, const char *fileName); void begin_InputWidget (iInputWidget *); void end_InputWidget (iInputWidget *, iBool accept); void selectAll_InputWidget (iInputWidget *); +void validate_InputWidget (iInputWidget *); void setSelectAllOnFocus_InputWidget (iInputWidget *, iBool selectAllOnFocus); void setSensitiveContent_InputWidget (iInputWidget *, iBool isSensitive); diff --git a/src/ui/mobile.c b/src/ui/mobile.c index bf3eb425..df2a661a 100644 --- a/src/ui/mobile.c +++ b/src/ui/mobile.c @@ -499,7 +499,7 @@ void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) { iLabelWidget *heading = NULL; iWidget * value = NULL; const char * spec = item->label; - const char * id = cstr_Rangecc(range_Command(spec, "id")); + const char * id = cstr_Command(spec, "id"); const char * label = hasLabel_Command(spec, "text") ? suffixPtr_Command(spec, "text") : format_CStr("${%s}", id); @@ -580,7 +580,7 @@ void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) { addChildFlags_Widget(widget, iClob(sep), 0); } isFirst = iFalse; - const char * radId = cstr_Rangecc(range_Command(radioItem->label, "id")); + const char * radId = cstr_Command(radioItem->label, "id"); int64_t flags = noBackground_WidgetFlag | frameless_WidgetFlag; if (!isHorizontal) { flags |= alignLeft_WidgetFlag; @@ -590,7 +590,7 @@ void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) { const char *radLabel = hasLabel_Command(radioItem->label, "label") ? format_CStr("${%s}", - cstr_Rangecc(range_Command(radioItem->label, "label"))) + cstr_Command(radioItem->label, "label")) : suffixPtr_Command(radioItem->label, "text"); button = new_LabelWidget(radLabel, radioItem->command); flags |= radio_WidgetFlag; @@ -613,7 +613,7 @@ void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) { else if (equal_Command(spec, "input")) { iInputWidget *input = new_InputWidget(argU32Label_Command(spec, "maxlen")); if (hasLabel_Command(spec, "hint")) { - setHint_InputWidget(input, cstr_Lang(cstr_Rangecc(range_Command(spec, "hint")))); + setHint_InputWidget(input, cstr_Lang(cstr_Command(spec, "hint"))); } setId_Widget(as_Widget(input), id); setUrlContent_InputWidget(input, argLabel_Command(spec, "url")); @@ -630,7 +630,7 @@ void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) { iWidget *unit = addChildFlags_Widget( as_Widget(input), iClob(new_LabelWidget( - format_CStr("${%s}", cstr_Rangecc(range_Command(spec, "unit"))), NULL)), + format_CStr("${%s}", cstr_Command(spec, "unit")), NULL)), frameless_WidgetFlag | moveToParentRightEdge_WidgetFlag | resizeToParentHeight_WidgetFlag); setContentPadding_InputWidget(input, -1, width_Widget(unit) - 4 * gap_UI); @@ -800,7 +800,7 @@ void initPanels_Mobile(iWidget *panels, iWidget *parentWidget, const iMenuItem *item = &itemsNullTerminated[i]; if (equal_Command(item->label, "panel")) { haveDetailPanels = iTrue; - const char *id = cstr_Rangecc(range_Command(item->label, "id")); + const char *id = cstr_Command(item->label, "id"); const iString *label = hasLabel_Command(item->label, "text") ? collect_String(suffix_Command(item->label, "text")) : collectNewFormat_String("${%s}", id); diff --git a/src/ui/root.c b/src/ui/root.c index 04586bac..780dc6e7 100644 --- a/src/ui/root.c +++ b/src/ui/root.c @@ -441,7 +441,7 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) { return iFalse; } else if (equal_Command(cmd, "focus.set")) { - setFocus_Widget(findWidget_App(cstr_Rangecc(range_Command(cmd, "id")))); + setFocus_Widget(findWidget_App(cstr_Command(cmd, "id"))); return iTrue; } else if (equal_Command(cmd, "input.resized")) { diff --git a/src/ui/util.c b/src/ui/util.c index a46c7f80..58e49230 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -1392,7 +1392,7 @@ static iBool tabSwitcher_(iWidget *tabs, const char *cmd) { if (equal_Command(cmd, "tabs.switch")) { iWidget *target = pointerLabel_Command(cmd, "page"); if (!target) { - target = findChild_Widget(tabs, cstr_Rangecc(range_Command(cmd, "id"))); + target = findChild_Widget(tabs, cstr_Command(cmd, "id")); } if (!target) return iFalse; unfocusFocusInsideTabPage_(currentTabPage_Widget(tabs)); @@ -1720,6 +1720,12 @@ iBool valueInputHandler_(iWidget *dlg, const char *cmd) { } return iFalse; } + else if (equal_Command(cmd, "valueinput.set")) { + iInputWidget *input = findChild_Widget(dlg, "input"); + setTextCStr_InputWidget(input, suffixPtr_Command(cmd, "text")); + validate_InputWidget(input); + return iTrue; + } else if (equal_Command(cmd, "valueinput.cancel")) { postCommandf_App("valueinput.cancelled id:%s", cstr_String(id_Widget(dlg))); setId_Widget(dlg, ""); /* no further commands to emit */ @@ -3317,7 +3323,7 @@ static const iMenuItem languages[] = { static iBool translationHandler_(iWidget *dlg, const char *cmd) { iUnused(dlg); if (equal_Command(cmd, "xlt.lang")) { - const iMenuItem *langItem = &languages[languageIndex_CStr(cstr_Rangecc(range_Command(cmd, "id")))]; + const iMenuItem *langItem = &languages[languageIndex_CStr(cstr_Command(cmd, "id"))]; iWidget *widget = pointer_Command(cmd); iLabelWidget *drop; if (flags_Widget(widget) & nativeMenu_WidgetFlag) { @@ -3337,7 +3343,7 @@ const char *languageId_String(const iString *menuItemLabel) { iForIndices(i, languages) { if (!languages[i].label) break; if (!cmp_String(menuItemLabel, translateCStr_Lang(languages[i].label))) { - return cstr_Rangecc(range_Command(languages[i].command, "id")); + return cstr_Command(languages[i].command, "id"); } } return ""; diff --git a/src/ui/window.c b/src/ui/window.c index a4929f51..0e13a57f 100644 --- a/src/ui/window.c +++ b/src/ui/window.c @@ -552,6 +552,7 @@ void init_MainWindow(iMainWindow *d, iRect rect) { d->splitMode = 0; d->pendingSplitMode = 0; d->pendingSplitUrl = new_String(); + d->pendingSplitOrigin = new_String(); d->place.initialPos = rect.pos; d->place.normalRect = rect; d->place.lastNotifiedSize = zero_I2(); @@ -634,6 +635,7 @@ void deinit_MainWindow(iMainWindow *d) { if (theMainWindow_ == d) { theMainWindow_ = NULL; } + delete_String(d->pendingSplitOrigin); delete_String(d->pendingSplitUrl); deinit_Window(&d->base); } @@ -1528,9 +1530,11 @@ void setSplitMode_MainWindow(iMainWindow *d, int splitFlags) { } } if (!isEmpty_String(d->pendingSplitUrl)) { - postCommandf_Root(w->roots[newRootIndex], "open url:%s", + postCommandf_Root(w->roots[newRootIndex], "open origin:%s url:%s", + cstr_String(d->pendingSplitOrigin), cstr_String(d->pendingSplitUrl)); clear_String(d->pendingSplitUrl); + clear_String(d->pendingSplitOrigin); } else if (~splitFlags & noEvents_WindowSplit) { iWidget *docTabs0 = findChild_Widget(w->roots[newRootIndex ^ 1]->widget, "doctabs"); diff --git a/src/ui/window.h b/src/ui/window.h index 6c921f09..ae111f4c 100644 --- a/src/ui/window.h +++ b/src/ui/window.h @@ -114,6 +114,7 @@ struct Impl_MainWindow { int splitMode; int pendingSplitMode; iString * pendingSplitUrl; /* URL to open in a newly opened split */ + iString * pendingSplitOrigin; /* tab from where split was initiated, if any */ SDL_Texture * appIcon; int keyboardHeight; /* mobile software keyboards */ }; -- cgit v1.2.3 From deccf169a4529746654a5a49d051cd4017254d11 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 16 Dec 2021 11:19:22 +0200 Subject: Updated release notes and AUTHORS --- AUTHORS.md | 20 +++++++++++--------- res/about/version.gmi | 10 +++++++++- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index 3a5c9dce..c9de6f98 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -5,24 +5,26 @@ Lagrange was created by Jaakko Keränen () in July 2020. Legend: `C` code, `T` translation ``` -C 2665 Jaakko Keränen -CT 39 Nikolay Korotkiy +C 2847 Jaakko Keränen +CT 41 Nikolay Korotkiy T 39 Olga Smirnova + T 35 Alyssa Liddell T 28 Anna “CyberTailor” - T 27 Alyssa Liddell T 25 Shibo Lyu - T 22 Страхиња Радић - T 17 MCMic - T 15 Wally Hackenslacker - T 10 Tadeáš Erban - T 10 Xosé M + T 23 Страхиња Радић + T 18 MCMic + T 17 Wally Hackenslacker + T 11 Tadeáš Erban + T 11 Xosé M T 9 Aaron Fischer T 8 El Mau T 7 Waterrail T 5 roy niang T 3 Alex Schroeder T 3 Botond Balázs + T 3 Emir C 2 Alyssa Rosenzweig + T 2 Andrij Mizyk T 2 Arns Udovič C 2 Br0000k <77938600+Br0000k@users.noreply.github.com> T 2 Gabriel de Oliveira Ferreira Machado @@ -33,12 +35,12 @@ C 1 Adam Mizerski C 1 Charles C 1 Dario Vladovic C 1 David Gillies - T 1 Emir T 1 Eric T 1 Marek Ľach C 1 Raph M C 1 SolidHal C 1 Thomas Adam + T 1 Timothée Goguely C 1 Waweic C 1 Waweic C 1 Zach DeCook diff --git a/res/about/version.gmi b/res/about/version.gmi index 90fa2bcd..64a547d4 100644 --- a/res/about/version.gmi +++ b/res/about/version.gmi @@ -8,14 +8,22 @@ ## 1.10 New features: +* Added "Paste Preceding Line" in the input prompt dialog. Potential use cases include user-editable sections on a page and suggested/example input values. * Identity toolbar menu can be used to switch between alternate identities. If you have used multiple identities on one site, this makes it more convenient to switch between them. +* Added option to disable cursor blinking in input fields. +* UI language for Dutch. Changes and enhancements: * Revised layout of the Upload dialog. There is a new edit field that lets you modify the file path of the URL. Identity names are in bold. -* Optimized memory use during UI event processing. +* Optimized UI layout procedure and memory use during UI event processing. +* Audio subsystem is only initialized when actually needed. +* Prevent state file corruption if the app happens to get killed while state is being saved. +* Minor improvements in page caching. Fixes: * Fixed a history caching issue: if there were multiple instances of the same URL in history, only the latest one's content would be used when navigating back/forward. +* Fixed very narrow input fields causing the app to hang. +* Fixed initial scroll position in multiline input fields. * Fixed lookup results list becoming too narrow. ## 1.9.2 -- cgit v1.2.3 From ff428e0fe60d8017dc340018faa62c75fcd24c20 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 16 Dec 2021 13:12:42 +0200 Subject: Cleanup --- src/ui/documentwidget.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 50df05f5..0a24f7e5 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -3846,7 +3846,7 @@ static void beginMarkingSelection_DocumentWidget_(iDocumentWidget *d, iInt2 pos) refresh_Widget(as_Widget(d)); } -static void linkWasTriggered_DocumentWidget_(iDocumentWidget *d, iGmLinkId id) { +static void interactingWithLink_DocumentWidget_(iDocumentWidget *d, iGmLinkId id) { iRangecc loc = linkUrlRange_GmDocument(d->doc, id); if (!loc.start) { clear_String(&d->linePrecedingLink); @@ -3912,7 +3912,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e : (d->flags & newTabViaHomeKeys_DocumentWidgetFlag ? 1 : 0)), cstr_String(absoluteUrl_String( d->mod.url, linkUrl_GmDocument(d->doc, run->linkId)))); - linkWasTriggered_DocumentWidget_(d, run->linkId); + interactingWithLink_DocumentWidget_(d, run->linkId); } setLinkNumberMode_DocumentWidget_(d, iFalse); invalidateVisibleLinks_DocumentWidget_(d); @@ -4030,7 +4030,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e return iTrue; } if (ev->button.button == SDL_BUTTON_MIDDLE && d->hoverLink) { - linkWasTriggered_DocumentWidget_(d, d->hoverLink->linkId); + interactingWithLink_DocumentWidget_(d, d->hoverLink->linkId); postCommandf_Root(w->root, "open newtab:%d url:%s", (isPinned_DocumentWidget_(d) ? otherRoot_OpenTabFlag : 0) | (modState_Keys() & KMOD_SHIFT ? new_OpenTabFlag : newBackground_OpenTabFlag), @@ -4051,7 +4051,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e init_Array(&items, sizeof(iMenuItem)); if (d->contextLink) { /* Context menu for a link. */ - linkWasTriggered_DocumentWidget_(d, d->contextLink->linkId); /* perhaps will be triggered */ + interactingWithLink_DocumentWidget_(d, d->contextLink->linkId); /* perhaps will be triggered */ const iString *linkUrl = linkUrl_GmDocument(d->doc, d->contextLink->linkId); // const int linkFlags = linkFlags_GmDocument(d->doc, d->contextLink->linkId); const iRangecc scheme = urlScheme_String(linkUrl); @@ -4460,7 +4460,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e if (isPinned_DocumentWidget_(d)) { tabMode ^= otherRoot_OpenTabFlag; } - linkWasTriggered_DocumentWidget_(d, linkId); + interactingWithLink_DocumentWidget_(d, linkId); postCommandf_Root(w->root, "open newtab:%d url:%s", tabMode, cstr_String(absoluteUrl_String( -- cgit v1.2.3 From 16d6b6199d580d581f5bdb3c51ab92d1a973d31c Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 17 Dec 2021 05:58:51 +0200 Subject: Prefs: Reorganizing booleans --- src/prefs.h | 126 +++++++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 96 insertions(+), 30 deletions(-) diff --git a/src/prefs.h b/src/prefs.h index f22953c5..6c79a3e1 100644 --- a/src/prefs.h +++ b/src/prefs.h @@ -37,76 +37,142 @@ enum iPrefsString { uiLanguage_PrefsString, downloadDir_PrefsString, searchUrl_PrefsString, + /* Network */ caFile_PrefsString, caPath_PrefsString, geminiProxy_PrefsString, gopherProxy_PrefsString, httpProxy_PrefsString, + /* Style */ uiFont_PrefsString, headingFont_PrefsString, bodyFont_PrefsString, monospaceFont_PrefsString, monospaceDocumentFont_PrefsString, + + /* Meta */ max_PrefsString }; -/* TODO: Refactor at least the boolean values into an array for easier manipulation. - Then they can be (de)serialized as a group. Need to use a systematic command naming - convention for notifications. */ +/* Note: These match match the array/struct in Prefs. */ +enum iPrefsBool { + /* Window and User Interface */ + useSystemTheme_PrefsBool, + customFrame_PrefsBool, + retainWindowSize_PrefsBool, + uiAnimations_PrefsBool, + hideToolbarOnScroll_PrefsBool, + + blinkingCursor_PrefsBool, + + /* Document presentation */ + sideIcon_PrefsBool, + time24h_PrefsBool, + + /* Behavior */ + hoverLink_PrefsBool, + smoothScrolling_PrefsBool, + loadImageInsteadOfScrolling_PrefsBool, + collapsePreOnLoad_PrefsBool, + openArchiveIndexPages_PrefsBool, + + addBookmarksToBottom_PrefsBool, + warnAboutMissingGlyphs_PrefsBool, + + /* Network */ + decodeUserVisibleURLs_PrefsBool, + + /* Style */ + monospaceGemini_PrefsBool, + monospaceGopher_PrefsBool, + boldLinkVisited_PrefsBool, + boldLinkDark_PrefsBool, + boldLinkLight_PrefsBool, + + fontSmoothing_PrefsBool, + bigFirstParagraph_PrefsBool, + quoteIcon_PrefsBool, + centerShortDocs_PrefsBool, + plainTextWrap_PrefsBool, + + /* Meta */ + max_PrefsBool +}; + +/* TODO: Use a systematic command naming convention for notifications. */ + struct Impl_Prefs { - iString strings[max_PrefsString]; + iString strings[max_PrefsString]; + union { + iBool bools[max_PrefsBool]; + /* For convenience, contents of the array are accessible also via these members. */ + struct { + /* Window and User Interface */ + iBool useSystemTheme; + iBool customFrame; /* when LAGRANGE_ENABLE_CUSTOM_FRAME is defined */ + iBool retainWindowSize; + iBool uiAnimations; + iBool hideToolbarOnScroll; + + iBool blinkingCursor; + + /* Document presentation */ + iBool sideIcon; + iBool time24h; + + /* Behavior */ + iBool hoverLink; + iBool smoothScrolling; + iBool loadImageInsteadOfScrolling; + iBool collapsePreOnLoad; + iBool openArchiveIndexPages; + + iBool addBookmarksToBottom; + iBool warnAboutMissingGlyphs; + + /* Network */ + iBool decodeUserVisibleURLs; + + /* Style */ + iBool monospaceGemini; + iBool monospaceGopher; + iBool boldLinkVisited; + iBool boldLinkDark; + iBool boldLinkLight; + + iBool fontSmoothing; + iBool bigFirstParagraph; + iBool quoteIcon; + iBool centerShortDocs; + iBool plainTextWrap; + }; + }; /* UI state (belongs to state.lgr...) */ int dialogTab; int langFrom; int langTo; /* Colors */ - iBool useSystemTheme; enum iColorTheme systemPreferredColorTheme[2]; /* dark, light */ enum iColorTheme theme; enum iColorAccent accent; /* Window and User Interface */ - iBool customFrame; /* when LAGRANGE_ENABLE_CUSTOM_FRAME is defined */ - iBool retainWindowSize; - iBool uiAnimations; float uiScale; - iBool hideToolbarOnScroll; - iBool blinkingCursor; /* Document presentation */ int zoomPercent; - iBool sideIcon; - iBool time24h; /* Behavior */ int pinSplit; /* 0: no pinning, 1: left doc, 2: right doc */ int returnKey; - iBool hoverLink; - iBool smoothScrolling; int smoothScrollSpeed[max_ScrollType]; - iBool loadImageInsteadOfScrolling; - iBool collapsePreOnLoad; - iBool openArchiveIndexPages; - iBool addBookmarksToBottom; - iBool warnAboutMissingGlyphs; /* Network */ - iBool decodeUserVisibleURLs; int maxCacheSize; /* MB */ int maxMemorySize; /* MB */ /* Style */ iStringSet * disabledFontPacks; - iBool fontSmoothing; int gemtextAnsiEscapes; - iBool monospaceGemini; - iBool monospaceGopher; - iBool boldLinkVisited; - iBool boldLinkDark; - iBool boldLinkLight; int lineWidth; float lineSpacing; - iBool bigFirstParagraph; - iBool quoteIcon; - iBool centerShortDocs; - iBool plainTextWrap; enum iImageStyle imageStyle; /* Colors */ enum iGmDocumentTheme docThemeDark; -- cgit v1.2.3 From 80e64189df25aafe461fb9fce107025fee2594dc Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 17 Dec 2021 05:59:38 +0200 Subject: macOS: Trackpad swipe navigation Work in progress. Something breaks down when swiping forward twice... --- src/app.c | 22 -------- src/macos.m | 103 ++++++++++++++++++++++++++++++++++ src/ui/documentwidget.c | 144 +++++++++++++++++++++++++++++++++++++++++++++--- src/ui/util.h | 19 ++++++- 4 files changed, 255 insertions(+), 33 deletions(-) diff --git a/src/app.c b/src/app.c index accdd991..68b17ade 100644 --- a/src/app.c +++ b/src/app.c @@ -1315,28 +1315,6 @@ void processEvents_App(enum iAppEventMode eventMode) { } /* Scroll events may be per-pixel or mouse wheel steps. */ if (ev.type == SDL_MOUSEWHEEL) { -#if defined (iPlatformAppleDesktop) - /* On macOS, we handle both trackpad and mouse events. We expect SDL to identify - which device is sending the event. */ - if (ev.wheel.which == 0) { - /* Trackpad with precise scrolling w/inertia (points). */ - setPerPixel_MouseWheelEvent(&ev.wheel, iTrue); - ev.wheel.x *= -d->window->base.pixelRatio; - ev.wheel.y *= d->window->base.pixelRatio; - /* Only scroll on one axis at a time. */ - if (iAbs(ev.wheel.x) > iAbs(ev.wheel.y)) { - ev.wheel.y = 0; - } - else { - ev.wheel.x = 0; - } - } - else { - /* Disregard wheel acceleration applied by the OS. */ - ev.wheel.x = -ev.wheel.x; - ev.wheel.y = iSign(ev.wheel.y); - } -#endif #if defined (iPlatformMsys) ev.wheel.x = -ev.wheel.x; #endif diff --git a/src/macos.m b/src/macos.m index 5fd76bb9..2d082d28 100644 --- a/src/macos.m +++ b/src/macos.m @@ -403,6 +403,94 @@ void registerURLHandler_MacOS(void) { [handler release]; } +#if 0 +static iBool isTracking_; + +static void trackSwipe_(NSEvent *event) { + if (isTracking_) { + return; + } + isTracking_ = iTrue; + [event trackSwipeEventWithOptions:NSEventSwipeTrackingLockDirection + dampenAmountThresholdMin:-1.0 + max:1.0 + usingHandler:^(CGFloat gestureAmount, NSEventPhase phase, + BOOL isComplete, BOOL *stop) { + printf("TRACK: amount:%f phase:%lu complete:%d\n", + gestureAmount, (unsigned long) phase, isComplete); + fflush(stdout); + if (isComplete) { + isTracking_ = iFalse; + } + } + ]; +} +#endif + +static int swipeDir_ = 0; + +static iBool processScrollWheelEvent_(NSEvent *event) { + const iBool isPerPixel = (event.hasPreciseScrollingDeltas != 0); + const iBool isInertia = (event.momentumPhase & (NSEventPhaseBegan | NSEventPhaseChanged)) != 0; + const iBool isEnded = event.scrollingDeltaX == 0.0f && event.scrollingDeltaY == 0.0f && !isInertia; + const iWindow *win = &get_MainWindow()->base; + /* Post corresponding MOUSEWHEEL events. */ + SDL_MouseWheelEvent e = { .type = SDL_MOUSEWHEEL }; + e.timestamp = SDL_GetTicks(); + e.which = isPerPixel ? 0 : 1; /* Distinction between trackpad and regular mouse. TODO: Still needed? */ + setPerPixel_MouseWheelEvent(&e, isPerPixel); + if (isPerPixel) { + setInertia_MouseWheelEvent(&e, isInertia); + setScrollFinished_MouseWheelEvent(&e, isEnded); + e.x = -event.scrollingDeltaX * win->pixelRatio; + e.y = event.scrollingDeltaY * win->pixelRatio; + /* Only scroll on one axis at a time. */ + if (swipeDir_ == 0) { + swipeDir_ = iAbs(e.x) > iAbs(e.y) ? 1 : 2; + } + if (swipeDir_ == 1) { + e.y = 0; + } + else if (swipeDir_ == 2) { + e.x = 0; + } + if (isEnded) { + swipeDir_ = 0; + } + } + else { + /* Disregard wheel acceleration applied by the OS. */ + e.x = -event.scrollingDeltaX; + e.y = iSign(event.scrollingDeltaY); + } +// printf("#### dx:%d dy:%d phase:%ld end:%d\n", e.x, e.y, (long) event.momentumPhase, isEnded); fflush(stdout); + SDL_PushEvent((SDL_Event *) &e); +#if 0 + /* On macOS, we handle both trackpad and mouse events. We expect SDL to identify + which device is sending the event. */ + if (ev.wheel.which == 0) { + /* Trackpad with precise scrolling w/inertia (points). */ + setPerPixel_MouseWheelEvent(&ev.wheel, iTrue); + ev.wheel.x *= -d->window->base.pixelRatio; + ev.wheel.y *= d->window->base.pixelRatio; + /* Only scroll on one axis at a time. */ + if (iAbs(ev.wheel.x) > iAbs(ev.wheel.y)) { + ev.wheel.y = 0; + } + else { + ev.wheel.x = 0; + } + } + else { + /* Disregard wheel acceleration applied by the OS. */ + ev.wheel.x = -ev.wheel.x; + ev.wheel.y = iSign(ev.wheel.y); + } +#endif + + return iTrue; +} + void setupApplication_MacOS(void) { NSApplication *app = [NSApplication sharedApplication]; [app setActivationPolicy:NSApplicationActivationPolicyRegular]; @@ -424,6 +512,21 @@ void setupApplication_MacOS(void) { NSMenuItem *windowCloseItem = [windowMenu itemWithTitle:@"Close"]; windowCloseItem.target = myDel; windowCloseItem.action = @selector(closeTab); + [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskScrollWheel + handler:^NSEvent*(NSEvent *event){ +// printf("event type: %lu\n", (unsigned long) event.type); +// fflush(stdout); +// if (event.type == NSEventTypeGesture) { +// trackSwipe_(event); +// printf("GESTURE phase:%lu\n", (unsigned long) event.phase); +//fflush(stdout); +// } + if (event.type == NSEventTypeScrollWheel && + processScrollWheelEvent_(event)) { + return nil; /* was eaten */ + } + return event; + }]; } void hideTitleBar_MacOS(iWindow *window) { diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 0a24f7e5..9f6cdc45 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -234,6 +234,10 @@ enum iDocumentWidgetFlag { fromCache_DocumentWidgetFlag = iBit(16), /* don't write anything to cache */ animationPlaceholder_DocumentWidgetFlag = iBit(17), /* avoid slow operations */ invalidationPending_DocumentWidgetFlag = iBit(18), /* invalidate as soon as convenient */ + leftWheelSwipe_DocumentWidgetFlag = iBit(19), /* swipe state flags are used on desktop */ + rightWheelSwipe_DocumentWidgetFlag = iBit(20), + eitherWheelSwipe_DocumentWidgetFlag = leftWheelSwipe_DocumentWidgetFlag | rightWheelSwipe_DocumentWidgetFlag, +// wheelSwipeFinished_DocumentWidgetFlag = iBit(21), }; enum iDocumentLinkOrdinalMode { @@ -241,6 +245,12 @@ enum iDocumentLinkOrdinalMode { homeRow_DocumentLinkOrdinalMode, }; +enum iWheelSwipeState { + none_WheelSwipeState, + direct_WheelSwipeState, + //inertia_WheelSwipeState, +}; + struct Impl_DocumentWidget { iWidget widget; int flags; /* internal behavior, see enum iDocumentWidgetFlag */ @@ -263,6 +273,9 @@ struct Impl_DocumentWidget { int pinchZoomInitial; int pinchZoomPosted; float swipeSpeed; /* points/sec */ + uint32_t lastSwipeTime; + int wheelSwipeDistance; + enum iWheelSwipeState wheelSwipeState; iString pendingGotoHeading; iString linePrecedingLink; @@ -329,7 +342,12 @@ void init_DocumentWidget(iDocumentWidget *d) { init_Widget(w); setId_Widget(w, format_CStr("document%03d", ++docEnum_)); setFlags_Widget(w, hover_WidgetFlag | noBackground_WidgetFlag, iTrue); - if (deviceType_App() != desktop_AppDeviceType) { +#if defined (iPlatformAppleDesktop) + iBool enableSwipeNavigation = iTrue; /* swipes on the trackpad */ +#else + iBool enableSwipeNavigation = (deviceType_App() != desktop_AppDeviceType); +#endif + if (enableSwipeNavigation) { setFlags_Widget(w, leftEdgeDraggable_WidgetFlag | rightEdgeDraggable_WidgetFlag | horizontalOffset_WidgetFlag, iTrue); } @@ -358,6 +376,7 @@ void init_DocumentWidget(iDocumentWidget *d) { } d->animWideRunId = 0; init_Anim(&d->animWideRunOffset, 0); + d->wheelSwipeState = none_WheelSwipeState; d->selectMark = iNullRange; d->foundMark = iNullRange; d->pageMargin = 5; @@ -1942,8 +1961,7 @@ static void updateFromCachedResponse_DocumentWidget_(iDocumentWidget *d, float n static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) { const iRecentUrl *recent = constMostRecentUrl_History(d->mod.history); - iAssert(equalCase_String(&recent->url, d->mod.url)); - if (recent && recent->cachedResponse) { + if (recent && recent->cachedResponse && equalCase_String(&recent->url, d->mod.url)) { iChangeFlags(d->flags, openedFromSidebar_DocumentWidgetFlag, recent->flags.openedFromSidebar); @@ -2043,10 +2061,10 @@ static void scrollToHeading_DocumentWidget_(iDocumentWidget *d, const char *head } } -static void scrollWideBlock_DocumentWidget_(iDocumentWidget *d, iInt2 mousePos, int delta, +static iBool scrollWideBlock_DocumentWidget_(iDocumentWidget *d, iInt2 mousePos, int delta, int duration) { - if (delta == 0) { - return; + if (delta == 0 || d->flags & eitherWheelSwipe_DocumentWidgetFlag) { + return iFalse; } const iInt2 docPos = documentPos_DocumentWidget_(d, mousePos); iConstForEach(PtrArray, i, &d->visibleWideRuns) { @@ -2087,9 +2105,10 @@ static void scrollWideBlock_DocumentWidget_(iDocumentWidget *d, iInt2 mousePos, d->animWideRunId = 0; init_Anim(&d->animWideRunOffset, 0); } - break; + return iTrue; } } + return iFalse; } static void togglePreFold_DocumentWidget_(iDocumentWidget *d, uint16_t preId) { @@ -2691,7 +2710,7 @@ static void setupSwipeOverlay_DocumentWidget_(iDocumentWidget *d, iWidget *overl const int toPos = width_Widget(overlay); setVisualOffset_Widget(overlay, fromPos, 0, 0); /* Bigger screen, faster swipes. */ - const float devFactor = (deviceType_App() == tablet_AppDeviceType ? 2.0f : 1.0f); + const float devFactor = (deviceType_App() == phone_AppDeviceType ? 1.0f : 2.0f); float swipe = iClamp(d->swipeSpeed, devFactor * 400, devFactor * 1000) * gap_UI; uint32_t span = ((toPos - fromPos) / swipe) * 1000; // printf("from:%d to:%d swipe:%f span:%u\n", fromPos, toPos, d->swipeSpeed, span); @@ -2841,6 +2860,7 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { target->flags |= animationPlaceholder_DocumentWidgetFlag; addChildPos_Widget(swipeParent, iClob(target), back_WidgetAddPos); setId_Widget(as_Widget(target), "swipeout"); + setFlags_Widget(as_Widget(target), disabled_WidgetFlag, iTrue); swap_DocumentWidget_(target, d->doc, d); setUrlAndSource_DocumentWidget(d, swipeIn->mod.url, @@ -3870,6 +3890,105 @@ static void interactingWithLink_DocumentWidget_(iDocumentWidget *d, iGmLinkId id setRange_String(&d->linePrecedingLink, loc); } +iLocalDef int wheelSwipeSide_DocumentWidget_(const iDocumentWidget *d) { + return (d->flags & rightWheelSwipe_DocumentWidgetFlag ? 2 + : d->flags & leftWheelSwipe_DocumentWidgetFlag ? 1 + : 0); +} + +static void finishWheelSwipe_DocumentWidget_(iDocumentWidget *d) { + if (d->flags & eitherWheelSwipe_DocumentWidgetFlag && + d->wheelSwipeState == direct_WheelSwipeState) { +// ~d->flags & wheelSwipeFinished_DocumentWidgetFlag) { +// d->wheelSwipeState = inertia_WheelSwipeState; + const int side = wheelSwipeSide_DocumentWidget_(d); +// d->flags |= wheelSwipeFinished_DocumentWidgetFlag; + int abort = (side == 1 && d->swipeSpeed < 0 || side == 2 && d->swipeSpeed > 0); + printf("speed:%f\n", d->swipeSpeed / gap_UI); + if (iAbs(d->wheelSwipeDistance) < width_Widget(d) / 4 && iAbs(d->swipeSpeed) < 4 * gap_UI) { + abort = 1; + } + postCommand_Widget(d, "edgeswipe.ended side:%d abort:%d", side, abort); + d->flags &= ~eitherWheelSwipe_DocumentWidgetFlag; + } +} + +static iBool handleWheelSwipe_DocumentWidget_(iDocumentWidget *d, const SDL_MouseWheelEvent *ev) { + iWidget *w = as_Widget(d); + if (~flags_Widget(w) & horizontalOffset_WidgetFlag) { + return iFalse; + } + iAssert(~d->flags & animationPlaceholder_DocumentWidgetFlag); +// printf("STATE:%d wheel x:%d inert:%d end:%d\n", d->wheelSwipeState, +// ev->x, isInertia_MouseWheelEvent(ev), +// isScrollFinished_MouseWheelEvent(ev)); +// fflush(stdout); + switch (d->wheelSwipeState) { + case none_WheelSwipeState: + /* A new swipe starts. */ + if (!isInertia_MouseWheelEvent(ev) && !isScrollFinished_MouseWheelEvent(ev)) { + int side = ev->x < 0 ? 1 : 2; + d->wheelSwipeDistance = -ev->x; + d->flags &= ~eitherWheelSwipe_DocumentWidgetFlag; + d->flags |= (side == 1 ? leftWheelSwipe_DocumentWidgetFlag + : rightWheelSwipe_DocumentWidgetFlag); + // printf("swipe starts at %d, side %d\n", d->wheelSwipeDistance, side); + d->wheelSwipeState = direct_WheelSwipeState; + d->swipeSpeed = 0; + postCommand_Widget(d, "edgeswipe.moved arg:%d side:%d", d->wheelSwipeDistance, side); + return iTrue; + } + break; + case direct_WheelSwipeState: + if (isInertia_MouseWheelEvent(ev) || isScrollFinished_MouseWheelEvent(ev)) { + finishWheelSwipe_DocumentWidget_(d); + d->wheelSwipeState = none_WheelSwipeState; + } +// else if (isInertia_MouseWheelEvent(ev)) { +// finishWheelSwipe_DocumentWidget_(d); +// d->wheelSwipeState = inertia_WheelSwipeState; +// } + else { + int step = -ev->x * 2; + d->wheelSwipeDistance += step; + /* Remember the maximum speed. */ + if (d->swipeSpeed < 0 && step < 0) { + d->swipeSpeed = iMin(d->swipeSpeed, step); + } + else if (d->swipeSpeed > 0 && step > 0) { + d->swipeSpeed = iMax(d->swipeSpeed, step); + } + else { + d->swipeSpeed = step; + } + switch (wheelSwipeSide_DocumentWidget_(d)) { + case 1: + d->wheelSwipeDistance = iMax(0, d->wheelSwipeDistance); + d->wheelSwipeDistance = iMin(width_Widget(d), d->wheelSwipeDistance); + break; + case 2: + d->wheelSwipeDistance = iMin(0, d->wheelSwipeDistance); + d->wheelSwipeDistance = iMax(-width_Widget(d), d->wheelSwipeDistance); + break; + } + /* TODO: calculate speed, rememeber direction */ + //printf("swipe moved to %d, side %d\n", d->wheelSwipeDistance, side); + postCommand_Widget(d, "edgeswipe.moved arg:%d side:%d", d->wheelSwipeDistance, + wheelSwipeSide_DocumentWidget_(d)); + } + return iTrue; +// case inertia_WheelSwipeState: +// if (isScrollFinished_MouseWheelEvent(ev)) { +// d->wheelSwipeState = none_WheelSwipeState; +// } +// else if (!isInertia_MouseWheelEvent(ev)) { +// d->wheelSwipeState = none_WheelSwipeState; +// } +// return iTrue; + } + return iFalse; +} + static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) { iWidget *w = as_Widget(d); if (isMetricsChange_UserEvent(ev)) { @@ -3967,13 +4086,20 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e #endif } } + else if (ev->type == SDL_MOUSEWHEEL && ev->wheel.y == 0 && + handleWheelSwipe_DocumentWidget_(d, &ev->wheel)) { + return iTrue; + } else if (ev->type == SDL_MOUSEWHEEL && isHover_Widget(w)) { const iInt2 mouseCoord = coord_MouseWheelEvent(&ev->wheel); if (isPerPixel_MouseWheelEvent(&ev->wheel)) { const iInt2 wheel = init_I2(ev->wheel.x, ev->wheel.y); stop_Anim(&d->scrollY.pos); immediateScroll_DocumentWidget_(d, -wheel.y); - scrollWideBlock_DocumentWidget_(d, mouseCoord, -wheel.x, 0); + if (!scrollWideBlock_DocumentWidget_(d, mouseCoord, -wheel.x, 0) && + wheel.x) { + handleWheelSwipe_DocumentWidget_(d, &ev->wheel); + } } else { /* Traditional mouse wheel. */ diff --git a/src/ui/util.h b/src/ui/util.h index b9109aee..4fedd083 100644 --- a/src/ui/util.h +++ b/src/ui/util.h @@ -47,16 +47,31 @@ iLocalDef iBool isMetricsChange_UserEvent(const SDL_Event *d) { } enum iMouseWheelFlag { - perPixel_MouseWheelFlag = iBit(9), /* e.g., trackpad or finger scroll; applied to `direction` */ + /* Note: A future version of SDL may support per-pixel scrolling, but 2.0.x doesn't. */ + perPixel_MouseWheelFlag = iBit(9), /* e.g., trackpad or finger scroll; applied to `direction` */ + inertia_MouseWheelFlag = iBit(10), + scrollFinished_MouseWheelFlag = iBit(11), }; -/* Note: A future version of SDL may support per-pixel scrolling, but 2.0.x doesn't. */ iLocalDef void setPerPixel_MouseWheelEvent(SDL_MouseWheelEvent *ev, iBool set) { iChangeFlags(ev->direction, perPixel_MouseWheelFlag, set); } +iLocalDef void setInertia_MouseWheelEvent(SDL_MouseWheelEvent *ev, iBool set) { + iChangeFlags(ev->direction, inertia_MouseWheelFlag, set); +} +iLocalDef void setScrollFinished_MouseWheelEvent(SDL_MouseWheelEvent *ev, iBool set) { + iChangeFlags(ev->direction, scrollFinished_MouseWheelFlag, set); +} + iLocalDef iBool isPerPixel_MouseWheelEvent(const SDL_MouseWheelEvent *ev) { return (ev->direction & perPixel_MouseWheelFlag) != 0; } +iLocalDef iBool isInertia_MouseWheelEvent(const SDL_MouseWheelEvent *ev) { + return (ev->direction & inertia_MouseWheelFlag) != 0; +} +iLocalDef iBool isScrollFinished_MouseWheelEvent(const SDL_MouseWheelEvent *ev) { + return (ev->direction & scrollFinished_MouseWheelFlag) != 0; +} iInt2 coord_MouseWheelEvent (const SDL_MouseWheelEvent *); -- cgit v1.2.3 From ee6ccc61511812f92be85c694c5d6b5a00426004 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 17 Dec 2021 06:07:02 +0200 Subject: Cleanup --- src/ui/documentwidget.c | 70 +++---------------------------------------------- 1 file changed, 3 insertions(+), 67 deletions(-) diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 9f6cdc45..f4e56d63 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -561,14 +561,6 @@ static int pageHeight_DocumentWidget_(const iDocumentWidget *d) { return height_Banner(d->banner) + documentTopPad_DocumentWidget_(d) + size_GmDocument(d->doc).y; } -//static int footerButtonsHeight_DocumentWidget_(const iDocumentWidget *d) { -// int height = height_Widget(d->footerButtons); -//// if (height) { -//// height += 3 * gap_UI; /* padding */ -//// } -// return height; -//} - static int phoneToolbarHeight_DocumentWidget_(const iDocumentWidget *d) { if (!d->phoneToolbar) { return 0; @@ -631,18 +623,6 @@ static int viewPos_DocumentWidget_(const iDocumentWidget *d) { return height_Banner(d->banner) + documentTopPad_DocumentWidget_(d) - pos_SmoothScroll(&d->scrollY); } -#if 0 -static iRect siteBannerRect_DocumentWidget_(const iDocumentWidget *d) { - const iGmRun *banner = siteBanner_GmDocument(d->doc); - if (!banner) { - return zero_Rect(); - } - const iRect docBounds = documentBounds_DocumentWidget_(d); - const iInt2 origin = addY_I2(topLeft_Rect(docBounds), -pos_SmoothScroll(&d->scrollY)); - return moved_Rect(banner->visBounds, origin); -} -#endif - static iInt2 documentPos_DocumentWidget_(const iDocumentWidget *d, iInt2 pos) { return addY_I2(sub_I2(pos, topLeft_Rect(documentBounds_DocumentWidget_(d))), -viewPos_DocumentWidget_(d)); @@ -3893,18 +3873,14 @@ static void interactingWithLink_DocumentWidget_(iDocumentWidget *d, iGmLinkId id iLocalDef int wheelSwipeSide_DocumentWidget_(const iDocumentWidget *d) { return (d->flags & rightWheelSwipe_DocumentWidgetFlag ? 2 : d->flags & leftWheelSwipe_DocumentWidgetFlag ? 1 - : 0); + : 0); } static void finishWheelSwipe_DocumentWidget_(iDocumentWidget *d) { if (d->flags & eitherWheelSwipe_DocumentWidgetFlag && d->wheelSwipeState == direct_WheelSwipeState) { -// ~d->flags & wheelSwipeFinished_DocumentWidgetFlag) { -// d->wheelSwipeState = inertia_WheelSwipeState; const int side = wheelSwipeSide_DocumentWidget_(d); -// d->flags |= wheelSwipeFinished_DocumentWidgetFlag; int abort = (side == 1 && d->swipeSpeed < 0 || side == 2 && d->swipeSpeed > 0); - printf("speed:%f\n", d->swipeSpeed / gap_UI); if (iAbs(d->wheelSwipeDistance) < width_Widget(d) / 4 && iAbs(d->swipeSpeed) < 4 * gap_UI) { abort = 1; } @@ -3944,10 +3920,6 @@ static iBool handleWheelSwipe_DocumentWidget_(iDocumentWidget *d, const SDL_Mous finishWheelSwipe_DocumentWidget_(d); d->wheelSwipeState = none_WheelSwipeState; } -// else if (isInertia_MouseWheelEvent(ev)) { -// finishWheelSwipe_DocumentWidget_(d); -// d->wheelSwipeState = inertia_WheelSwipeState; -// } else { int step = -ev->x * 2; d->wheelSwipeDistance += step; @@ -3977,14 +3949,6 @@ static iBool handleWheelSwipe_DocumentWidget_(iDocumentWidget *d, const SDL_Mous wheelSwipeSide_DocumentWidget_(d)); } return iTrue; -// case inertia_WheelSwipeState: -// if (isScrollFinished_MouseWheelEvent(ev)) { -// d->wheelSwipeState = none_WheelSwipeState; -// } -// else if (!isInertia_MouseWheelEvent(ev)) { -// d->wheelSwipeState = none_WheelSwipeState; -// } -// return iTrue; } return iFalse; } @@ -4627,6 +4591,8 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e return processEvent_Widget(w, ev); } +/*----------------------------------------------------------------------------------------------*/ + iDeclareType(DrawContext) struct Impl_DrawContext { @@ -4753,15 +4719,6 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { /* Preformatted runs can be scrolled. */ runOffset_DocumentWidget_(d->widget, run)); const iRect visRect = { visPos, run->visBounds.size }; -#if 0 - if (run->flags & footer_GmRunFlag) { - iRect footerBack = - (iRect){ visPos, init_I2(width_Rect(d->widgetBounds), run->visBounds.size.y) }; - footerBack.pos.x = left_Rect(d->widgetBounds); - fillRect_Paint(&d->paint, footerBack, tmBackground_ColorId); - return; - } -#endif /* Fill the background. */ { if (run->linkId && linkFlags & isOpen_GmLinkFlag && ~linkFlags & content_GmLinkFlag) { /* Open links get a highlighted background. */ @@ -4814,16 +4771,6 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { run->color, run->text); } -#if 0 - else if (run->flags & siteBanner_GmRunFlag) { - /* Banner background. */ - iRect bannerBack = initCorners_Rect(topLeft_Rect(d->widgetBounds), - init_I2(right_Rect(bounds_Widget(constAs_Widget(d->widget))), - visPos.y + height_Rect(run->visBounds))); - fillRect_Paint(&d->paint, bannerBack, tmBannerBackground_ColorId); - drawBannerRun_DrawContext_(d, run, visPos); - } -#endif else { if (d->showLinkNumbers && run->linkId && run->flags & decoration_GmRunFlag) { const size_t ord = visibleLinkOrdinal_DocumentWidget_(d->widget, run->linkId); @@ -5722,18 +5669,7 @@ void updateSize_DocumentWidget(iDocumentWidget *d) { arrange_Widget(d->footerButtons); } -#if 0 -static void sizeChanged_DocumentWidget_(iDocumentWidget *d) { - if (current_Root()) { - /* TODO: This gets called more than once during a single arrange. - It could be done via some sort of callback instead. */ - updateVisible_DocumentWidget_(d); - } -} -#endif - iBeginDefineSubclass(DocumentWidget, Widget) .processEvent = (iAny *) processEvent_DocumentWidget_, .draw = (iAny *) draw_DocumentWidget_, -// .sizeChanged = (iAny *) sizeChanged_DocumentWidget_, iEndDefineSubclass(DocumentWidget) -- cgit v1.2.3 From 51a46787f7e37a5b9afac4d895e117824d7fce38 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 17 Dec 2021 07:02:34 +0200 Subject: App: Fixed ticker management issue Multiple tickers with the same context would override each other. The ticker callback must be considered as well or tickers may fail to run as expected. --- src/app.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app.c b/src/app.c index 68b17ade..86ef8b73 100644 --- a/src/app.c +++ b/src/app.c @@ -159,7 +159,11 @@ struct Impl_Ticker { static int cmp_Ticker_(const void *a, const void *b) { const iTicker *elems[2] = { a, b }; - return iCmp(elems[0]->context, elems[1]->context); + const int cmp = iCmp(elems[0]->context, elems[1]->context); + if (cmp) { + return cmp; + } + return iCmp((void *) elems[0]->callback, (void *) elems[1]->callback); } /*----------------------------------------------------------------------------------------------*/ -- cgit v1.2.3 From 8298f3fc47d2c859ffd1cd72f6b4b0c480565bcf Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 17 Dec 2021 07:04:07 +0200 Subject: DocumentWidget: Swipe animation glitches Most crucially, sometimes the swipe placeholders would not get deleted at all because the visual offset animation was not finishing as expected. This would cause a number of problems with the document behavior. --- src/ui/documentwidget.c | 30 ++++++++++++++++++++++-------- src/ui/widget.c | 12 +++++++++++- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index f4e56d63..d0a9a55d 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -445,6 +445,8 @@ void cancelAllRequests_DocumentWidget(iDocumentWidget *d) { } void deinit_DocumentWidget(iDocumentWidget *d) { +// printf("\n* * * * * * * *\nDEINIT DOCUMENT: %s\n* * * * * * * *\n\n", +// cstr_String(&d->widget.id)); fflush(stdout); cancelAllRequests_DocumentWidget(d); pauseAllPlayers_Media(media_GmDocument(d->doc), iTrue); removeTicker_App(animate_DocumentWidget_, d); @@ -2690,12 +2692,17 @@ static void setupSwipeOverlay_DocumentWidget_(iDocumentWidget *d, iWidget *overl const int toPos = width_Widget(overlay); setVisualOffset_Widget(overlay, fromPos, 0, 0); /* Bigger screen, faster swipes. */ - const float devFactor = (deviceType_App() == phone_AppDeviceType ? 1.0f : 2.0f); - float swipe = iClamp(d->swipeSpeed, devFactor * 400, devFactor * 1000) * gap_UI; - uint32_t span = ((toPos - fromPos) / swipe) * 1000; -// printf("from:%d to:%d swipe:%f span:%u\n", fromPos, toPos, d->swipeSpeed, span); - setVisualOffset_Widget(overlay, toPos, span, deviceType_App() == tablet_AppDeviceType ? - easeOut_AnimFlag : 0); + if (deviceType_App() == desktop_AppDeviceType) { + setVisualOffset_Widget(overlay, toPos, 250, easeOut_AnimFlag | softer_AnimFlag); + } + else { + const float devFactor = (deviceType_App() == phone_AppDeviceType ? 1.0f : 2.0f); + float swipe = iClamp(d->swipeSpeed, devFactor * 400, devFactor * 1000) * gap_UI; + uint32_t span = ((toPos - fromPos) / swipe) * 1000; + // printf("from:%d to:%d swipe:%f span:%u\n", fromPos, toPos, d->swipeSpeed, span); + setVisualOffset_Widget(overlay, toPos, span, deviceType_App() == tablet_AppDeviceType ? + easeOut_AnimFlag : 0); + } setVisualOffset_Widget(w, 0, 0, 0); } @@ -2722,9 +2729,12 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { return iTrue; } iWidget *swipeParent = swipeParent_DocumentWidget_(d); + if (findChild_Widget(swipeParent, "swipeout")) { + return iTrue; /* too fast, previous animation hasn't finished */ + } /* The temporary "swipein" will display the previous page until the finger is lifted. */ iDocumentWidget *swipeIn = findChild_Widget(swipeParent, "swipein"); - if (!swipeIn) { + if (!swipeIn) { swipeIn = new_DocumentWidget(); swipeIn->flags |= animationPlaceholder_DocumentWidgetFlag; setId_Widget(as_Widget(swipeIn), "swipein"); @@ -2761,12 +2771,15 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { if (offset < -get_Window()->pixelRatio * 10) { int animSpan = 10; if (!atNewest_History(d->mod.history) && ~flags_Widget(w) & dragged_WidgetFlag) { + iWidget *swipeParent = swipeParent_DocumentWidget_(d); + if (findChild_Widget(swipeParent, "swipeout")) { + return iTrue; /* too fast, previous animation hasn't finished */ + } /* Setup the drag. `d` will be moving with the finger. */ animSpan = 0; postCommand_Widget(d, "navigate.forward"); setFlags_Widget(w, dragged_WidgetFlag, iTrue); /* Set up the swipe dummy. */ - iWidget *swipeParent = swipeParent_DocumentWidget_(d); iDocumentWidget *target = new_DocumentWidget(); target->flags |= animationPlaceholder_DocumentWidgetFlag; setId_Widget(as_Widget(target), "swipeout"); @@ -2820,6 +2833,7 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { // setupSwipeOverlay_DocumentWidget_(d, as_Widget(swipeOut)); return iTrue; } + iAssert(~d->flags & animationPlaceholder_DocumentWidgetFlag); setFlags_Widget(w, dragged_WidgetFlag, iFalse); setVisualOffset_Widget(w, 0, 250, easeOut_AnimFlag | softer_AnimFlag); return iTrue; diff --git a/src/ui/widget.c b/src/ui/widget.c index df74a744..1b90378a 100644 --- a/src/ui/widget.c +++ b/src/ui/widget.c @@ -144,8 +144,10 @@ static void visualOffsetAnimation_Widget_(void *ptr) { iWidget *d = ptr; postRefresh_App(); d->root->didAnimateVisualOffsets = iTrue; +// printf("'%s' visoffanim: fin:%d val:%f\n", cstr_String(&d->id), +// isFinished_Anim(&d->visualOffset), value_Anim(&d->visualOffset)); fflush(stdout); if (!isFinished_Anim(&d->visualOffset)) { - addTicker_App(visualOffsetAnimation_Widget_, ptr); + addTickerRoot_App(visualOffsetAnimation_Widget_, d->root, ptr); } else { d->flags &= ~visualOffset_WidgetFlag; @@ -919,6 +921,14 @@ int visualOffsetByReference_Widget(const iWidget *d) { // const float factor = width_Widget(d) / (float) size_Root(d->root).x; const int invOff = width_Widget(d) - iRound(value_Anim(&child->visualOffset)); offX -= invOff / 4; +#if 0 + if (invOff) { + printf(" [%p] %s (%p, fin:%d visoff:%d drag:%d): invOff %d\n", d, cstr_String(&child->id), child, + isFinished_Anim(&child->visualOffset), + (child->flags & visualOffset_WidgetFlag) != 0, + (child->flags & dragged_WidgetFlag) != 0, invOff); fflush(stdout); + } +#endif } } return offX; -- cgit v1.2.3 From 1c38deb8409ef9e3f1f4a089ef0e87dcb84bc81e Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 17 Dec 2021 10:26:26 +0200 Subject: macOS: Fixed scrolling of wide preformatted blocks --- src/ui/documentwidget.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index d0a9a55d..4ab17b42 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -3917,8 +3917,8 @@ static iBool handleWheelSwipe_DocumentWidget_(iDocumentWidget *d, const SDL_Mous case none_WheelSwipeState: /* A new swipe starts. */ if (!isInertia_MouseWheelEvent(ev) && !isScrollFinished_MouseWheelEvent(ev)) { - int side = ev->x < 0 ? 1 : 2; - d->wheelSwipeDistance = -ev->x; + int side = ev->x > 0 ? 1 : 2; + d->wheelSwipeDistance = ev->x * 2; d->flags &= ~eitherWheelSwipe_DocumentWidgetFlag; d->flags |= (side == 1 ? leftWheelSwipe_DocumentWidgetFlag : rightWheelSwipe_DocumentWidgetFlag); @@ -3935,7 +3935,7 @@ static iBool handleWheelSwipe_DocumentWidget_(iDocumentWidget *d, const SDL_Mous d->wheelSwipeState = none_WheelSwipeState; } else { - int step = -ev->x * 2; + int step = ev->x * 2; d->wheelSwipeDistance += step; /* Remember the maximum speed. */ if (d->swipeSpeed < 0 && step < 0) { @@ -4064,7 +4064,9 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e #endif } } - else if (ev->type == SDL_MOUSEWHEEL && ev->wheel.y == 0 && + else if (ev->type == SDL_MOUSEWHEEL && + ev->wheel.y == 0 && + d->wheelSwipeState == direct_WheelSwipeState && handleWheelSwipe_DocumentWidget_(d, &ev->wheel)) { return iTrue; } -- cgit v1.2.3 From 417b69c781cd6ee4d5600994b318a2120dbc10ed Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 17 Dec 2021 10:51:43 +0200 Subject: macOS: Incorrect horizontal scroll direction --- src/macos.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/macos.m b/src/macos.m index 2d082d28..e05222a0 100644 --- a/src/macos.m +++ b/src/macos.m @@ -442,7 +442,7 @@ static iBool processScrollWheelEvent_(NSEvent *event) { if (isPerPixel) { setInertia_MouseWheelEvent(&e, isInertia); setScrollFinished_MouseWheelEvent(&e, isEnded); - e.x = -event.scrollingDeltaX * win->pixelRatio; + e.x = event.scrollingDeltaX * win->pixelRatio; e.y = event.scrollingDeltaY * win->pixelRatio; /* Only scroll on one axis at a time. */ if (swipeDir_ == 0) { -- cgit v1.2.3 From eab10d435a5a38c60642bf549f2a37192e2902bb Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 17 Dec 2021 10:52:10 +0200 Subject: Mobile: Wheel scrolling is not applicable --- src/ui/documentwidget.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 4ab17b42..78c95f40 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -3905,6 +3905,9 @@ static void finishWheelSwipe_DocumentWidget_(iDocumentWidget *d) { static iBool handleWheelSwipe_DocumentWidget_(iDocumentWidget *d, const SDL_MouseWheelEvent *ev) { iWidget *w = as_Widget(d); + if (deviceType_App() != desktop_AppDeviceType) { + return iFalse; + } if (~flags_Widget(w) & horizontalOffset_WidgetFlag) { return iFalse; } @@ -4064,12 +4067,14 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e #endif } } +#if defined (iPlatformAppleDesktop) else if (ev->type == SDL_MOUSEWHEEL && ev->wheel.y == 0 && d->wheelSwipeState == direct_WheelSwipeState && handleWheelSwipe_DocumentWidget_(d, &ev->wheel)) { return iTrue; } +#endif else if (ev->type == SDL_MOUSEWHEEL && isHover_Widget(w)) { const iInt2 mouseCoord = coord_MouseWheelEvent(&ev->wheel); if (isPerPixel_MouseWheelEvent(&ev->wheel)) { -- cgit v1.2.3 From 27e60fdf26c031db7a967742f1870c3bdcf3f769 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 17 Dec 2021 10:52:25 +0200 Subject: Mobile: Fixed phone sidebar background fade --- src/ui/mobile.c | 1 + src/ui/widget.c | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/ui/mobile.c b/src/ui/mobile.c index df2a661a..bffc2177 100644 --- a/src/ui/mobile.c +++ b/src/ui/mobile.c @@ -739,6 +739,7 @@ void initPanels_Mobile(iWidget *panels, iWidget *parentWidget, horizontalOffset_WidgetFlag | /*overflowScrollable_WidgetFlag |*/ leftEdgeDraggable_WidgetFlag, iTrue); + panels->flags2 |= fadeBackground_WidgetFlag2; setFlags_Widget(panels, overflowScrollable_WidgetFlag, iFalse); /* The top-level split between main and detail panels. */ iWidget *mainDetailSplit = makeHDiv_Widget(); { diff --git a/src/ui/widget.c b/src/ui/widget.c index 1b90378a..1ac4326a 100644 --- a/src/ui/widget.c +++ b/src/ui/widget.c @@ -1430,9 +1430,19 @@ void drawLayerEffects_Widget(const iWidget *d) { init_Paint(&p); p.alpha = 0x50; if (flags_Widget(d) & (visualOffset_WidgetFlag | dragged_WidgetFlag)) { - const float area = d->rect.size.x * d->rect.size.y; + const float area = d->rect.size.x * d->rect.size.y; + const float rootArea = area_Rect(rect_Root(d->root)); const float visibleArea = area_Rect(intersect_Rect(bounds_Widget(d), rect_Root(d->root))); - p.alpha *= (area > 0 ? visibleArea / area : 0.0f); + if (isPortraitPhone_App() && !cmp_String(&d->id, "sidebar")) { + p.alpha *= iClamp(visibleArea / rootArea * 2, 0.0f, 1.0f); + } + else if (area > 0) { + p.alpha *= visibleArea / area; + } + else { + p.alpha = 0; + } + //printf("area:%f visarea:%f alpha:%d\n", rootArea, visibleArea, p.alpha); } SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND); fillRect_Paint(&p, rect_Root(d->root), backgroundFadeColor_Widget()); -- cgit v1.2.3 From 75291b4f8ff38dcc3fb2602783e8b7ef80a04fb0 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 17 Dec 2021 13:45:28 +0200 Subject: Mobile: Input prompt actions menu --- src/ui/documentwidget.c | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 78c95f40..1449d10d 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -2238,7 +2238,7 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) { addChildPos_Widget(buttons, iClob(counter), front_WidgetAddPos); } else { - insertChildAfter_Widget(buttons, iClob(counter), 0); + insertChildAfter_Widget(buttons, iClob(counter), 1); } if (lineBreak && deviceType_App() != desktop_AppDeviceType) { addChildPos_Widget(buttons, iClob(lineBreak), front_WidgetAddPos); @@ -2251,7 +2251,15 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) { buttons, cstr_String(&d->linePrecedingLink)) } }; iLabelWidget *menu = makeMenuButton_LabelWidget(midEllipsis_Icon, items, 1); - addChildPos_Widget(buttons, iClob(menu), front_WidgetAddPos); + if (deviceType_App() == desktop_AppDeviceType) { + addChildPos_Widget(buttons, iClob(menu), front_WidgetAddPos); + } + else { + insertChildAfterFlags_Widget(buttons, iClob(menu), 0, + frameless_WidgetFlag | noBackground_WidgetFlag); + setFont_LabelWidget(menu, font_LabelWidget((iLabelWidget *) lastChild_Widget(buttons))); + setTextColor_LabelWidget(menu, uiTextAction_ColorId); + } } setValidator_InputWidget(findChild_Widget(dlg, "input"), inputQueryValidator_, d); setSensitiveContent_InputWidget(findChild_Widget(dlg, "input"), @@ -3894,7 +3902,7 @@ static void finishWheelSwipe_DocumentWidget_(iDocumentWidget *d) { if (d->flags & eitherWheelSwipe_DocumentWidgetFlag && d->wheelSwipeState == direct_WheelSwipeState) { const int side = wheelSwipeSide_DocumentWidget_(d); - int abort = (side == 1 && d->swipeSpeed < 0 || side == 2 && d->swipeSpeed > 0); + int abort = ((side == 1 && d->swipeSpeed < 0) || (side == 2 && d->swipeSpeed > 0)); if (iAbs(d->wheelSwipeDistance) < width_Widget(d) / 4 && iAbs(d->swipeSpeed) < 4 * gap_UI) { abort = 1; } -- cgit v1.2.3 From 0bd47c9287e93aeabb02fad82a00712bb2fdd023 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 17 Dec 2021 14:01:15 +0200 Subject: Prefs: Assert that struct layout is as expected --- src/prefs.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/prefs.c b/src/prefs.c index 5d85f195..c4f71ed5 100644 --- a/src/prefs.c +++ b/src/prefs.c @@ -24,6 +24,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include +static_assert(offsetof(iPrefs, plainTextWrap) == offsetof(iPrefs, bools[plainTextWrap_PrefsBool]), + "memory layout mismatch (needs struct packing?)"); + void init_Prefs(iPrefs *d) { iForIndices(i, d->strings) { init_String(&d->strings[i]); -- cgit v1.2.3 From f63f9b240d318fec9ea03404c45134ef7ce162df Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 17 Dec 2021 14:01:41 +0200 Subject: iOS: Bumped version number; updated release notes --- CMakeLists.txt | 4 ++-- res/about/ios-version.gmi | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index edf58863..cccf2176 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,8 +25,8 @@ project (Lagrange set (COPYRIGHT_YEAR 2021) if (IOS) set (PROJECT_VERSION 1.10) - set (IOS_BUNDLE_VERSION 5) - set (IOS_BUILD_DATE "2021-12-15") + set (IOS_BUNDLE_VERSION 6) + set (IOS_BUILD_DATE "2021-12-17") endif () # Defaults that depend on environment. diff --git a/res/about/ios-version.gmi b/res/about/ios-version.gmi index 352c7e36..f3f366de 100644 --- a/res/about/ios-version.gmi +++ b/res/about/ios-version.gmi @@ -6,6 +6,12 @@ ``` # Release notes +## 1.10 (6) +* Added "Paste Preceding Line" in the input prompt dialog. Potential use cases include user-editable sections on a page and suggested/example input values. +* Fixed UI glitches and a potential memory leak when navigating via swipes. Sometimes swipe navigation would stop working because animation placeholders were not destroyed. +* Fixed Settings background fade. +* Fixed sidebar background fade in portrait phone layout. + ## 1.10 (5) * Fixed positioning of native UI controls in non-animated input widgets. * Fixed input widgets not reacting to keyboard being dismissed by system. -- cgit v1.2.3 From 90a0a0a2913dcc38f18c6d85849d3bde9dfbaa26 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 17 Dec 2021 14:05:11 +0200 Subject: Fixed release build --- src/prefs.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/prefs.c b/src/prefs.c index c4f71ed5..6b0164b6 100644 --- a/src/prefs.c +++ b/src/prefs.c @@ -23,6 +23,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "prefs.h" #include +#include static_assert(offsetof(iPrefs, plainTextWrap) == offsetof(iPrefs, bools[plainTextWrap_PrefsBool]), "memory layout mismatch (needs struct packing?)"); -- cgit v1.2.3 From 8b604e06c4ab305ef18faa0df67d0223897265e1 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 17 Dec 2021 16:55:16 +0200 Subject: App: Fixed links opening in the wrong split Switching the current root may change which DocumentWidget the `open` command affects. --- src/app.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app.c b/src/app.c index 86ef8b73..feabcf7f 100644 --- a/src/app.c +++ b/src/app.c @@ -2743,13 +2743,14 @@ iBool handleCommand_App(const char *cmd) { root = otherRoot_Window(as_Window(d->window), root); setKeyRoot_Window(as_Window(d->window), root); setCurrent_Root(root); /* need to change for widget creation */ + doc = document_Command(cmd); /* may be different */ } if (newTab & (new_OpenTabFlag | newBackground_OpenTabFlag)) { doc = newTab_App(NULL, (newTab & new_OpenTabFlag) != 0); /* `newtab:2` to open in background */ } - iHistory *history = history_DocumentWidget(doc); - const iBool isHistory = argLabel_Command(cmd, "history") != 0; - int redirectCount = argLabel_Command(cmd, "redirect"); + iHistory *history = history_DocumentWidget(doc); + const iBool isHistory = argLabel_Command(cmd, "history") != 0; + int redirectCount = argLabel_Command(cmd, "redirect"); if (!isHistory) { if (redirectCount) { replace_History(history, url); -- cgit v1.2.3 From 0c1817062433d70ff8799425139ffccf39a19725 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 17 Dec 2021 18:01:40 +0200 Subject: iOS: Updated release notes --- res/about/ios-version.gmi | 3 +++ 1 file changed, 3 insertions(+) diff --git a/res/about/ios-version.gmi b/res/about/ios-version.gmi index f3f366de..893d6dbc 100644 --- a/res/about/ios-version.gmi +++ b/res/about/ios-version.gmi @@ -6,6 +6,9 @@ ``` # Release notes +## 1.10 (7) +* Fixed opening links in split view mode. Links would open on the wrong side. + ## 1.10 (6) * Added "Paste Preceding Line" in the input prompt dialog. Potential use cases include user-editable sections on a page and suggested/example input values. * Fixed UI glitches and a potential memory leak when navigating via swipes. Sometimes swipe navigation would stop working because animation placeholders were not destroyed. -- cgit v1.2.3 From d3dbe844b61e3b122020ace3bcbd254bd7b4075f Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 18 Dec 2021 06:29:01 +0200 Subject: Fixed misaligned time in audio player TODO: This is font-dependent, so should actually use visual alignment... --- src/ui/mediaui.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/mediaui.c b/src/ui/mediaui.c index 4f2499b0..1c828194 100644 --- a/src/ui/mediaui.c +++ b/src/ui/mediaui.c @@ -104,7 +104,7 @@ static int drawSevenSegmentTime_(iInt2 pos, int color, int align, int seconds) { if (align == right_Alignment) { pos.x -= size.x; } - drawRange_Text(font, addY_I2(pos, -gap_UI / 8), color, range_String(&num)); + drawRange_Text(font, addY_I2(pos, gap_UI / 2), color, range_String(&num)); deinit_String(&num); return size.x; } -- cgit v1.2.3 From f27ee15db4cda256456f9f7b7a622d235741f279 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 18 Dec 2021 06:32:33 +0200 Subject: SidebarWidget: Use correct palette for outline The outline tab uses the current document's palette. --- src/ui/sidebarwidget.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index fcc1d807..4b4968a3 100644 --- a/src/ui/sidebarwidget.c +++ b/src/ui/sidebarwidget.c @@ -1895,6 +1895,9 @@ static void draw_SidebarWidget_(const iSidebarWidget *d) { const iRect bounds = bounds_Widget(w); iPaint p; init_Paint(&p); + if (d->mode == documentOutline_SidebarMode) { + makePaletteGlobal_GmDocument(document_DocumentWidget(document_App())); + } if (!isPortraitPhone_App()) { /* this would erase page contents during transition on the phone */ if (flags_Widget(w) & visualOffset_WidgetFlag && flags_Widget(w) & horizontalOffset_WidgetFlag && isVisible_Widget(w)) { -- cgit v1.2.3 From 2b78e40ce5d86a517ec081214d369f0064a72250 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 18 Dec 2021 07:00:03 +0200 Subject: Gempub: Open books in 1:2 split mode --- src/app.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app.c b/src/app.c index feabcf7f..e81490aa 100644 --- a/src/app.c +++ b/src/app.c @@ -2730,7 +2730,9 @@ iBool handleCommand_App(const char *cmd) { if (newTab & otherRoot_OpenTabFlag && numRoots_Window(get_Window()) == 1) { /* Need to split first. */ const iInt2 winSize = get_Window()->size; - postCommandf_App("ui.split arg:3 axis:%d origin:%s newtab:%d url:%s", + const int splitMode = argLabel_Command(cmd, "splitmode"); + postCommandf_App("ui.split arg:%d axis:%d origin:%s newtab:%d url:%s", + splitMode ? splitMode : 3, (float) winSize.x / (float) winSize.y < 0.7f ? 1 : 0, cstr_String(id_Widget(as_Widget(origin))), newTab & ~otherRoot_OpenTabFlag, -- cgit v1.2.3 From cc798c0f7ca53cc0f14a29439049309ae20616a8 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 18 Dec 2021 07:01:20 +0200 Subject: Nicer image metadata and open URLs highlight Metadata of inlined images shouldn't overlap the caption text. Open URL highlights are not drawn fullwidth to avoid clashing with side elements and generally to look more consistent. --- src/gmdocument.c | 37 ++++++++++++++++++++++++++++++++++++- src/gmdocument.h | 2 +- src/ui/documentwidget.c | 24 +++++++----------------- 3 files changed, 44 insertions(+), 19 deletions(-) diff --git a/src/gmdocument.c b/src/gmdocument.c index bec89ca0..29e97300 100644 --- a/src/gmdocument.c +++ b/src/gmdocument.c @@ -37,6 +37,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include #include #include +#include #include #include @@ -163,6 +164,7 @@ struct Impl_GmDocument { iBool enableCommandLinks; /* `about:command?` only allowed on selected pages */ iBool isLayoutInvalidated; iArray layout; /* contents of source, laid out in document space */ + iStringArray auxText; /* generated text that appears on the page but is not part of the source */ iPtrArray links; iString title; /* the first top-level title */ iArray headings; @@ -638,6 +640,7 @@ static void doLayout_GmDocument_(iGmDocument *d) { static const char *uploadArrow = upload_Icon; static const char *image = photo_Icon; clear_Array(&d->layout); + clear_StringArray(&d->auxText); clearLinks_GmDocument_(d); clear_Array(&d->headings); const iArray *oldPreMeta = collect_Array(copy_Array(&d->preMeta)); /* remember fold states */ @@ -1070,7 +1073,9 @@ static void doLayout_GmDocument_(iGmDocument *d) { run.bounds.pos.x -= d->outsideMargin; } run.visBounds = run.bounds; - const iInt2 maxSize = mulf_I2(imgSize, get_Window()->pixelRatio); + const iInt2 maxSize = mulf_I2( + imgSize, + get_Window()->pixelRatio * iMax(1.0f, (prefs_App()->zoomPercent / 100.0f))); if (width_Rect(run.visBounds) > maxSize.x) { /* Don't scale the image up. */ run.visBounds.size.y = @@ -1080,6 +1085,34 @@ static void doLayout_GmDocument_(iGmDocument *d) { run.bounds.size.y = run.visBounds.size.y; } pushBack_Array(&d->layout, &run); + pos.y += run.bounds.size.y + margin / 2; + /* Image metadata caption. */ { + run.font = FONT_ID(documentBody_FontId, semiBold_FontStyle, contentSmall_FontSize); + run.color = tmQuoteIcon_ColorId; + run.flags = decoration_GmRunFlag; + run.mediaId = 0; + run.mediaType = 0; + run.visBounds.pos.y = pos.y; + run.visBounds.size.y = lineHeight_Text(run.font); + run.bounds = zero_Rect(); + iString caption; + init_String(&caption); + format_String(&caption, + "%s \u2014 %d x %d \u2014 %.1f%s", + info.type, + imgSize.x, + imgSize.y, + info.numBytes / 1.0e6f, + cstr_Lang("mb")); + pushBack_StringArray(&d->auxText, &caption); + run.text = range_String(&caption); + /* Center it. */ + run.visBounds.size.x = measureRange_Text(run.font, range_String(&caption)).bounds.size.x; + run.visBounds.pos.x = d->size.x / 2 - run.visBounds.size.x / 2; + deinit_String(&caption); + pushBack_Array(&d->layout, &run); + pos.y += run.visBounds.size.y + margin; + } break; } case audio_MediaType: { @@ -1152,6 +1185,7 @@ void init_GmDocument(iGmDocument *d) { d->enableCommandLinks = iFalse; d->isLayoutInvalidated = iFalse; init_Array(&d->layout, sizeof(iGmRun)); + init_StringArray(&d->auxText); init_PtrArray(&d->links); init_String(&d->title); init_Array(&d->headings, sizeof(iGmHeading)); @@ -1173,6 +1207,7 @@ void deinit_GmDocument(iGmDocument *d) { deinit_PtrArray(&d->links); deinit_Array(&d->preMeta); deinit_Array(&d->headings); + deinit_StringArray(&d->auxText); deinit_Array(&d->layout); deinit_String(&d->localHost); deinit_String(&d->url); diff --git a/src/gmdocument.h b/src/gmdocument.h index 58fc3db3..eb02a26c 100644 --- a/src/gmdocument.h +++ b/src/gmdocument.h @@ -139,7 +139,7 @@ struct Impl_GmRun { uint32_t font : 14; uint32_t mediaType : 3; /* note: max_MediaType means preformatted block */ - uint32_t mediaId : 11; /* zero if not an image */ + uint32_t mediaId : 11; uint32_t lineType : 3; uint32_t isLede : 1; }; diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 1449d10d..06e3475a 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -1529,7 +1529,7 @@ static void postProcessRequestContent_DocumentWidget_(iDocumentWidget *d, iBool } else { postCommandf_App( - "open newtab:%d url:%s", otherRoot_OpenTabFlag, cstr_String(navStart)); + "open splitmode:1 newtab:%d url:%s", otherRoot_OpenTabFlag, cstr_String(navStart)); } } } @@ -4753,9 +4753,9 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { /* Open links get a highlighted background. */ int bg = tmBackgroundOpenLink_ColorId; const int frame = tmFrameOpenLink_ColorId; - iRect wideRect = { init_I2(left_Rect(d->widgetBounds), visPos.y), - init_I2(width_Rect(d->widgetBounds) + - width_Widget(d->widget->scroll), + const int pad = gap_Text; + iRect wideRect = { init_I2(d->docBounds.pos.x - pad, visPos.y), + init_I2(d->docBounds.size.x + 2 * pad, height_Rect(run->visBounds)) }; /* The first line is composed of two runs that may be drawn in either order, so only draw half of the background. */ @@ -4767,14 +4767,6 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { wideRect.pos.x = left_Rect(visRect); } fillRect_Paint(&d->paint, wideRect, bg); - if (run->flags & (startOfLine_GmRunFlag | decoration_GmRunFlag)) { - drawHLine_Paint(&d->paint, topLeft_Rect(wideRect), width_Rect(wideRect), frame); - } - /* TODO: The decoration is not marked as endOfLine, so it lacks the bottom line. */ -// if (run->flags & endOfLine_GmRunFlag) { -// drawHLine_Paint( -// &d->paint, addY_I2(bottomLeft_Rect(wideRect), -1), width_Rect(wideRect), frame); -// } } else { /* Normal background for other runs. There are cases when runs get drawn multiple times, @@ -4866,11 +4858,7 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { info_Media(constMedia_GmDocument(doc), linkMedia, &info); switch (linkMedia.type) { case image_MediaType: { - iAssert(!isEmpty_Rect(run->bounds)); - const iInt2 imgSize = imageSize_Media(constMedia_GmDocument(doc), linkMedia); - format_String(&text, "%s \u2014 %d x %d \u2014 %.1f%s", - info.type, imgSize.x, imgSize.y, info.numBytes / 1.0e6f, - cstr_Lang("mb")); + /* There's a separate decorative GmRun for the metadata. */ break; } case audio_MediaType: @@ -4883,6 +4871,7 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { break; } if (linkMedia.type != download_MediaType && /* can't cancel downloads currently */ + linkMedia.type != image_MediaType && findMediaRequest_DocumentWidget_(d->widget, run->linkId)) { appendFormat_String( &text, " %s" close_Icon, isHover ? escape_Color(tmLinkText_ColorId) : ""); @@ -4911,6 +4900,7 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { } } else if (isHover) { + /* TODO: Make this a dynamic overlay, not part of the VisBuf content. */ const iGmLinkId linkId = d->widget->hoverLink->linkId; const iString * url = linkUrl_GmDocument(doc, linkId); const int flags = linkFlags; -- cgit v1.2.3 From a47ce7adf6f7852869921fe6152ed03fc5c7cf49 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 18 Dec 2021 08:18:08 +0200 Subject: Banner: Increased padding in items --- src/ui/banner.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/banner.c b/src/ui/banner.c index 7ec189a4..218b0981 100644 --- a/src/ui/banner.c +++ b/src/ui/banner.c @@ -64,8 +64,8 @@ struct Impl_Banner { iDefineTypeConstruction(Banner) #define itemGap_Banner_ (3 * gap_UI) -#define itemVPad_Banner_ (2 * gap_UI) -#define itemHPad_Banner_ (3 * gap_UI) +#define itemVPad_Banner_ (3 * gap_UI) +#define itemHPad_Banner_ (4 * gap_UI) #define bottomPad_Banner_ (4 * gap_UI) static void updateHeight_Banner_(iBanner *d) { @@ -76,7 +76,7 @@ static void updateHeight_Banner_(iBanner *d) { } const size_t numItems = size_Array(&d->items); if (numItems) { - const int innerPad = gap_UI; +// const int innerPad = gap_UI; iConstForEach(Array, i, &d->items) { const iBannerItem *item = i.value; d->rect.size.y += item->height; -- cgit v1.2.3 From 30172eb27e3c4719b18aace7c41615035cb98d2d Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 18 Dec 2021 08:20:59 +0200 Subject: Image metadata; improved open URLs indicator Put a new GmRun under an inlined image with the image metadata, so it doesn't overlap the caption text. Open URLs are now indicated with a document-width box instead of window-width box. The box is also slightly expanded for a nicer look. Links have a bit of padding around them so this doesn't cause overlaps with other runs. --- src/gmdocument.c | 9 ++++++-- src/ui/documentwidget.c | 56 +++++++++++++++++++++++++++++++++++-------------- 2 files changed, 47 insertions(+), 18 deletions(-) diff --git a/src/gmdocument.c b/src/gmdocument.c index 29e97300..0197ed99 100644 --- a/src/gmdocument.c +++ b/src/gmdocument.c @@ -925,7 +925,7 @@ static void doLayout_GmDocument_(iGmDocument *d) { : paragraph_FontId; alignDecoration_GmRun_(&icon, iFalse); icon.color = linkColor_GmDocument(d, run.linkId, icon_GmLinkPart); - icon.flags |= decoration_GmRunFlag; + icon.flags |= decoration_GmRunFlag | startOfLine_GmRunFlag; pushBack_Array(&d->layout, &icon); } run.lineType = type; @@ -1039,7 +1039,12 @@ static void doLayout_GmDocument_(iGmDocument *d) { deinit_RunTypesetter_(&rts); } /* Flag the end of line, too. */ - ((iGmRun *) back_Array(&d->layout))->flags |= endOfLine_GmRunFlag; + iGmRun *lastRun = back_Array(&d->layout); + lastRun->flags |= endOfLine_GmRunFlag; + if (lastRun->linkId && lastRun->flags & startOfLine_GmRunFlag) { + /* Single-run link: the icon should also be marked endOfLine. */ + lastRun[-1].flags |= endOfLine_GmRunFlag; + } /* Image or audio content. */ if (type == link_GmLineType) { /* TODO: Cleanup here? Move to a function of its own. */ diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 06e3475a..45b96082 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -4749,7 +4749,18 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { runOffset_DocumentWidget_(d->widget, run)); const iRect visRect = { visPos, run->visBounds.size }; /* Fill the background. */ { - if (run->linkId && linkFlags & isOpen_GmLinkFlag && ~linkFlags & content_GmLinkFlag) { +#if 0 + iBool isInlineImageCaption = run->linkId && linkFlags & content_GmLinkFlag && + ~linkFlags & permanent_GmLinkFlag; + if (run->flags & decoration_GmRunFlag && ~run->flags & startOfLine_GmRunFlag) { + /* This is the metadata. */ + isInlineImageCaption = iFalse; + } +#endif + /* While this is consistent, it's a bit excessive to indicate that an inlined image + is open: the image itself is the indication. */ + const iBool isInlineImageCaption = iFalse; + if (run->linkId && (linkFlags & isOpen_GmLinkFlag || isInlineImageCaption)) { /* Open links get a highlighted background. */ int bg = tmBackgroundOpenLink_ColorId; const int frame = tmFrameOpenLink_ColorId; @@ -4757,6 +4768,9 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { iRect wideRect = { init_I2(d->docBounds.pos.x - pad, visPos.y), init_I2(d->docBounds.size.x + 2 * pad, height_Rect(run->visBounds)) }; + adjustEdges_Rect(&wideRect, + run->flags & startOfLine_GmRunFlag ? -pad * 3 / 4 : 0, 0, + run->flags & endOfLine_GmRunFlag ? pad * 3 / 4 : 0, 0); /* The first line is composed of two runs that may be drawn in either order, so only draw half of the background. */ if (run->flags & decoration_GmRunFlag) { @@ -4773,13 +4787,21 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { e.g., at the buffer boundary, and there are slightly overlapping characters in monospace blocks. Clearing the background here ensures a cleaner visual appearance since only one glyph is visible at any given point. */ - fillRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmBackground_ColorId); + fillRect_Paint(&d->paint, visRect, tmBackground_ColorId); } } - if (run->linkId && ~run->flags & decoration_GmRunFlag) { - fg = linkColor_GmDocument(doc, run->linkId, isHover ? textHover_GmLinkPart : text_GmLinkPart); - if (linkFlags & content_GmLinkFlag) { - fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart); /* link is inactive */ + if (run->linkId) { + if (run->flags & decoration_GmRunFlag && run->flags & startOfLine_GmRunFlag) { + /* Link icon. */ + if (linkFlags & content_GmLinkFlag) { + fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart); + } + } + else if (~run->flags & decoration_GmRunFlag) { + fg = linkColor_GmDocument(doc, run->linkId, isHover ? textHover_GmLinkPart : text_GmLinkPart); + if (linkFlags & content_GmLinkFlag) { + fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart); /* link is inactive */ + } } } if (run->flags & altText_GmRunFlag) { @@ -4877,16 +4899,18 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { &text, " %s" close_Icon, isHover ? escape_Color(tmLinkText_ColorId) : ""); } const iInt2 size = measureRange_Text(metaFont, range_String(&text)).bounds.size; - fillRect_Paint( - &d->paint, - (iRect){ add_I2(origin, addX_I2(topRight_Rect(run->bounds), -size.x - gap_UI)), - addX_I2(size, 2 * gap_UI) }, - tmBackground_ColorId); - drawAlign_Text(metaFont, - add_I2(topRight_Rect(run->bounds), origin), - fg, - right_Alignment, - "%s", cstr_String(&text)); + if (size.x) { + fillRect_Paint( + &d->paint, + (iRect){ add_I2(origin, addX_I2(topRight_Rect(run->bounds), -size.x - gap_UI)), + addX_I2(size, 2 * gap_UI) }, + tmBackground_ColorId); + drawAlign_Text(metaFont, + add_I2(topRight_Rect(run->bounds), origin), + fg, + right_Alignment, + "%s", cstr_String(&text)); + } deinit_String(&text); } else if (run->flags & endOfLine_GmRunFlag && -- cgit v1.2.3 From 686251cc0692403cf5ce30ec552e9774f4c34b2a Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 18 Dec 2021 08:53:33 +0200 Subject: Focusing the search input field --- src/ui/root.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/ui/root.c b/src/ui/root.c index 780dc6e7..9e264993 100644 --- a/src/ui/root.c +++ b/src/ui/root.c @@ -1034,7 +1034,14 @@ static iBool handleSearchBarCommands_(iWidget *searchBar, const char *cmd) { else if (equal_Command(cmd, "focus.gained")) { if (pointer_Command(cmd) == findChild_Widget(searchBar, "find.input")) { if (!isVisible_Widget(searchBar)) { + /* InputWidget will unfocus itself if there isn't enough space for editing + text. A collapsed widget will not have been arranged yet, so on the first + time the widget will just be unfocused immediately. */ + const iBool wasArranged = area_Rect(bounds_Widget(searchBar)) > 0; showCollapsed_Widget(searchBar, iTrue); + if (!wasArranged) { + postCommand_App("focus.set id:find.input"); + } } } } -- cgit v1.2.3 From 819d4955b4b5630947358dc3421967ada5fbaf72 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 18 Dec 2021 08:53:40 +0200 Subject: Updated release notes --- res/about/ios-version.gmi | 3 +++ res/about/version.gmi | 3 +++ 2 files changed, 6 insertions(+) diff --git a/res/about/ios-version.gmi b/res/about/ios-version.gmi index 893d6dbc..6cdaef3e 100644 --- a/res/about/ios-version.gmi +++ b/res/about/ios-version.gmi @@ -7,6 +7,9 @@ # Release notes ## 1.10 (7) +* Inline image metadata goes under the image instead of possibly overlapping the image caption text. +* Improved apperance of opened links highlighting. It no longer goes under the side elements on the page. +* Gempub: Open books in 1:2 split mode instead of 1:1. * Fixed opening links in split view mode. Links would open on the wrong side. ## 1.10 (6) diff --git a/res/about/version.gmi b/res/about/version.gmi index 64a547d4..5a7b3c99 100644 --- a/res/about/version.gmi +++ b/res/about/version.gmi @@ -14,10 +14,13 @@ New features: * UI language for Dutch. Changes and enhancements: +* Inline image metadata goes under the image instead of possibly overlapping the image caption text. +* Improved apperance of opened links highlighting. It no longer goes under the side elements on the page. * Revised layout of the Upload dialog. There is a new edit field that lets you modify the file path of the URL. Identity names are in bold. * Optimized UI layout procedure and memory use during UI event processing. * Audio subsystem is only initialized when actually needed. * Prevent state file corruption if the app happens to get killed while state is being saved. +* Gempub: Open books in 1:2 split mode instead of 1:1. * Minor improvements in page caching. Fixes: -- cgit v1.2.3 From 764372a4a4c460c28616fa198ead9bc1f029c9cb Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 18 Dec 2021 10:58:41 +0200 Subject: DocumentWidget: Fixed open links highlight position --- src/ui/documentwidget.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 45b96082..afd0070f 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -4765,7 +4765,7 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { int bg = tmBackgroundOpenLink_ColorId; const int frame = tmFrameOpenLink_ColorId; const int pad = gap_Text; - iRect wideRect = { init_I2(d->docBounds.pos.x - pad, visPos.y), + iRect wideRect = { init_I2(origin.x - pad, visPos.y), init_I2(d->docBounds.size.x + 2 * pad, height_Rect(run->visBounds)) }; adjustEdges_Rect(&wideRect, -- cgit v1.2.3 From a26637839a14e70f8f722cb5b37031f2cffe44e2 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 18 Dec 2021 12:25:16 +0200 Subject: Added LinkInfo --- CMakeLists.txt | 2 ++ src/ui/linkinfo.c | 41 +++++++++++++++++++++++++++++++++++++++++ src/ui/linkinfo.h | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 src/ui/linkinfo.c create mode 100644 src/ui/linkinfo.h diff --git a/CMakeLists.txt b/CMakeLists.txt index cccf2176..ba63bf78 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -196,6 +196,8 @@ set (SOURCES src/ui/documentwidget.h src/ui/indicatorwidget.c src/ui/indicatorwidget.h + src/ui/linkinfo.c + src/ui/linkinfo.h src/ui/listwidget.c src/ui/listwidget.h src/ui/lookupwidget.c diff --git a/src/ui/linkinfo.c b/src/ui/linkinfo.c new file mode 100644 index 00000000..e20c183c --- /dev/null +++ b/src/ui/linkinfo.c @@ -0,0 +1,41 @@ +/* Copyright 2021 Jaakko Keränen + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + +#include "linkinfo.h" + +#define hPad_LinkInfo_ (2 * gap_UI) +#define vPad_LinkInfo_ (1 * gap_UI) + +void init_LinkInfo(iLinkInfo *d) { + d->buf = NULL; +} + +void deinit_LinkInfo(iLinkInfo *d) { + delete_TextBuf(d->buf); +} + +iInt2 size_LinkInfo(const iLinkInfo *d) { + if (!d->buf) { + return zero_I2(); + } + return add_I2(d->buf->size, init_I2(2 * hPad_LinkInfo_, 2 * vPad_LinkInfo_))); +} diff --git a/src/ui/linkinfo.h b/src/ui/linkinfo.h new file mode 100644 index 00000000..dbedf359 --- /dev/null +++ b/src/ui/linkinfo.h @@ -0,0 +1,32 @@ +/* Copyright 2021 Jaakko Keränen + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + +#pragma once + +#include "text.h" + +iDeclareType(LinkInfo) +iDeclareTypeConstruction(LinkInfo) + +struct Impl_LinkInfo { + iTextBuf *buf; +}; -- cgit v1.2.3 From 075cef7b43d2b26d32a5ccb8b916f06987af8cea Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 18 Dec 2021 16:50:16 +0200 Subject: App: If in the background, don't do periodic events Does SDL stop all timers? --- src/app.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/app.c b/src/app.c index e81490aa..2590da0d 100644 --- a/src/app.c +++ b/src/app.c @@ -125,6 +125,7 @@ struct Impl_App { iBool isRunning; iBool isRunningUnderWindowSystem; iBool isDarkSystemTheme; + iBool isSuspended; #if defined (LAGRANGE_ENABLE_IDLE_SLEEP) iBool isIdling; uint32_t lastEventTime; @@ -721,6 +722,7 @@ static void init_App_(iApp *d, int argc, char **argv) { d->isRunningUnderWindowSystem = iTrue; #endif d->isDarkSystemTheme = iTrue; /* will be updated by system later on, if supported */ + d->isSuspended = iFalse; init_CommandLine(&d->args, argc, argv); /* Where was the app started from? We ask SDL first because the command line alone cannot be relied on (behavior differs depending on OS). */ { @@ -1195,6 +1197,10 @@ iLocalDef iBool isWaitingAllowed_App_(iApp *d) { } static iBool nextEvent_App_(iApp *d, enum iAppEventMode eventMode, SDL_Event *event) { + if (d->isSuspended) { + /* Do nothing except wait for the app to return to the foreground. */ + return SDL_WaitEvent(event); + } if (eventMode == waitForNewEvents_AppEventMode && isWaitingAllowed_App_(d)) { /* If there are periodic commands pending, wait only for a short while. */ if (!isEmpty_Periodic(&d->periodic)) { @@ -1244,6 +1250,7 @@ void processEvents_App(enum iAppEventMode eventMode) { break; case SDL_APP_WILLENTERFOREGROUND: invalidate_Window(as_Window(d->window)); + d->isSuspended = iFalse; break; case SDL_APP_DIDENTERFOREGROUND: gotEvents = iTrue; @@ -1251,7 +1258,7 @@ void processEvents_App(enum iAppEventMode eventMode) { #if defined (LAGRANGE_ENABLE_IDLE_SLEEP) d->isIdling = iFalse; d->lastEventTime = SDL_GetTicks(); -#endif +#endif postRefresh_App(); break; case SDL_APP_WILLENTERBACKGROUND: @@ -1261,6 +1268,7 @@ void processEvents_App(enum iAppEventMode eventMode) { setFreezeDraw_MainWindow(d->window, iTrue); savePrefs_App_(d); saveState_App_(d); + d->isSuspended = iTrue; break; case SDL_APP_TERMINATING: setFreezeDraw_MainWindow(d->window, iTrue); -- cgit v1.2.3 From ccde1781a07ab418050efdfb00d9231291ce52d8 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 18 Dec 2021 16:50:44 +0200 Subject: TextBuf: Set base attributes for ANSI escapes --- src/ui/text.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ui/text.c b/src/ui/text.c index f3d945e4..fc552db9 100644 --- a/src/ui/text.c +++ b/src/ui/text.c @@ -2242,6 +2242,7 @@ void init_TextBuf(iTextBuf *d, iWrapText *wrapText, int font, int color) { SDL_Texture *oldTarget = SDL_GetRenderTarget(render); const iInt2 oldOrigin = origin_Paint; origin_Paint = zero_I2(); + setBaseAttributes_Text(font, color); SDL_SetRenderTarget(render, d->texture); SDL_SetRenderDrawBlendMode(render, SDL_BLENDMODE_NONE); SDL_SetRenderDrawColor(render, 255, 255, 255, 0); @@ -2252,6 +2253,7 @@ void init_TextBuf(iTextBuf *d, iWrapText *wrapText, int font, int color) { SDL_SetRenderTarget(render, oldTarget); origin_Paint = oldOrigin; SDL_SetTextureBlendMode(d->texture, SDL_BLENDMODE_BLEND); + setBaseAttributes_Text(-1, -1); } } -- cgit v1.2.3 From 5f7709f0b84e74fde847cdcf02c41990ef037daa Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 18 Dec 2021 16:51:04 +0200 Subject: LinkInfo: Improved link metadata popup --- src/ui/documentwidget.c | 50 +++++++++++++--- src/ui/linkinfo.c | 150 ++++++++++++++++++++++++++++++++++++++++++++++-- src/ui/linkinfo.h | 13 +++++ 3 files changed, 200 insertions(+), 13 deletions(-) diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index afd0070f..1870efd6 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -41,6 +41,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "inputwidget.h" #include "keys.h" #include "labelwidget.h" +#include "linkinfo.h" #include "media.h" #include "paint.h" #include "periodic.h" @@ -161,10 +162,10 @@ enum iDrawBufsFlag { }; struct Impl_DrawBufs { - int flags; - SDL_Texture * sideIconBuf; - iTextBuf * timestampBuf; - uint32_t lastRenderTime; + int flags; + SDL_Texture *sideIconBuf; + iTextBuf *timestampBuf; + uint32_t lastRenderTime; }; static void init_DrawBufs(iDrawBufs *d) { @@ -234,10 +235,11 @@ enum iDocumentWidgetFlag { fromCache_DocumentWidgetFlag = iBit(16), /* don't write anything to cache */ animationPlaceholder_DocumentWidgetFlag = iBit(17), /* avoid slow operations */ invalidationPending_DocumentWidgetFlag = iBit(18), /* invalidate as soon as convenient */ - leftWheelSwipe_DocumentWidgetFlag = iBit(19), /* swipe state flags are used on desktop */ - rightWheelSwipe_DocumentWidgetFlag = iBit(20), - eitherWheelSwipe_DocumentWidgetFlag = leftWheelSwipe_DocumentWidgetFlag | rightWheelSwipe_DocumentWidgetFlag, -// wheelSwipeFinished_DocumentWidgetFlag = iBit(21), + leftWheelSwipe_DocumentWidgetFlag = iBit(19), /* swipe state flags are used on desktop */ + rightWheelSwipe_DocumentWidgetFlag = iBit(20), + eitherWheelSwipe_DocumentWidgetFlag = leftWheelSwipe_DocumentWidgetFlag | + rightWheelSwipe_DocumentWidgetFlag, +// wheelSwipeFinished_DocumentWidgetFlag = iBit(21), }; enum iDocumentLinkOrdinalMode { @@ -322,6 +324,7 @@ struct Impl_DocumentWidget { iVisBufMeta * visBufMeta; iGmRunRange renderRuns; iPtrSet * invalidRuns; + iLinkInfo * linkInfo; /* Widget structure: */ iScrollWidget *scroll; @@ -413,6 +416,7 @@ void init_DocumentWidget(iDocumentWidget *d) { init_String(&d->pendingGotoHeading); init_String(&d->linePrecedingLink); init_Click(&d->click, d, SDL_BUTTON_LEFT); + d->linkInfo = (deviceType_App() == desktop_AppDeviceType ? new_LinkInfo() : NULL); addChild_Widget(w, iClob(d->scroll = new_ScrollWidget())); d->menu = NULL; /* created when clicking */ d->playerMenu = NULL; @@ -457,6 +461,7 @@ void deinit_DocumentWidget(iDocumentWidget *d) { delete_VisBuf(d->visBuf); free(d->visBufMeta); delete_PtrSet(d->invalidRuns); + delete_LinkInfo(d->linkInfo); iRelease(d->media); iRelease(d->request); delete_Gempub(d->sourceGempub); @@ -737,7 +742,8 @@ static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse); static void animate_DocumentWidget_(void *ticker) { iDocumentWidget *d = ticker; refresh_Widget(d); - if (!isFinished_Anim(&d->sideOpacity) || !isFinished_Anim(&d->altTextOpacity)) { + if (!isFinished_Anim(&d->sideOpacity) || !isFinished_Anim(&d->altTextOpacity) || + (d->linkInfo && !isFinished_Anim(&d->linkInfo->opacity))) { addTicker_App(animate_DocumentWidget_, d); } } @@ -789,6 +795,10 @@ static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) { if (d->hoverLink) { invalidateLink_DocumentWidget_(d, d->hoverLink->linkId); } + if (update_LinkInfo(d->linkInfo, d->doc, d->hoverLink ? d->hoverLink->linkId : 0, + width_Widget(w))) { + animate_DocumentWidget_(d); + } refresh_Widget(w); } /* Hovering over preformatted blocks. */ @@ -4923,6 +4933,7 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { (float) bodySize_GmRequest(mr->req) / 1.0e6f); } } +#if 0 else if (isHover) { /* TODO: Make this a dynamic overlay, not part of the VisBuf content. */ const iGmLinkId linkId = d->widget->hoverLink->linkId; @@ -5002,6 +5013,7 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { deinit_String(&str); } } +#endif } if (0) { drawRect_Paint(&d->paint, (iRect){ visPos, run->bounds.size }, green_ColorId); @@ -5446,6 +5458,25 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) { } unsetClip_Paint(&ctx.paint); drawSideElements_DocumentWidget_(d); + if (deviceType_App() == desktop_AppDeviceType && prefs_App()->hoverLink && d->linkInfo) { + const int pad = gap_UI; + update_LinkInfo(d->linkInfo, + d->doc, + d->hoverLink ? d->hoverLink->linkId : 0, + width_Rect(bounds) - 2 * pad); + const iInt2 infoSize = size_LinkInfo(d->linkInfo); + iInt2 infoPos = add_I2(bottomLeft_Rect(bounds), init_I2(pad, -infoSize.y - pad)); + if (d->hoverLink) { + const iRect runRect = runRect_DocumentWidget_(d, d->hoverLink); + d->linkInfo->isAltPos = + (bottom_Rect(runRect) >= infoPos.y - lineHeight_Text(paragraph_FontId)); + } + if (d->linkInfo->isAltPos) { + infoPos.y = top_Rect(bounds) + pad; + } + draw_LinkInfo(d->linkInfo, infoPos); + } +#if 0 if (prefs_App()->hoverLink && d->hoverLink) { const int font = uiLabel_FontId; const iRangecc linkUrl = range_String(linkUrl_GmDocument(d->doc, d->hoverLink->linkId)); @@ -5455,6 +5486,7 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) { fillRect_Paint(&ctx.paint, linkRect, tmBackground_ColorId); drawRange_Text(font, addX_I2(topLeft_Rect(linkRect), gap_UI), tmParagraph_ColorId, linkUrl); } +#endif } if (colorTheme_App() == pureWhite_ColorTheme) { drawHLine_Paint(&ctx.paint, topLeft_Rect(bounds), width_Rect(bounds), uiSeparator_ColorId); diff --git a/src/ui/linkinfo.c b/src/ui/linkinfo.c index e20c183c..72abea1a 100644 --- a/src/ui/linkinfo.c +++ b/src/ui/linkinfo.c @@ -21,12 +21,23 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "linkinfo.h" +#include "metrics.h" +#include "paint.h" +#include "../gmcerts.h" +#include "../app.h" -#define hPad_LinkInfo_ (2 * gap_UI) -#define vPad_LinkInfo_ (1 * gap_UI) +#include + +iDefineTypeConstruction(LinkInfo) + +#define minWidth_LinkInfo_ (40 * gap_UI) +#define hPad_LinkInfo_ (2 * gap_UI) +#define vPad_LinkInfo_ (1 * gap_UI) void init_LinkInfo(iLinkInfo *d) { - d->buf = NULL; + d->buf = NULL; + init_Anim(&d->opacity, 0.0f); + d->isAltPos = iFalse; } void deinit_LinkInfo(iLinkInfo *d) { @@ -37,5 +48,136 @@ iInt2 size_LinkInfo(const iLinkInfo *d) { if (!d->buf) { return zero_I2(); } - return add_I2(d->buf->size, init_I2(2 * hPad_LinkInfo_, 2 * vPad_LinkInfo_))); + return add_I2(d->buf->size, init_I2(2 * hPad_LinkInfo_, 2 * vPad_LinkInfo_)); +} + +iBool update_LinkInfo(iLinkInfo *d, const iGmDocument *doc, iGmLinkId linkId, int maxWidth) { + if (!d) { + return iFalse; + } + if (d->linkId != linkId || d->maxWidth != maxWidth) { + d->linkId = linkId; + d->maxWidth = maxWidth; + invalidate_LinkInfo(d); + if (linkId) { + /* Measure and draw. */ + if (targetValue_Anim(&d->opacity) < 1) { + setValue_Anim(&d->opacity, 1, 75); + } + const int avail = iMax(minWidth_LinkInfo_, maxWidth) - 2 * hPad_LinkInfo_; + const iString *url = linkUrl_GmDocument(doc, linkId); + iUrl parts; + init_Url(&parts, url); + const int flags = linkFlags_GmDocument(doc, linkId); + const enum iGmLinkScheme scheme = scheme_GmLinkFlag(flags); + const iBool showHost = (flags & humanReadable_GmLinkFlag && + (!isEmpty_Range(&parts.host) || + scheme == mailto_GmLinkScheme)); + const iBool showImage = (flags & imageFileExtension_GmLinkFlag) != 0; + const iBool showAudio = (flags & audioFileExtension_GmLinkFlag) != 0; +// int fg = linkColor_GmDocument(doc, linkId, textHover_GmLinkPart); + iString str; + init_String(&str); + /* Identity that will be used. */ + const iGmIdentity *ident = identityForUrl_GmCerts(certs_App(), url); + if (ident) { + appendFormat_String(&str, person_Icon " %s", + //escape_Color(tmBannerItemTitle_ColorId), + cstr_String(name_GmIdentity(ident))); + } /* Show scheme and host. */ + if ((showHost || + (flags & (imageFileExtension_GmLinkFlag | audioFileExtension_GmLinkFlag))) && + scheme != mailto_GmLinkScheme) { + if (!isEmpty_String(&str)) { + appendCStr_String(&str, "\n"); + } + if (showHost && scheme != gemini_GmLinkScheme) { + append_String( + &str, collect_String(upper_String(collectNewRange_String(parts.scheme)))); + appendCStr_String(&str, " \u2014 "); + } + if (showHost) { + appendFormat_String(&str, "\x1b[1m%s\x1b[0m", cstr_Rangecc(parts.host)); + } + if (showImage || showAudio) { + appendFormat_String( + &str, + "%s%s%s", + // showHost ? (scheme == mailto_GmLinkScheme ? cstr_String(url) + // : scheme != gemini_GmLinkScheme + // ? format_CStr("%s \u2014 %s", + // cstrCollect_String(upper_String( + // collectNewRange_String(parts.scheme))), + // cstr_Rangecc(parts.host)) + // : cstr_Rangecc(parts.host)) + // : "", + // showHost ? format_CStr("\x1b[1m%s\x1b[0m", cstr_Rangecc(parts.host)) : "", + showHost && (showImage || showAudio) ? " \u2014" : "", + showImage || showAudio + ? "" + : "", // escape_Color(linkColor_GmDocument(doc, linkId, domain_GmLinkPart)), + showImage || showAudio + ? format_CStr(showImage ? photo_Icon " %s " : "\U0001f3b5 %s", + cstr_Lang(showImage ? "link.hint.image" : "link.hint.audio")) + : ""); + } + } + if (flags & visited_GmLinkFlag) { + iDate date; + init_Date(&date, linkTime_GmDocument(doc, linkId)); + if (!isEmpty_String(&str)) { + appendCStr_String(&str, " \u2014 "); + } +// appendCStr_String(&str, escape_Color(tmQuoteIcon_ColorId)); + iString *dateStr = format_Date(&date, "%b %d"); + append_String(&str, dateStr); + delete_String(dateStr); + } + if (!isEmpty_String(&str)) { + appendCStr_String(&str, "\n"); + } + appendRange_String(&str, range_String(url)); + /* Draw the text. */ + iWrapText wt = { .text = range_String(&str), .maxWidth = avail, .mode = word_WrapTextMode }; + d->buf = new_TextBuf(&wt, uiLabel_FontId, tmQuote_ColorId); + deinit_String(&str); + } + else { + if (targetValue_Anim(&d->opacity) > 0) { + setValue_Anim(&d->opacity, 0, 150); + } + } + return iTrue; + } + return iFalse; +} + +void invalidate_LinkInfo(iLinkInfo *d) { + if (targetValue_Anim(&d->opacity) > 0) { + setValue_Anim(&d->opacity, 0, 150); + } + + // if (d->buf) { +// delete_TextBuf(d->buf); +// d->buf = NULL; +// } +} + +void draw_LinkInfo(const iLinkInfo *d, iInt2 topLeft) { + const float opacity = value_Anim(&d->opacity); + if (!d->buf || opacity <= 0.01f) { + return; + } + iPaint p; + init_Paint(&p); + iInt2 size = size_LinkInfo(d); + iRect rect = { topLeft, size }; + SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND); + p.alpha = 255 * opacity; + fillRect_Paint(&p, rect, tmBackgroundAltText_ColorId); + drawRect_Paint(&p, rect, tmFrameAltText_ColorId); + SDL_SetTextureAlphaMod(d->buf->texture, p.alpha); + draw_TextBuf(d->buf, add_I2(topLeft, init_I2(hPad_LinkInfo_, vPad_LinkInfo_)), + white_ColorId); + SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE); } diff --git a/src/ui/linkinfo.h b/src/ui/linkinfo.h index dbedf359..a1669f95 100644 --- a/src/ui/linkinfo.h +++ b/src/ui/linkinfo.h @@ -23,10 +23,23 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #pragma once #include "text.h" +#include "util.h" +#include "../gmdocument.h" iDeclareType(LinkInfo) iDeclareTypeConstruction(LinkInfo) struct Impl_LinkInfo { + iGmLinkId linkId; + int maxWidth; iTextBuf *buf; + iAnim opacity; + iBool isAltPos; }; + +iBool update_LinkInfo (iLinkInfo *, const iGmDocument *doc, iGmLinkId linkId, + int maxWidth); /* returns true if changed */ +void invalidate_LinkInfo (iLinkInfo *); + +iInt2 size_LinkInfo (const iLinkInfo *); +void draw_LinkInfo (const iLinkInfo *, iInt2 topLeft); -- cgit v1.2.3 From 5ba0ab94a5fda3eb83885bf33f552f296bb13669 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 18 Dec 2021 19:16:48 +0200 Subject: LinkInfo: Identity at end of first line --- src/ui/linkinfo.c | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/ui/linkinfo.c b/src/ui/linkinfo.c index 72abea1a..38da84c7 100644 --- a/src/ui/linkinfo.c +++ b/src/ui/linkinfo.c @@ -78,13 +78,6 @@ iBool update_LinkInfo(iLinkInfo *d, const iGmDocument *doc, iGmLinkId linkId, in // int fg = linkColor_GmDocument(doc, linkId, textHover_GmLinkPart); iString str; init_String(&str); - /* Identity that will be used. */ - const iGmIdentity *ident = identityForUrl_GmCerts(certs_App(), url); - if (ident) { - appendFormat_String(&str, person_Icon " %s", - //escape_Color(tmBannerItemTitle_ColorId), - cstr_String(name_GmIdentity(ident))); - } /* Show scheme and host. */ if ((showHost || (flags & (imageFileExtension_GmLinkFlag | audioFileExtension_GmLinkFlag))) && scheme != mailto_GmLinkScheme) { @@ -133,6 +126,17 @@ iBool update_LinkInfo(iLinkInfo *d, const iGmDocument *doc, iGmLinkId linkId, in append_String(&str, dateStr); delete_String(dateStr); } + /* Identity that will be used. */ + const iGmIdentity *ident = identityForUrl_GmCerts(certs_App(), url); + if (ident) { + if (!isEmpty_String(&str)) { + appendCStr_String(&str, " \u2014 "); + } + appendFormat_String(&str, person_Icon " %s", + //escape_Color(tmBannerItemTitle_ColorId), + cstr_String(name_GmIdentity(ident))); + } + /* Show scheme and host. */ if (!isEmpty_String(&str)) { appendCStr_String(&str, "\n"); } -- cgit v1.2.3 From 8d03b1f7479e30873e8ecee4a62478c6fc0511cb Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 18 Dec 2021 19:25:03 +0200 Subject: Cleanup --- src/ui/documentwidget.c | 2 -- src/ui/linkinfo.c | 22 ++++------------------ 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 1870efd6..fa035d86 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -239,7 +239,6 @@ enum iDocumentWidgetFlag { rightWheelSwipe_DocumentWidgetFlag = iBit(20), eitherWheelSwipe_DocumentWidgetFlag = leftWheelSwipe_DocumentWidgetFlag | rightWheelSwipe_DocumentWidgetFlag, -// wheelSwipeFinished_DocumentWidgetFlag = iBit(21), }; enum iDocumentLinkOrdinalMode { @@ -250,7 +249,6 @@ enum iDocumentLinkOrdinalMode { enum iWheelSwipeState { none_WheelSwipeState, direct_WheelSwipeState, - //inertia_WheelSwipeState, }; struct Impl_DocumentWidget { diff --git a/src/ui/linkinfo.c b/src/ui/linkinfo.c index 38da84c7..cb1404fc 100644 --- a/src/ui/linkinfo.c +++ b/src/ui/linkinfo.c @@ -95,24 +95,10 @@ iBool update_LinkInfo(iLinkInfo *d, const iGmDocument *doc, iGmLinkId linkId, in if (showImage || showAudio) { appendFormat_String( &str, - "%s%s%s", - // showHost ? (scheme == mailto_GmLinkScheme ? cstr_String(url) - // : scheme != gemini_GmLinkScheme - // ? format_CStr("%s \u2014 %s", - // cstrCollect_String(upper_String( - // collectNewRange_String(parts.scheme))), - // cstr_Rangecc(parts.host)) - // : cstr_Rangecc(parts.host)) - // : "", - // showHost ? format_CStr("\x1b[1m%s\x1b[0m", cstr_Rangecc(parts.host)) : "", - showHost && (showImage || showAudio) ? " \u2014" : "", - showImage || showAudio - ? "" - : "", // escape_Color(linkColor_GmDocument(doc, linkId, domain_GmLinkPart)), - showImage || showAudio - ? format_CStr(showImage ? photo_Icon " %s " : "\U0001f3b5 %s", - cstr_Lang(showImage ? "link.hint.image" : "link.hint.audio")) - : ""); + "%s%s", + showHost ? " \u2014" : "", + format_CStr(showImage ? photo_Icon " %s " : "\U0001f3b5 %s", + cstr_Lang(showImage ? "link.hint.image" : "link.hint.audio"))); } } if (flags & visited_GmLinkFlag) { -- cgit v1.2.3 From 86ec7ac6940dd4b39a43b41e70b142fd2eda0ff3 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 19 Dec 2021 08:17:50 +0200 Subject: DocumentWidget: Refactor to separate DocumentView Work in progress, but now DocumentView at least has its own type. The relationship is a bit muddled, though. --- src/periodic.c | 1 + src/ui/documentwidget.c | 1431 +++++++++++++++++++++++------------------------ 2 files changed, 698 insertions(+), 734 deletions(-) diff --git a/src/periodic.c b/src/periodic.c index ef3d8033..c65c8b07 100644 --- a/src/periodic.c +++ b/src/periodic.c @@ -121,6 +121,7 @@ void deinit_Periodic(iPeriodic *d) { } void add_Periodic(iPeriodic *d, iAny *context, const char *command) { + iAssert(isInstance_Object(context, &Class_Widget)); lock_Mutex(d->mutex); size_t pos; iPeriodicCommand key = { .context = context }; diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index fa035d86..f5b9a4fc 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -251,6 +251,35 @@ enum iWheelSwipeState { direct_WheelSwipeState, }; +/* TODO: DocumentView is supposed to be useful on its own; move to a separate source file. */ +iDeclareType(DocumentView) + +struct Impl_DocumentView { + iDocumentWidget *owner; /* TODO: Convert to an abstract provider of metrics? */ + iGmDocument * doc; + int pageMargin; + iSmoothScroll scrollY; + iAnim sideOpacity; + iAnim altTextOpacity; + iGmRunRange visibleRuns; + iPtrArray visibleLinks; + iPtrArray visiblePre; + iPtrArray visibleMedia; /* currently playing audio / ongoing downloads */ + iPtrArray visibleWideRuns; /* scrollable blocks; TODO: merge into `visiblePre` */ + const iGmRun * hoverPre; /* for clicking */ + const iGmRun * hoverAltPre; /* for drawing alt text */ + const iGmRun * hoverLink; + iArray wideRunOffsets; + iAnim animWideRunOffset; + uint16_t animWideRunId; + iGmRunRange animWideRunRange; + iDrawBufs * drawBufs; /* dynamic state for drawing */ + iVisBuf * visBuf; + iVisBufMeta * visBufMeta; + iGmRunRange renderRuns; + iPtrSet * invalidRuns; +}; + struct Impl_DocumentWidget { iWidget widget; int flags; /* internal behavior, see enum iDocumentWidgetFlag */ @@ -264,9 +293,6 @@ struct Impl_DocumentWidget { const iGmRun * grabbedPlayer; /* currently adjusting volume in a player */ float grabbedStartVolume; int mediaTimer; - const iGmRun * hoverPre; /* for clicking */ - const iGmRun * hoverAltPre; /* for drawing alt text */ - const iGmRun * hoverLink; const iGmRun * contextLink; iClick click; iInt2 contextPos; /* coordinates of latest right click */ @@ -299,29 +325,11 @@ struct Impl_DocumentWidget { iBlock sourceContent; /* original content as received, for saving; set on request finish */ iTime sourceTime; iGempub * sourceGempub; /* NULL unless the page is Gempub content */ - iGmDocument * doc; iBanner * banner; + float initNormScrollY; /* Rendering: */ - int pageMargin; - float initNormScrollY; - iSmoothScroll scrollY; - iAnim sideOpacity; - iAnim altTextOpacity; - iGmRunRange visibleRuns; - iPtrArray visibleLinks; - iPtrArray visiblePre; - iPtrArray visibleMedia; /* currently playing audio / ongoing downloads */ - iPtrArray visibleWideRuns; /* scrollable blocks; TODO: merge into `visiblePre` */ - iArray wideRunOffsets; - iAnim animWideRunOffset; - uint16_t animWideRunId; - iGmRunRange animWideRunRange; - iDrawBufs * drawBufs; /* dynamic state for drawing */ - iVisBuf * visBuf; - iVisBufMeta * visBufMeta; - iGmRunRange renderRuns; - iPtrSet * invalidRuns; + iDocumentView view; iLinkInfo * linkInfo; /* Widget structure: */ @@ -338,6 +346,44 @@ iDefineObjectConstruction(DocumentWidget) static int docEnum_ = 0; +void init_DocumentView(iDocumentView *d) { + d->owner = NULL; + d->doc = new_GmDocument(); + d->invalidRuns = new_PtrSet(); + d->drawBufs = new_DrawBufs(); + d->pageMargin = 5; + if (deviceType_App() != desktop_AppDeviceType) { + d->scrollY.flags |= pullDownAction_SmoothScrollFlag; /* pull to refresh */ + } + d->hoverPre = NULL; + d->hoverAltPre = NULL; + d->hoverLink = NULL; + d->animWideRunId = 0; + init_Anim(&d->animWideRunOffset, 0); + iZap(d->renderRuns); + iZap(d->visibleRuns); + d->visBuf = new_VisBuf(); { + d->visBufMeta = malloc(sizeof(iVisBufMeta) * numBuffers_VisBuf); + /* Additional metadata for each buffer. */ + d->visBuf->bufferInvalidated = visBufInvalidated_; + for (size_t i = 0; i < numBuffers_VisBuf; i++) { + d->visBuf->buffers[i].user = d->visBufMeta + i; + } + } + init_Anim(&d->sideOpacity, 0); + init_Anim(&d->altTextOpacity, 0); + init_PtrArray(&d->visibleLinks); + init_PtrArray(&d->visiblePre); + init_PtrArray(&d->visibleWideRuns); + init_Array(&d->wideRunOffsets, sizeof(int)); + init_PtrArray(&d->visibleMedia); +} + +static void setOwner_DocumentView_(iDocumentView *d, iDocumentWidget *doc) { + d->owner = doc; + init_SmoothScroll(&d->scrollY, as_Widget(doc), scrollBegan_DocumentWidget_); +} + void init_DocumentWidget(iDocumentWidget *d) { iWidget *w = as_Widget(d); init_Widget(w); @@ -365,61 +411,33 @@ void init_DocumentWidget(iDocumentWidget *d) { d->request = NULL; d->isRequestUpdated = iFalse; d->media = new_ObjectList(); - d->doc = new_GmDocument(); d->banner = new_Banner(); setOwner_Banner(d->banner, d); d->redirectCount = 0; d->ordinalBase = 0; - d->initNormScrollY = 0; - init_SmoothScroll(&d->scrollY, w, scrollBegan_DocumentWidget_); - if (deviceType_App() != desktop_AppDeviceType) { - d->scrollY.flags |= pullDownAction_SmoothScrollFlag; /* pull to refresh */ - } - d->animWideRunId = 0; - init_Anim(&d->animWideRunOffset, 0); d->wheelSwipeState = none_WheelSwipeState; d->selectMark = iNullRange; d->foundMark = iNullRange; - d->pageMargin = 5; - d->hoverPre = NULL; - d->hoverAltPre = NULL; - d->hoverLink = NULL; d->contextLink = NULL; - iZap(d->renderRuns); - iZap(d->visibleRuns); - d->visBuf = new_VisBuf(); { - d->visBufMeta = malloc(sizeof(iVisBufMeta) * numBuffers_VisBuf); - /* Additional metadata for each buffer. */ - d->visBuf->bufferInvalidated = visBufInvalidated_; - for (size_t i = 0; i < numBuffers_VisBuf; i++) { - d->visBuf->buffers[i].user = d->visBufMeta + i; - } - } - d->invalidRuns = new_PtrSet(); - init_Anim(&d->sideOpacity, 0); - init_Anim(&d->altTextOpacity, 0); d->sourceStatus = none_GmStatusCode; init_String(&d->sourceHeader); init_String(&d->sourceMime); init_Block(&d->sourceContent, 0); iZap(d->sourceTime); - d->sourceGempub = NULL; - init_PtrArray(&d->visibleLinks); - init_PtrArray(&d->visiblePre); - init_PtrArray(&d->visibleWideRuns); - init_Array(&d->wideRunOffsets, sizeof(int)); - init_PtrArray(&d->visibleMedia); - d->grabbedPlayer = NULL; - d->mediaTimer = 0; + d->sourceGempub = NULL; + d->initNormScrollY = 0; + d->grabbedPlayer = NULL; + d->mediaTimer = 0; init_String(&d->pendingGotoHeading); init_String(&d->linePrecedingLink); init_Click(&d->click, d, SDL_BUTTON_LEFT); d->linkInfo = (deviceType_App() == desktop_AppDeviceType ? new_LinkInfo() : NULL); + init_DocumentView(&d->view); + setOwner_DocumentView_(&d->view, d); addChild_Widget(w, iClob(d->scroll = new_ScrollWidget())); d->menu = NULL; /* created when clicking */ d->playerMenu = NULL; d->copyMenu = NULL; - d->drawBufs = new_DrawBufs(); d->translation = NULL; addChildFlags_Widget(w, iClob(new_IndicatorWidget()), @@ -443,22 +461,32 @@ void cancelAllRequests_DocumentWidget(iDocumentWidget *d) { } if (d->request) { cancel_GmRequest(d->request); +} } + +void deinit_DocumentView(iDocumentView *d) { + delete_DrawBufs(d->drawBufs); + delete_VisBuf(d->visBuf); + free(d->visBufMeta); + delete_PtrSet(d->invalidRuns); + deinit_Array(&d->wideRunOffsets); + deinit_PtrArray(&d->visibleMedia); + deinit_PtrArray(&d->visibleWideRuns); + deinit_PtrArray(&d->visiblePre); + deinit_PtrArray(&d->visibleLinks); + iReleasePtr(&d->doc); } void deinit_DocumentWidget(iDocumentWidget *d) { // printf("\n* * * * * * * *\nDEINIT DOCUMENT: %s\n* * * * * * * *\n\n", // cstr_String(&d->widget.id)); fflush(stdout); cancelAllRequests_DocumentWidget(d); - pauseAllPlayers_Media(media_GmDocument(d->doc), iTrue); + pauseAllPlayers_Media(media_GmDocument(d->view.doc), iTrue); removeTicker_App(animate_DocumentWidget_, d); removeTicker_App(prerender_DocumentWidget_, d); remove_Periodic(periodic_App(), d); delete_Translation(d->translation); - delete_DrawBufs(d->drawBufs); - delete_VisBuf(d->visBuf); - free(d->visBufMeta); - delete_PtrSet(d->invalidRuns); + deinit_DocumentView(&d->view); delete_LinkInfo(d->linkInfo); iRelease(d->media); iRelease(d->request); @@ -469,15 +497,9 @@ void deinit_DocumentWidget(iDocumentWidget *d) { deinit_String(&d->sourceMime); deinit_String(&d->sourceHeader); delete_Banner(d->banner); - iRelease(d->doc); if (d->mediaTimer) { SDL_RemoveTimer(d->mediaTimer); } - deinit_Array(&d->wideRunOffsets); - deinit_PtrArray(&d->visibleMedia); - deinit_PtrArray(&d->visibleWideRuns); - deinit_PtrArray(&d->visiblePre); - deinit_PtrArray(&d->visibleLinks); delete_Block(d->certFingerprint); delete_String(d->certSubject); delete_String(d->titleUser); @@ -511,7 +533,7 @@ static void setLinkNumberMode_DocumentWidget_(iDocumentWidget *d, iBool set) { } } -static void resetWideRuns_DocumentWidget_(iDocumentWidget *d) { +static void resetWideRuns_DocumentView_(iDocumentView *d) { clear_Array(&d->wideRunOffsets); d->animWideRunId = 0; init_Anim(&d->animWideRunOffset, 0); @@ -539,8 +561,8 @@ static void requestFinished_DocumentWidget_(iAnyObject *obj) { d->request); } -static int documentWidth_DocumentWidget_(const iDocumentWidget *d) { - const iWidget *w = constAs_Widget(d); +static int documentWidth_DocumentView_(const iDocumentView *d) { + const iWidget *w = constAs_Widget(d->owner); const iRect bounds = bounds_Widget(w); const iPrefs * prefs = prefs_App(); const int minWidth = 50 * gap_UI; /* lines must fit a word at least */ @@ -552,18 +574,18 @@ static int documentWidth_DocumentWidget_(const iDocumentWidget *d) { prefs->lineWidth * prefs->zoomPercent / 100); } -static int documentTopPad_DocumentWidget_(const iDocumentWidget *d) { +static int documentTopPad_DocumentView_(const iDocumentView *d) { /* Amount of space between banner and top of the document. */ - return isEmpty_Banner(d->banner) ? 0 : lineHeight_Text(paragraph_FontId); + return isEmpty_Banner(d->owner->banner) ? 0 : lineHeight_Text(paragraph_FontId); } -static int documentTopMargin_DocumentWidget_(const iDocumentWidget *d) { - return (isEmpty_Banner(d->banner) ? d->pageMargin * gap_UI : height_Banner(d->banner)) + - documentTopPad_DocumentWidget_(d); +static int documentTopMargin_DocumentView_(const iDocumentView *d) { + return (isEmpty_Banner(d->owner->banner) ? d->pageMargin * gap_UI : height_Banner(d->owner->banner)) + + documentTopPad_DocumentView_(d); } -static int pageHeight_DocumentWidget_(const iDocumentWidget *d) { - return height_Banner(d->banner) + documentTopPad_DocumentWidget_(d) + size_GmDocument(d->doc).y; +static int pageHeight_DocumentView_(const iDocumentView *d) { + return height_Banner(d->owner->banner) + documentTopPad_DocumentView_(d) + size_GmDocument(d->doc).y; } static int phoneToolbarHeight_DocumentWidget_(const iDocumentWidget *d) { @@ -582,39 +604,41 @@ static int footerHeight_DocumentWidget_(const iDocumentWidget *d) { return hgt; } -static iRect documentBounds_DocumentWidget_(const iDocumentWidget *d) { - const iRect bounds = bounds_Widget(constAs_Widget(d)); +static iRect documentBounds_DocumentView_(const iDocumentView *d) { + const iRect bounds = bounds_Widget(constAs_Widget(d->owner)); const int margin = gap_UI * d->pageMargin; iRect rect; - rect.size.x = documentWidth_DocumentWidget_(d); + rect.size.x = documentWidth_DocumentView_(d); rect.pos.x = mid_Rect(bounds).x - rect.size.x / 2; rect.pos.y = top_Rect(bounds) + margin; rect.size.y = height_Rect(bounds) - margin; iBool wasCentered = iFalse; - if (d->flags & centerVertically_DocumentWidgetFlag) { + /* TODO: Further separation of View and Widget: configure header and footer heights + without involving the widget here. */ + if (d->owner->flags & centerVertically_DocumentWidgetFlag) { const int docSize = size_GmDocument(d->doc).y + - documentTopMargin_DocumentWidget_(d); + documentTopMargin_DocumentView_(d); if (size_GmDocument(d->doc).y == 0) { /* Document is empty; maybe just showing an error banner. */ rect.pos.y = top_Rect(bounds) + height_Rect(bounds) / 2 - - documentTopPad_DocumentWidget_(d) - height_Banner(d->banner) / 2; + documentTopPad_DocumentView_(d) - height_Banner(d->owner->banner) / 2; rect.size.y = 0; wasCentered = iTrue; } - else if (docSize < rect.size.y - footerHeight_DocumentWidget_(d)) { + else if (docSize < rect.size.y - footerHeight_DocumentWidget_(d->owner)) { /* TODO: Phone toolbar? */ /* Center vertically when the document is short. */ - const int relMidY = (height_Rect(bounds) - footerHeight_DocumentWidget_(d)) / 2; + const int relMidY = (height_Rect(bounds) - footerHeight_DocumentWidget_(d->owner)) / 2; const int visHeight = size_GmDocument(d->doc).y; - const int offset = -height_Banner(d->banner) - documentTopPad_DocumentWidget_(d); + const int offset = -height_Banner(d->owner->banner) - documentTopPad_DocumentView_(d); rect.pos.y = top_Rect(bounds) + iMaxi(0, relMidY - visHeight / 2 + offset); - rect.size.y = size_GmDocument(d->doc).y + documentTopMargin_DocumentWidget_(d); + rect.size.y = size_GmDocument(d->doc).y + documentTopMargin_DocumentView_(d); wasCentered = iTrue; } } if (!wasCentered) { /* The banner overtakes the top margin. */ - if (!isEmpty_Banner(d->banner)) { + if (!isEmpty_Banner(d->owner->banner)) { rect.pos.y -= margin; } else { @@ -624,26 +648,28 @@ static iRect documentBounds_DocumentWidget_(const iDocumentWidget *d) { return rect; } -static int viewPos_DocumentWidget_(const iDocumentWidget *d) { - return height_Banner(d->banner) + documentTopPad_DocumentWidget_(d) - pos_SmoothScroll(&d->scrollY); +static int viewPos_DocumentView_(const iDocumentView *d) { + return height_Banner(d->owner->banner) + documentTopPad_DocumentView_(d) - + pos_SmoothScroll(&d->scrollY); } -static iInt2 documentPos_DocumentWidget_(const iDocumentWidget *d, iInt2 pos) { - return addY_I2(sub_I2(pos, topLeft_Rect(documentBounds_DocumentWidget_(d))), - -viewPos_DocumentWidget_(d)); +static iInt2 documentPos_DocumentView_(const iDocumentView *d, iInt2 pos) { + return addY_I2(sub_I2(pos, topLeft_Rect(documentBounds_DocumentView_(d))), + -viewPos_DocumentView_(d)); } -static iRangei visibleRange_DocumentWidget_(const iDocumentWidget *d) { - int top = pos_SmoothScroll(&d->scrollY) - height_Banner(d->banner) - documentTopPad_DocumentWidget_(d); - if (isEmpty_Banner(d->banner)) { +static iRangei visibleRange_DocumentView_(const iDocumentView *d) { + int top = pos_SmoothScroll(&d->scrollY) - height_Banner(d->owner->banner) - + documentTopPad_DocumentView_(d); + if (isEmpty_Banner(d->owner->banner)) { /* Top padding is not collapsed. */ top -= d->pageMargin * gap_UI; } - return (iRangei){ top, top + height_Rect(bounds_Widget(constAs_Widget(d))) }; + return (iRangei){ top, top + height_Rect(bounds_Widget(constAs_Widget(d->owner))) }; } -static void addVisible_DocumentWidget_(void *context, const iGmRun *run) { - iDocumentWidget *d = context; +static void addVisible_DocumentView_(void *context, const iGmRun *run) { + iDocumentView *d = context; if (~run->flags & decoration_GmRunFlag && !run->mediaId) { if (!d->visibleRuns.start) { d->visibleRuns.start = run; @@ -666,7 +692,7 @@ static void addVisible_DocumentWidget_(void *context, const iGmRun *run) { } } -static const iGmRun *lastVisibleLink_DocumentWidget_(const iDocumentWidget *d) { +static const iGmRun *lastVisibleLink_DocumentView_(const iDocumentView *d) { iReverseConstForEach(PtrArray, i, &d->visibleLinks) { const iGmRun *run = i.ptr; if (run->flags & decoration_GmRunFlag && run->linkId) { @@ -676,8 +702,8 @@ static const iGmRun *lastVisibleLink_DocumentWidget_(const iDocumentWidget *d) { return NULL; } -static float normScrollPos_DocumentWidget_(const iDocumentWidget *d) { - const int docSize = pageHeight_DocumentWidget_(d); // size_GmDocument(d->doc).y; +static float normScrollPos_DocumentView_(const iDocumentView *d) { + const int docSize = pageHeight_DocumentView_(d); if (docSize) { float pos = pos_SmoothScroll(&d->scrollY) / (float) docSize; return iMax(pos, 0.0f); @@ -685,15 +711,15 @@ static float normScrollPos_DocumentWidget_(const iDocumentWidget *d) { return 0; } -static int scrollMax_DocumentWidget_(const iDocumentWidget *d) { - const iWidget *w = constAs_Widget(d); - int sm = pageHeight_DocumentWidget_(d) + - (isEmpty_Banner(d->banner) ? 2 : 1) * d->pageMargin * gap_UI + /* top and bottom margins */ - footerHeight_DocumentWidget_(d) - height_Rect(bounds_Widget(w)); +static int scrollMax_DocumentView_(const iDocumentView *d) { + const iWidget *w = constAs_Widget(d->owner); + int sm = pageHeight_DocumentView_(d) + + (isEmpty_Banner(d->owner->banner) ? 2 : 1) * d->pageMargin * gap_UI + /* top and bottom margins */ + footerHeight_DocumentWidget_(d->owner) - height_Rect(bounds_Widget(w)); return sm; } -static void invalidateLink_DocumentWidget_(iDocumentWidget *d, iGmLinkId id) { +static void invalidateLink_DocumentView_(iDocumentView *d, iGmLinkId id) { /* A link has multiple runs associated with it. */ iConstForEach(PtrArray, i, &d->visibleLinks) { const iGmRun *run = i.ptr; @@ -703,7 +729,7 @@ static void invalidateLink_DocumentWidget_(iDocumentWidget *d, iGmLinkId id) { } } -static void invalidateVisibleLinks_DocumentWidget_(iDocumentWidget *d) { +static void invalidateVisibleLinks_DocumentView_(iDocumentView *d) { iConstForEach(PtrArray, i, &d->visibleLinks) { const iGmRun *run = i.ptr; if (run->linkId) { @@ -712,7 +738,7 @@ static void invalidateVisibleLinks_DocumentWidget_(iDocumentWidget *d) { } } -static int runOffset_DocumentWidget_(const iDocumentWidget *d, const iGmRun *run) { +static int runOffset_DocumentView_(const iDocumentView *d, const iGmRun *run) { if (preId_GmRun(run) && run->flags & wide_GmRunFlag) { if (d->animWideRunId == preId_GmRun(run)) { return -value_Anim(&d->animWideRunOffset); @@ -726,21 +752,20 @@ static int runOffset_DocumentWidget_(const iDocumentWidget *d, const iGmRun *run return 0; } -static void invalidateWideRunsWithNonzeroOffset_DocumentWidget_(iDocumentWidget *d) { +static void invalidateWideRunsWithNonzeroOffset_DocumentView_(iDocumentView *d) { iConstForEach(PtrArray, i, &d->visibleWideRuns) { const iGmRun *run = i.ptr; - if (runOffset_DocumentWidget_(d, run)) { + if (runOffset_DocumentView_(d, run)) { insert_PtrSet(d->invalidRuns, run); } } } -static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse); - static void animate_DocumentWidget_(void *ticker) { iDocumentWidget *d = ticker; + iAssert(isInstance_Object(d, &Class_DocumentWidget)); refresh_Widget(d); - if (!isFinished_Anim(&d->sideOpacity) || !isFinished_Anim(&d->altTextOpacity) || + if (!isFinished_Anim(&d->view.sideOpacity) || !isFinished_Anim(&d->view.altTextOpacity) || (d->linkInfo && !isFinished_Anim(&d->linkInfo->opacity))) { addTicker_App(animate_DocumentWidget_, d); } @@ -768,15 +793,15 @@ static iBool isHoverAllowed_DocumentWidget_(const iDocumentWidget *d) { return iTrue; } -static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) { - const iWidget *w = constAs_Widget(d); - const iRect docBounds = documentBounds_DocumentWidget_(d); +static void updateHover_DocumentView_(iDocumentView *d, iInt2 mouse) { + const iWidget *w = constAs_Widget(d->owner); + const iRect docBounds = documentBounds_DocumentView_(d); const iGmRun * oldHoverLink = d->hoverLink; - d->hoverPre = NULL; - d->hoverLink = NULL; + d->hoverPre = NULL; + d->hoverLink = NULL; const iInt2 hoverPos = addY_I2(sub_I2(mouse, topLeft_Rect(docBounds)), - -viewPos_DocumentWidget_(d)); - if (isHoverAllowed_DocumentWidget_(d)) { + -viewPos_DocumentView_(d)); + if (isHoverAllowed_DocumentWidget_(d->owner)) { iConstForEach(PtrArray, i, &d->visibleLinks) { const iGmRun *run = i.ptr; /* Click targets are slightly expanded so there are no gaps between links. */ @@ -788,19 +813,21 @@ static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) { } if (d->hoverLink != oldHoverLink) { if (oldHoverLink) { - invalidateLink_DocumentWidget_(d, oldHoverLink->linkId); + invalidateLink_DocumentView_(d, oldHoverLink->linkId); } if (d->hoverLink) { - invalidateLink_DocumentWidget_(d, d->hoverLink->linkId); + invalidateLink_DocumentView_(d, d->hoverLink->linkId); } - if (update_LinkInfo(d->linkInfo, d->doc, d->hoverLink ? d->hoverLink->linkId : 0, + if (update_LinkInfo(d->owner->linkInfo, + d->doc, + d->hoverLink ? d->hoverLink->linkId : 0, width_Widget(w))) { - animate_DocumentWidget_(d); + animate_DocumentWidget_(d->owner); } refresh_Widget(w); } /* Hovering over preformatted blocks. */ - if (isHoverAllowed_DocumentWidget_(d)) { + if (isHoverAllowed_DocumentWidget_(d->owner)) { iConstForEach(PtrArray, j, &d->visiblePre) { const iGmRun *run = j.ptr; if (contains_Rect(run->bounds, hoverPos)) { @@ -813,18 +840,18 @@ static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) { if (!d->hoverPre) { setValueSpeed_Anim(&d->altTextOpacity, 0.0f, 1.5f); if (!isFinished_Anim(&d->altTextOpacity)) { - animate_DocumentWidget_(d); + animate_DocumentWidget_(d->owner); } } else if (d->hoverPre && preHasAltText_GmDocument(d->doc, preId_GmRun(d->hoverPre)) && - ~d->flags & noHoverWhileScrolling_DocumentWidgetFlag) { + ~d->owner->flags & noHoverWhileScrolling_DocumentWidgetFlag) { setValueSpeed_Anim(&d->altTextOpacity, 1.0f, 1.5f); if (!isFinished_Anim(&d->altTextOpacity)) { - animate_DocumentWidget_(d); + animate_DocumentWidget_(d->owner); } } - if (isHover_Widget(w) && !contains_Widget(constAs_Widget(d->scroll), mouse)) { + if (isHover_Widget(w) && !contains_Widget(constAs_Widget(d->owner->scroll), mouse)) { setCursor_Window(get_Window(), d->hoverLink || d->hoverPre ? SDL_SYSTEM_CURSOR_HAND : SDL_SYSTEM_CURSOR_IBEAM); @@ -835,15 +862,14 @@ static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) { } } -static void updateSideOpacity_DocumentWidget_(iDocumentWidget *d, iBool isAnimated) { +static void updateSideOpacity_DocumentView_(iDocumentView *d, iBool isAnimated) { float opacity = 0.0f; -// const iGmRun *banner = siteBanner_GmDocument(d->doc); - if (!isEmpty_Banner(d->banner) && height_Banner(d->banner) < pos_SmoothScroll(&d->scrollY)) { -// if (banner && bottom_Rect(banner->visBounds) < pos_SmoothScroll(&d->scrollY)) { + if (!isEmpty_Banner(d->owner->banner) && + height_Banner(d->owner->banner) < pos_SmoothScroll(&d->scrollY)) { opacity = 1.0f; } setValue_Anim(&d->sideOpacity, opacity, isAnimated ? (opacity < 0.5f ? 100 : 200) : 0); - animate_DocumentWidget_(d); + animate_DocumentWidget_(d->owner); } static uint32_t mediaUpdateInterval_DocumentWidget_(const iDocumentWidget *d) { @@ -855,10 +881,10 @@ static uint32_t mediaUpdateInterval_DocumentWidget_(const iDocumentWidget *d) { } static const uint32_t invalidInterval_ = ~0u; uint32_t interval = invalidInterval_; - iConstForEach(PtrArray, i, &d->visibleMedia) { + iConstForEach(PtrArray, i, &d->view.visibleMedia) { const iGmRun *run = i.ptr; if (run->mediaType == audio_MediaType) { - iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run)); + iPlayer *plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run)); if (flags_Player(plr) & adjustingVolume_PlayerFlag || (isStarted_Player(plr) && !isPaused_Player(plr))) { interval = iMin(interval, 1000 / 15); @@ -881,10 +907,10 @@ static uint32_t postMediaUpdate_DocumentWidget_(uint32_t interval, void *context static void updateMedia_DocumentWidget_(iDocumentWidget *d) { if (document_App() == d) { refresh_Widget(d); - iConstForEach(PtrArray, i, &d->visibleMedia) { + iConstForEach(PtrArray, i, &d->view.visibleMedia) { const iGmRun *run = i.ptr; if (run->mediaType == audio_MediaType) { - iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run)); + iPlayer *plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run)); if (idleTimeMs_Player(plr) > 3000 && ~flags_Player(plr) & volumeGrabbed_PlayerFlag && flags_Player(plr) & adjustingVolume_PlayerFlag) { setFlags_Player(plr, adjustingVolume_PlayerFlag, iFalse); @@ -912,7 +938,7 @@ static void animateMedia_DocumentWidget_(iDocumentWidget *d) { } } -static iRangecc currentHeading_DocumentWidget_(const iDocumentWidget *d) { +static iRangecc currentHeading_DocumentView_(const iDocumentView *d) { iRangecc heading = iNullRange; if (d->visibleRuns.start) { iConstForEach(Array, i, headings_GmDocument(d->doc)) { @@ -930,65 +956,68 @@ static iRangecc currentHeading_DocumentWidget_(const iDocumentWidget *d) { return heading; } -static int updateScrollMax_DocumentWidget_(iDocumentWidget *d) { - arrange_Widget(d->footerButtons); /* scrollMax depends on footer height */ - const int scrollMax = scrollMax_DocumentWidget_(d); +static int updateScrollMax_DocumentView_(iDocumentView *d) { + arrange_Widget(d->owner->footerButtons); /* scrollMax depends on footer height */ + const int scrollMax = scrollMax_DocumentView_(d); setMax_SmoothScroll(&d->scrollY, scrollMax); return scrollMax; } -static void updateVisible_DocumentWidget_(iDocumentWidget *d) { - iChangeFlags(d->flags, +static void updateVisible_DocumentView_(iDocumentView *d) { + /* TODO: The concerns of Widget and View are too tangled together here. */ + iChangeFlags(d->owner->flags, centerVertically_DocumentWidgetFlag, - prefs_App()->centerShortDocs || startsWithCase_String(d->mod.url, "about:") || - !isSuccess_GmStatusCode(d->sourceStatus)); - const iRangei visRange = visibleRange_DocumentWidget_(d); + prefs_App()->centerShortDocs || startsWithCase_String(d->owner->mod.url, "about:") || + !isSuccess_GmStatusCode(d->owner->sourceStatus)); + iScrollWidget *scrollBar = d->owner->scroll; + const iRangei visRange = visibleRange_DocumentView_(d); // printf("visRange: %d...%d\n", visRange.start, visRange.end); - const iRect bounds = bounds_Widget(as_Widget(d)); - const int scrollMax = updateScrollMax_DocumentWidget_(d); + const iRect bounds = bounds_Widget(as_Widget(d->owner)); + const int scrollMax = updateScrollMax_DocumentView_(d); /* Reposition the footer buttons as appropriate. */ - setRange_ScrollWidget(d->scroll, (iRangei){ 0, scrollMax }); - const int docSize = pageHeight_DocumentWidget_(d) + footerHeight_DocumentWidget_(d); + setRange_ScrollWidget(scrollBar, (iRangei){ 0, scrollMax }); + const int docSize = pageHeight_DocumentView_(d) + footerHeight_DocumentWidget_(d->owner); const float scrollPos = pos_SmoothScroll(&d->scrollY); - setThumb_ScrollWidget(d->scroll, + setThumb_ScrollWidget(scrollBar, pos_SmoothScroll(&d->scrollY), docSize > 0 ? height_Rect(bounds) * size_Range(&visRange) / docSize : 0); - if (d->footerButtons) { - const iRect bounds = bounds_Widget(as_Widget(d)); - const iRect docBounds = documentBounds_DocumentWidget_(d); + if (d->owner->footerButtons) { + const iRect bounds = bounds_Widget(as_Widget(d->owner)); + const iRect docBounds = documentBounds_DocumentView_(d); const int hPad = (width_Rect(bounds) - iMin(120 * gap_UI, width_Rect(docBounds))) / 2; const int vPad = 3 * gap_UI; - setPadding_Widget(d->footerButtons, hPad, 0, hPad, vPad); - d->footerButtons->rect.pos.y = height_Rect(bounds) - footerHeight_DocumentWidget_(d) + - (scrollMax > 0 ? scrollMax - scrollPos : 0); + setPadding_Widget(d->owner->footerButtons, hPad, 0, hPad, vPad); + d->owner->footerButtons->rect.pos.y = height_Rect(bounds) - + footerHeight_DocumentWidget_(d->owner) + + (scrollMax > 0 ? scrollMax - scrollPos : 0); } clear_PtrArray(&d->visibleLinks); clear_PtrArray(&d->visibleWideRuns); clear_PtrArray(&d->visiblePre); clear_PtrArray(&d->visibleMedia); - const iRangecc oldHeading = currentHeading_DocumentWidget_(d); + const iRangecc oldHeading = currentHeading_DocumentView_(d); /* Scan for visible runs. */ { iZap(d->visibleRuns); - render_GmDocument(d->doc, visRange, addVisible_DocumentWidget_, d); + render_GmDocument(d->doc, visRange, addVisible_DocumentView_, d); } - const iRangecc newHeading = currentHeading_DocumentWidget_(d); + const iRangecc newHeading = currentHeading_DocumentView_(d); if (memcmp(&oldHeading, &newHeading, sizeof(oldHeading))) { d->drawBufs->flags |= updateSideBuf_DrawBufsFlag; } - updateHover_DocumentWidget_(d, mouseCoord_Window(get_Window(), 0)); - updateSideOpacity_DocumentWidget_(d, iTrue); - animateMedia_DocumentWidget_(d); + updateHover_DocumentView_(d, mouseCoord_Window(get_Window(), 0)); + updateSideOpacity_DocumentView_(d, iTrue); + animateMedia_DocumentWidget_(d->owner); /* Remember scroll positions of recently visited pages. */ { - iRecentUrl *recent = mostRecentUrl_History(d->mod.history); - if (recent && docSize && d->state == ready_RequestState && - equal_String(&recent->url, d->mod.url)) { - recent->normScrollY = normScrollPos_DocumentWidget_(d); + iRecentUrl *recent = mostRecentUrl_History(d->owner->mod.history); + if (recent && docSize && d->owner->state == ready_RequestState && + equal_String(&recent->url, d->owner->mod.url)) { + recent->normScrollY = normScrollPos_DocumentView_(d); } } /* After scrolling/resizing stops, begin pre-rendering the visbuf contents. */ { - removeTicker_App(prerender_DocumentWidget_, d); + removeTicker_App(prerender_DocumentWidget_, d->owner); remove_Periodic(periodic_App(), d); - add_Periodic(periodic_App(), d, "document.render"); + add_Periodic(periodic_App(), d->owner, "document.render"); } } @@ -1000,8 +1029,8 @@ static void updateWindowTitle_DocumentWidget_(const iDocumentWidget *d) { return; } iStringArray *title = iClob(new_StringArray()); - if (!isEmpty_String(title_GmDocument(d->doc))) { - pushBack_StringArray(title, title_GmDocument(d->doc)); + if (!isEmpty_String(title_GmDocument(d->view.doc))) { + pushBack_StringArray(title, title_GmDocument(d->view.doc)); } if (!isEmpty_String(d->titleUser)) { pushBack_StringArray(title, d->titleUser); @@ -1032,7 +1061,7 @@ static void updateWindowTitle_DocumentWidget_(const iDocumentWidget *d) { setTitle_MainWindow(get_MainWindow(), text); setWindow = iFalse; } - const iChar siteIcon = siteIcon_GmDocument(d->doc); + const iChar siteIcon = siteIcon_GmDocument(d->view.doc); if (siteIcon) { if (!isEmpty_String(text)) { prependCStr_String(text, " " restore_ColorEscape); @@ -1072,7 +1101,7 @@ static void updateWindowTitle_DocumentWidget_(const iDocumentWidget *d) { } } -static void updateTimestampBuf_DocumentWidget_(const iDocumentWidget *d) { +static void updateTimestampBuf_DocumentView_(const iDocumentView *d) { if (!isExposed_Window(get_Window())) { return; } @@ -1080,17 +1109,22 @@ static void updateTimestampBuf_DocumentWidget_(const iDocumentWidget *d) { delete_TextBuf(d->drawBufs->timestampBuf); d->drawBufs->timestampBuf = NULL; } - if (isValid_Time(&d->sourceTime)) { + if (isValid_Time(&d->owner->sourceTime)) { iString *fmt = timeFormatHourPreference_Lang("page.timestamp"); d->drawBufs->timestampBuf = newRange_TextBuf( uiLabel_FontId, white_ColorId, - range_String(collect_String(format_Time(&d->sourceTime, cstr_String(fmt))))); + range_String(collect_String(format_Time(&d->owner->sourceTime, cstr_String(fmt))))); delete_String(fmt); } d->drawBufs->flags &= ~updateTimestampBuf_DrawBufsFlag; } +static void invalidate_DocumentView_(iDocumentView *d) { + invalidate_VisBuf(d->visBuf); + clear_PtrSet(d->invalidRuns); +} + static void invalidate_DocumentWidget_(iDocumentWidget *d) { if (flags_Widget(as_Widget(d)) & destroyPending_WidgetFlag) { return; @@ -1103,8 +1137,7 @@ static void invalidate_DocumentWidget_(iDocumentWidget *d) { return; } d->flags &= ~invalidationPending_DocumentWidgetFlag; - invalidate_VisBuf(d->visBuf); - clear_PtrSet(d->invalidRuns); + invalidate_DocumentView_(&d->view); // printf("[%p] '%s' invalidated\n", d, cstr_String(id_Widget(as_Widget(d)))); } @@ -1113,17 +1146,21 @@ static iRangecc siteText_DocumentWidget_(const iDocumentWidget *d) { : range_String(d->titleUser); } -static void documentRunsInvalidated_DocumentWidget_(iDocumentWidget *d) { - d->foundMark = iNullRange; - d->selectMark = iNullRange; +static void documentRunsInvalidated_DocumentView_(iDocumentView *d) { d->hoverPre = NULL; d->hoverAltPre = NULL; d->hoverLink = NULL; - d->contextLink = NULL; iZap(d->visibleRuns); iZap(d->renderRuns); } +static void documentRunsInvalidated_DocumentWidget_(iDocumentWidget *d) { + d->foundMark = iNullRange; + d->selectMark = iNullRange; + d->contextLink = NULL; + documentRunsInvalidated_DocumentView_(&d->view); +} + iBool isPinned_DocumentWidget_(const iDocumentWidget *d) { if (deviceType_App() == phone_AppDeviceType) { return iFalse; @@ -1150,11 +1187,11 @@ static void showOrHidePinningIndicator_DocumentWidget_(iDocumentWidget *d) { static void documentWasChanged_DocumentWidget_(iDocumentWidget *d) { iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iFalse); setFlags_Widget(as_Widget(d), touchDrag_WidgetFlag, iFalse); - updateVisitedLinks_GmDocument(d->doc); + updateVisitedLinks_GmDocument(d->view.doc); documentRunsInvalidated_DocumentWidget_(d); updateWindowTitle_DocumentWidget_(d); - updateVisible_DocumentWidget_(d); - d->drawBufs->flags |= updateSideBuf_DrawBufsFlag; + updateVisible_DocumentView_(&d->view); + d->view.drawBufs->flags |= updateSideBuf_DrawBufsFlag; invalidate_DocumentWidget_(d); refresh_Widget(as_Widget(d)); /* Check for special bookmark tags. */ @@ -1169,15 +1206,15 @@ static void documentWasChanged_DocumentWidget_(iDocumentWidget *d) { showOrHidePinningIndicator_DocumentWidget_(d); if (~d->flags & fromCache_DocumentWidgetFlag) { setCachedDocument_History(d->mod.history, - d->doc, /* keeps a ref */ + d->view.doc, /* keeps a ref */ (d->flags & openedFromSidebar_DocumentWidgetFlag) != 0); } } void setSource_DocumentWidget(iDocumentWidget *d, const iString *source) { - setUrl_GmDocument(d->doc, d->mod.url); - const int docWidth = documentWidth_DocumentWidget_(d); - setSource_GmDocument(d->doc, + setUrl_GmDocument(d->view.doc, d->mod.url); + const int docWidth = documentWidth_DocumentView_(&d->view); + setSource_GmDocument(d->view.doc, source, docWidth, width_Widget(d), @@ -1188,22 +1225,21 @@ void setSource_DocumentWidget(iDocumentWidget *d, const iString *source) { } static void replaceDocument_DocumentWidget_(iDocumentWidget *d, iGmDocument *newDoc) { - pauseAllPlayers_Media(media_GmDocument(d->doc), iTrue); - iRelease(d->doc); - d->doc = ref_Object(newDoc); + pauseAllPlayers_Media(media_GmDocument(d->view.doc), iTrue); + iRelease(d->view.doc); + d->view.doc = ref_Object(newDoc); documentWasChanged_DocumentWidget_(d); } static void updateBanner_DocumentWidget_(iDocumentWidget *d) { - setSite_Banner(d->banner, siteText_DocumentWidget_(d), siteIcon_GmDocument(d->doc)); + setSite_Banner(d->banner, siteText_DocumentWidget_(d), siteIcon_GmDocument(d->view.doc)); } static void updateTheme_DocumentWidget_(iDocumentWidget *d) { if (document_App() != d || category_GmStatusCode(d->sourceStatus) == categoryInput_GmStatusCode) { return; } -// setThemeSeed_GmDocument(d->doc, urlThemeSeed_String(d->mod.url)); /* theme palette and icon */ - d->drawBufs->flags |= updateTimestampBuf_DrawBufsFlag; + d->view.drawBufs->flags |= updateTimestampBuf_DrawBufsFlag; updateBanner_DocumentWidget_(d); } @@ -1234,17 +1270,20 @@ static void makeFooterButtons_DocumentWidget_(iDocumentWidget *d, const iMenuIte addChild_Widget(as_Widget(d), iClob(d->footerButtons)); arrange_Widget(d->footerButtons); arrange_Widget(w); - updateVisible_DocumentWidget_(d); /* final placement for the buttons */ + updateVisible_DocumentView_(&d->view); /* final placement for the buttons */ +} + +static void resetScroll_DocumentView_(iDocumentView *d) { + reset_SmoothScroll(&d->scrollY); + init_Anim(&d->sideOpacity, 0); + init_Anim(&d->altTextOpacity, 0); + resetWideRuns_DocumentView_(d); } static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode code, const iString *meta) { - /* TODO: No such thing as an "error page". It should be an empty page with an error banner. */ - iString *src = collectNew_String(); + iString *src = collectNew_String(); const iGmError *msg = get_GmError(code); - // appendChar_String(src, msg->icon ? msg->icon : 0x2327); /* X in a box */ - //appendFormat_String(src, " %s\n%s", msg->title, msg->info); -// iBool useBanner = iTrue; destroy_Widget(d->footerButtons); d->footerButtons = NULL; const iString *serverErrorMsg = NULL; @@ -1330,7 +1369,7 @@ static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode } /* Make a new document for the error page.*/ iGmDocument *errorDoc = new_GmDocument(); - setWidth_GmDocument(errorDoc, documentWidth_DocumentWidget_(d), width_Widget(d)); + setWidth_GmDocument(errorDoc, documentWidth_DocumentView_(&d->view), width_Widget(d)); setUrl_GmDocument(errorDoc, d->mod.url); setFormat_GmDocument(errorDoc, gemini_SourceFormat); replaceDocument_DocumentWidget_(d, errorDoc); @@ -1340,10 +1379,7 @@ static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode d->state = ready_RequestState; setSource_DocumentWidget(d, src); updateTheme_DocumentWidget_(d); - reset_SmoothScroll(&d->scrollY); - init_Anim(&d->sideOpacity, 0); - init_Anim(&d->altTextOpacity, 0); - resetWideRuns_DocumentWidget_(d); + resetScroll_DocumentView_(&d->view); } static void updateFetchProgress_DocumentWidget_(iDocumentWidget *d) { @@ -1459,9 +1495,9 @@ static void postProcessRequestContent_DocumentWidget_(iDocumentWidget *d, iBool "document.save" } }, 2); } - if (preloadCoverImage_Gempub(d->sourceGempub, d->doc)) { - redoLayout_GmDocument(d->doc); - updateVisible_DocumentWidget_(d); + if (preloadCoverImage_Gempub(d->sourceGempub, d->view.doc)) { + redoLayout_GmDocument(d->view.doc); + updateVisible_DocumentView_(&d->view); invalidate_DocumentWidget_(d); } } @@ -1545,6 +1581,14 @@ static void postProcessRequestContent_DocumentWidget_(iDocumentWidget *d, iBool } } +static void updateWidth_DocumentView_(iDocumentView *d) { + updateWidth_GmDocument(d->doc, documentWidth_DocumentView_(d), width_Widget(d->owner)); +} + +static void updateWidthAndRedoLayout_DocumentView_(iDocumentView *d) { + setWidth_GmDocument(d->doc, documentWidth_DocumentView_(d), width_Widget(d->owner)); +} + static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse *response, iGmDocument *cachedDoc, @@ -1565,7 +1609,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, } clear_String(&d->sourceMime); d->sourceTime = response->when; - d->drawBufs->flags |= updateTimestampBuf_DrawBufsFlag; + d->view.drawBufs->flags |= updateTimestampBuf_DrawBufsFlag; initBlock_String(&str, &response->body); /* Note: Body may be megabytes in size. */ if (isSuccess_GmStatusCode(statusCode)) { /* Check the MIME type. */ @@ -1709,16 +1753,16 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, format_String(&str, "=> %s %s\n", cstr_String(canonicalUrl_String(d->mod.url)), linkTitle); - setData_Media(media_GmDocument(d->doc), + setData_Media(media_GmDocument(d->view.doc), imgLinkId, mimeStr, &response->body, !isRequestFinished ? partialData_MediaFlag : 0); - redoLayout_GmDocument(d->doc); + redoLayout_GmDocument(d->view.doc); } else if (isAudio && !isInitialUpdate) { /* Update the audio content. */ - setData_Media(media_GmDocument(d->doc), + setData_Media(media_GmDocument(d->view.doc), imgLinkId, mimeStr, &response->body, @@ -1748,11 +1792,11 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, return; } d->flags |= drawDownloadCounter_DocumentWidgetFlag; - clear_PtrSet(d->invalidRuns); + clear_PtrSet(d->view.invalidRuns); deinit_String(&str); return; } - setFormat_GmDocument(d->doc, docFormat); + setFormat_GmDocument(d->view.doc, docFormat); /* Convert the source to UTF-8 if needed. */ if (!equalCase_Rangecc(charset, "utf-8")) { set_String(&str, @@ -1761,7 +1805,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, } if (cachedDoc) { replaceDocument_DocumentWidget_(d, cachedDoc); - updateWidth_GmDocument(d->doc, documentWidth_DocumentWidget_(d), width_Widget(d)); + updateWidth_DocumentView_(&d->view); } else if (setSource) { setSource_DocumentWidget(d, &str); @@ -1840,9 +1884,9 @@ static void cacheDocumentGlyphs_DocumentWidget_(const iDocumentWidget *d) { /* Just cache the top of the document, since this is what we usually need. */ int maxY = height_Widget(&d->widget) * 2; if (maxY == 0) { - maxY = size_GmDocument(d->doc).y; + maxY = size_GmDocument(d->view.doc).y; } - render_GmDocument(d->doc, (iRangei){ 0, maxY }, cacheRunGlyphs_, NULL); + render_GmDocument(d->view.doc, (iRangei){ 0, maxY }, cacheRunGlyphs_, NULL); } } @@ -1893,7 +1937,7 @@ static void addBannerWarnings_DocumentWidget_(iDocumentWidget *d) { value_SiteSpec(collectNewRange_String(urlRoot_String(d->mod.url)), dismissWarnings_SiteSpecKey) | (!prefs_App()->warnAboutMissingGlyphs ? missingGlyphs_GmDocumentWarning : 0); - const int warnings = warnings_GmDocument(d->doc) & ~dismissed; + const int warnings = warnings_GmDocument(d->view.doc) & ~dismissed; if (warnings & missingGlyphs_GmDocumentWarning) { add_Banner(d->banner, warning_BannerType, missingGlyphs_GmStatusCode, NULL, NULL); /* TODO: List one or more of the missing characters and/or their Unicode blocks? */ @@ -1910,12 +1954,11 @@ static void updateFromCachedResponse_DocumentWidget_(iDocumentWidget *d, float n clear_ObjectList(d->media); delete_Gempub(d->sourceGempub); d->sourceGempub = NULL; - pauseAllPlayers_Media(media_GmDocument(d->doc), iTrue); - iRelease(d->doc); + pauseAllPlayers_Media(media_GmDocument(d->view.doc), iTrue); destroy_Widget(d->footerButtons); d->footerButtons = NULL; - d->doc = new_GmDocument(); - resetWideRuns_DocumentWidget_(d); + iRelease(d->view.doc); + d->view.doc = new_GmDocument(); d->state = fetching_RequestState; d->flags |= fromCache_DocumentWidgetFlag; /* Do the fetch. */ { @@ -1927,7 +1970,7 @@ static void updateFromCachedResponse_DocumentWidget_(iDocumentWidget *d, float n format_String(&d->sourceHeader, cstr_Lang("pageinfo.header.cached")); set_Block(&d->sourceContent, &resp->body); if (!cachedDoc) { - setWidth_GmDocument(d->doc, documentWidth_DocumentWidget_(d), width_Widget(d)); + updateWidthAndRedoLayout_DocumentView_(&d->view); } updateDocument_DocumentWidget_(d, resp, cachedDoc, iTrue); clear_Banner(d->banner); @@ -1936,14 +1979,13 @@ static void updateFromCachedResponse_DocumentWidget_(iDocumentWidget *d, float n } d->state = ready_RequestState; postProcessRequestContent_DocumentWidget_(d, iTrue); - init_Anim(&d->altTextOpacity, 0); - reset_SmoothScroll(&d->scrollY); - init_Anim(&d->scrollY.pos, d->initNormScrollY * pageHeight_DocumentWidget_(d)); - updateSideOpacity_DocumentWidget_(d, iFalse); - updateVisible_DocumentWidget_(d); - moveSpan_SmoothScroll(&d->scrollY, 0, 0); /* clamp position to new max */ + resetScroll_DocumentView_(&d->view); + init_Anim(&d->view.scrollY.pos, d->initNormScrollY * pageHeight_DocumentView_(&d->view)); + updateVisible_DocumentView_(&d->view); + moveSpan_SmoothScroll(&d->view.scrollY, 0, 0); /* clamp position to new max */ + updateSideOpacity_DocumentView_(&d->view, iFalse); cacheDocumentGlyphs_DocumentWidget_(d); - d->drawBufs->flags |= updateTimestampBuf_DrawBufsFlag | updateSideBuf_DrawBufsFlag; + d->view.drawBufs->flags |= updateTimestampBuf_DrawBufsFlag | updateSideBuf_DrawBufsFlag; d->flags &= ~(urlChanged_DocumentWidgetFlag | drawDownloadCounter_DocumentWidgetFlag); postCommandf_Root( as_Widget(d)->root, "document.changed doc:%p url:%s", d, cstr_String(d->mod.url)); @@ -1959,7 +2001,7 @@ static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) { d, recent->normScrollY, recent->cachedResponse, recent->cachedDoc); if (!recent->cachedDoc) { /* We have a cached copy now. */ - setCachedDocument_History(d->mod.history, d->doc, iFalse); + setCachedDocument_History(d->mod.history, d->view.doc, iFalse); } return iTrue; } @@ -1974,23 +2016,25 @@ static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) { } static void refreshWhileScrolling_DocumentWidget_(iAny *ptr) { + iAssert(isInstance_Object(ptr, &Class_DocumentWidget)); iDocumentWidget *d = ptr; - updateVisible_DocumentWidget_(d); + iDocumentView *view = &d->view; + updateVisible_DocumentView_(view); refresh_Widget(d); - if (d->animWideRunId) { - for (const iGmRun *r = d->animWideRunRange.start; r != d->animWideRunRange.end; r++) { - insert_PtrSet(d->invalidRuns, r); + if (view->animWideRunId) { + for (const iGmRun *r = view->animWideRunRange.start; r != view->animWideRunRange.end; r++) { + insert_PtrSet(view->invalidRuns, r); } } - if (isFinished_Anim(&d->animWideRunOffset)) { - d->animWideRunId = 0; + if (isFinished_Anim(&view->animWideRunOffset)) { + view->animWideRunId = 0; } - if (!isFinished_SmoothScroll(&d->scrollY) || !isFinished_Anim(&d->animWideRunOffset)) { + if (!isFinished_SmoothScroll(&view->scrollY) || !isFinished_Anim(&view->animWideRunOffset)) { addTicker_App(refreshWhileScrolling_DocumentWidget_, d); } - if (isFinished_SmoothScroll(&d->scrollY)) { + if (isFinished_SmoothScroll(&view->scrollY)) { iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iFalse); - updateHover_DocumentWidget_(d, mouseCoord_Window(get_Window(), 0)); + updateHover_DocumentView_(view, mouseCoord_Window(get_Window(), 0)); } } @@ -1999,16 +2043,16 @@ static void scrollBegan_DocumentWidget_(iAnyObject *any, int offset, uint32_t du /* Get rid of link numbers when scrolling. */ if (offset && d->flags & showLinkNumbers_DocumentWidgetFlag) { setLinkNumberMode_DocumentWidget_(d, iFalse); - invalidateVisibleLinks_DocumentWidget_(d); + invalidateVisibleLinks_DocumentView_(&d->view); } /* Show and hide toolbar on scroll. */ if (deviceType_App() == phone_AppDeviceType) { - const float normPos = normScrollPos_DocumentWidget_(d); + const float normPos = normScrollPos_DocumentView_(&d->view); if (prefs_App()->hideToolbarOnScroll && iAbs(offset) > 5 && normPos >= 0) { showToolbar_Root(as_Widget(d)->root, offset < 0); } } - updateVisible_DocumentWidget_(d); + updateVisible_DocumentView_(&d->view); refresh_Widget(as_Widget(d)); if (duration > 0) { iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iTrue); @@ -2016,47 +2060,47 @@ static void scrollBegan_DocumentWidget_(iAnyObject *any, int offset, uint32_t du } } -static void clampScroll_DocumentWidget_(iDocumentWidget *d) { +static void clampScroll_DocumentView_(iDocumentView *d) { move_SmoothScroll(&d->scrollY, 0); } -static void immediateScroll_DocumentWidget_(iDocumentWidget *d, int offset) { +static void immediateScroll_DocumentView_(iDocumentView *d, int offset) { move_SmoothScroll(&d->scrollY, offset); } -static void smoothScroll_DocumentWidget_(iDocumentWidget *d, int offset, int duration) { +static void smoothScroll_DocumentView_(iDocumentView *d, int offset, int duration) { moveSpan_SmoothScroll(&d->scrollY, offset, duration); } -static void scrollTo_DocumentWidget_(iDocumentWidget *d, int documentY, iBool centered) { - if (!isEmpty_Banner(d->banner)) { - documentY += height_Banner(d->banner) + documentTopPad_DocumentWidget_(d); +static void scrollTo_DocumentView_(iDocumentView *d, int documentY, iBool centered) { + if (!isEmpty_Banner(d->owner->banner)) { + documentY += height_Banner(d->owner->banner) + documentTopPad_DocumentView_(d); } else { - documentY += documentTopPad_DocumentWidget_(d) + d->pageMargin * gap_UI; + documentY += documentTopPad_DocumentView_(d) + d->pageMargin * gap_UI; } init_Anim(&d->scrollY.pos, - documentY - (centered ? documentBounds_DocumentWidget_(d).size.y / 2 + documentY - (centered ? documentBounds_DocumentView_(d).size.y / 2 : lineHeight_Text(paragraph_FontId))); - clampScroll_DocumentWidget_(d); + clampScroll_DocumentView_(d); } -static void scrollToHeading_DocumentWidget_(iDocumentWidget *d, const char *heading) { +static void scrollToHeading_DocumentView_(iDocumentView *d, const char *heading) { iConstForEach(Array, h, headings_GmDocument(d->doc)) { const iGmHeading *head = h.value; if (startsWithCase_Rangecc(head->text, heading)) { - postCommandf_Root(as_Widget(d)->root, "document.goto loc:%p", head->text.start); + postCommandf_Root(as_Widget(d->owner)->root, "document.goto loc:%p", head->text.start); break; } } } -static iBool scrollWideBlock_DocumentWidget_(iDocumentWidget *d, iInt2 mousePos, int delta, +static iBool scrollWideBlock_DocumentView_(iDocumentView *d, iInt2 mousePos, int delta, int duration) { - if (delta == 0 || d->flags & eitherWheelSwipe_DocumentWidgetFlag) { + if (delta == 0 || d->owner->flags & eitherWheelSwipe_DocumentWidgetFlag) { return iFalse; } - const iInt2 docPos = documentPos_DocumentWidget_(d, mousePos); + const iInt2 docPos = documentPos_DocumentView_(d, mousePos); iConstForEach(PtrArray, i, &d->visibleWideRuns) { const iGmRun *run = i.ptr; if (docPos.y >= top_Rect(run->bounds) && docPos.y <= bottom_Rect(run->bounds)) { @@ -2066,7 +2110,7 @@ static iBool scrollWideBlock_DocumentWidget_(iDocumentWidget *d, iInt2 mousePos, for (const iGmRun *r = range.start; r != range.end; r++) { maxWidth = iMax(maxWidth, width_Rect(r->visBounds)); } - const int maxOffset = maxWidth - documentWidth_DocumentWidget_(d) + d->pageMargin * gap_UI; + const int maxOffset = maxWidth - documentWidth_DocumentView_(d) + d->pageMargin * gap_UI; if (size_Array(&d->wideRunOffsets) <= preId_GmRun(run)) { resize_Array(&d->wideRunOffsets, preId_GmRun(run) + 1); } @@ -2079,8 +2123,8 @@ static iBool scrollWideBlock_DocumentWidget_(iDocumentWidget *d, iInt2 mousePos, insert_PtrSet(d->invalidRuns, r); } refresh_Widget(d); - d->selectMark = iNullRange; - d->foundMark = iNullRange; + d->owner->selectMark = iNullRange; + d->owner->foundMark = iNullRange; } if (duration) { if (d->animWideRunId != preId_GmRun(run) || isFinished_Anim(&d->animWideRunOffset)) { @@ -2102,13 +2146,13 @@ static iBool scrollWideBlock_DocumentWidget_(iDocumentWidget *d, iInt2 mousePos, } static void togglePreFold_DocumentWidget_(iDocumentWidget *d, uint16_t preId) { - d->hoverPre = NULL; - d->hoverAltPre = NULL; - d->selectMark = iNullRange; - foldPre_GmDocument(d->doc, preId); - redoLayout_GmDocument(d->doc); - clampScroll_DocumentWidget_(d); - updateHover_DocumentWidget_(d, mouseCoord_Window(get_Window(), 0)); + d->view.hoverPre = NULL; + d->view.hoverAltPre = NULL; + d->selectMark = iNullRange; + foldPre_GmDocument(d->view.doc, preId); + redoLayout_GmDocument(d->view.doc); + clampScroll_DocumentView_(&d->view); + updateHover_DocumentView_(&d->view, mouseCoord_Window(get_Window(), 0)); invalidate_DocumentWidget_(d); refresh_Widget(as_Widget(d)); } @@ -2188,8 +2232,8 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) { equalCase_Rangecc(urlScheme_String(d->mod.url), "gemini")) { statusCode = tlsServerCertificateNotVerified_GmStatusCode; } - init_Anim(&d->sideOpacity, 0); - init_Anim(&d->altTextOpacity, 0); + init_Anim(&d->view.sideOpacity, 0); + init_Anim(&d->view.altTextOpacity, 0); format_String(&d->sourceHeader, "%s%s", humanReadableStatusCode_(statusCode), @@ -2283,17 +2327,17 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) { case categorySuccess_GmStatusCode: if (d->flags & urlChanged_DocumentWidgetFlag) { /* Keep scroll position when reloading the same page. */ - reset_SmoothScroll(&d->scrollY); + resetScroll_DocumentView_(&d->view); } - d->scrollY.pullActionTriggered = 0; - pauseAllPlayers_Media(media_GmDocument(d->doc), iTrue); - iRelease(d->doc); /* new content incoming */ - d->doc = new_GmDocument(); + d->view.scrollY.pullActionTriggered = 0; + pauseAllPlayers_Media(media_GmDocument(d->view.doc), iTrue); + iReleasePtr(&d->view.doc); /* new content incoming */ delete_Gempub(d->sourceGempub); d->sourceGempub = NULL; destroy_Widget(d->footerButtons); d->footerButtons = NULL; - resetWideRuns_DocumentWidget_(d); + d->view.doc = new_GmDocument(); + resetWideRuns_DocumentView_(&d->view); updateDocument_DocumentWidget_(d, resp, NULL, iTrue); break; case categoryRedirect_GmStatusCode: @@ -2361,8 +2405,8 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) { unlockResponse_GmRequest(d->request); } -static iRangecc sourceLoc_DocumentWidget_(const iDocumentWidget *d, iInt2 pos) { - return findLoc_GmDocument(d->doc, documentPos_DocumentWidget_(d, pos)); +static iRangecc sourceLoc_DocumentView_(const iDocumentView *d, iInt2 pos) { + return findLoc_GmDocument(d->doc, documentPos_DocumentView_(d, pos)); } iDeclareType(MiddleRunParams) @@ -2385,8 +2429,8 @@ static void find_MiddleRunParams_(void *params, const iGmRun *run) { } } -static const iGmRun *middleRun_DocumentWidget_(const iDocumentWidget *d) { - iRangei visRange = visibleRange_DocumentWidget_(d); +static const iGmRun *middleRun_DocumentView_(const iDocumentView *d) { + iRangei visRange = visibleRange_DocumentView_(d); iMiddleRunParams params = { (visRange.start + visRange.end) / 2, NULL, 0 }; render_GmDocument(d->doc, visRange, find_MiddleRunParams_, ¶ms); return params.closest; @@ -2414,7 +2458,7 @@ static iMediaRequest *findMediaRequest_DocumentWidget_(const iDocumentWidget *d, static iBool requestMedia_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId, iBool enableFilters) { if (!findMediaRequest_DocumentWidget_(d, linkId)) { - const iString *mediaUrl = absoluteUrl_String(d->mod.url, linkUrl_GmDocument(d->doc, linkId)); + const iString *mediaUrl = absoluteUrl_String(d->mod.url, linkUrl_GmDocument(d->view.doc, linkId)); pushBack_ObjectList(d->media, iClob(new_MediaRequest(d, linkId, mediaUrl, enableFilters))); invalidate_DocumentWidget_(d); return iTrue; @@ -2423,7 +2467,7 @@ static iBool requestMedia_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId, } static iBool isDownloadRequest_DocumentWidget(const iDocumentWidget *d, const iMediaRequest *req) { - return findMediaForLink_Media(constMedia_GmDocument(d->doc), req->linkId, download_MediaType).type != 0; + return findMediaForLink_Media(constMedia_GmDocument(d->view.doc), req->linkId, download_MediaType).type != 0; } static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) { @@ -2447,21 +2491,21 @@ static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char * if (isDownloadRequest_DocumentWidget(d, req) || startsWith_String(&resp->meta, "audio/")) { /* TODO: Use a helper? This is same as below except for the partialData flag. */ - if (setData_Media(media_GmDocument(d->doc), + if (setData_Media(media_GmDocument(d->view.doc), req->linkId, &resp->meta, &resp->body, partialData_MediaFlag | allowHide_MediaFlag)) { - redoLayout_GmDocument(d->doc); + redoLayout_GmDocument(d->view.doc); } - updateVisible_DocumentWidget_(d); + updateVisible_DocumentView_(&d->view); invalidate_DocumentWidget_(d); refresh_Widget(as_Widget(d)); } unlockResponse_GmRequest(req->req); } /* Update the link's progress. */ - invalidateLink_DocumentWidget_(d, req->linkId); + invalidateLink_DocumentView_(&d->view, req->linkId); refresh_Widget(d); return iTrue; } @@ -2472,14 +2516,14 @@ static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char * if (isDownloadRequest_DocumentWidget(d, req) || startsWith_String(meta_GmRequest(req->req), "image/") || startsWith_String(meta_GmRequest(req->req), "audio/")) { - setData_Media(media_GmDocument(d->doc), + setData_Media(media_GmDocument(d->view.doc), req->linkId, meta_GmRequest(req->req), body_GmRequest(req->req), allowHide_MediaFlag); - redoLayout_GmDocument(d->doc); - iZap(d->visibleRuns); /* pointers invalidated */ - updateVisible_DocumentWidget_(d); + redoLayout_GmDocument(d->view.doc); + iZap(d->view.visibleRuns); /* pointers invalidated */ + updateVisible_DocumentView_(&d->view); invalidate_DocumentWidget_(d); refresh_Widget(as_Widget(d)); } @@ -2494,8 +2538,8 @@ static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char * return iFalse; } -static void allocVisBuffer_DocumentWidget_(const iDocumentWidget *d) { - const iWidget *w = constAs_Widget(d); +static void allocVisBuffer_DocumentView_(const iDocumentView *d) { + const iWidget *w = constAs_Widget(d->owner); const iBool isVisible = isVisible_Widget(w); const iInt2 size = bounds_Widget(w).size; if (isVisible) { @@ -2507,12 +2551,12 @@ static void allocVisBuffer_DocumentWidget_(const iDocumentWidget *d) { } static iBool fetchNextUnfetchedImage_DocumentWidget_(iDocumentWidget *d) { - iConstForEach(PtrArray, i, &d->visibleLinks) { + iConstForEach(PtrArray, i, &d->view.visibleLinks) { const iGmRun *run = i.ptr; if (run->linkId && run->mediaType == none_MediaType && ~run->flags & decoration_GmRunFlag) { - const int linkFlags = linkFlags_GmDocument(d->doc, run->linkId); - if (isMediaLink_GmDocument(d->doc, run->linkId) && + const int linkFlags = linkFlags_GmDocument(d->view.doc, run->linkId); + if (isMediaLink_GmDocument(d->view.doc, run->linkId) && linkFlags & imageFileExtension_GmLinkFlag && ~linkFlags & content_GmLinkFlag && ~linkFlags & permanent_GmLinkFlag ) { if (requestMedia_DocumentWidget_(d, run->linkId, iTrue)) { @@ -2570,9 +2614,9 @@ static void addAllLinks_(void *context, const iGmRun *run) { } } -static size_t visibleLinkOrdinal_DocumentWidget_(const iDocumentWidget *d, iGmLinkId linkId) { +static size_t visibleLinkOrdinal_DocumentView_(const iDocumentView *d, iGmLinkId linkId) { size_t ord = 0; - const iRangei visRange = visibleRange_DocumentWidget_(d); + const iRangei visRange = visibleRange_DocumentView_(d); iConstForEach(PtrArray, i, &d->visibleLinks) { const iGmRun *run = i.ptr; if (top_Rect(run->visBounds) >= visRange.start + gap_UI * d->pageMargin * 4 / 5) { @@ -2598,38 +2642,36 @@ static const int homeRowKeys_[] = { 't', 'y', }; -static iBool updateDocumentWidthRetainingScrollPosition_DocumentWidget_(iDocumentWidget *d, - iBool keepCenter) { - const int newWidth = documentWidth_DocumentWidget_(d); +static iBool updateDocumentWidthRetainingScrollPosition_DocumentView_(iDocumentView *d, + iBool keepCenter) { + const int newWidth = documentWidth_DocumentView_(d); if (newWidth == size_GmDocument(d->doc).x && !keepCenter /* not a font change */) { return iFalse; } /* Font changes (i.e., zooming) will keep the view centered, otherwise keep the top of the visible area fixed. */ - const iGmRun *run = keepCenter ? middleRun_DocumentWidget_(d) : d->visibleRuns.start; + const iGmRun *run = keepCenter ? middleRun_DocumentView_(d) : d->visibleRuns.start; const char * runLoc = (run ? run->text.start : NULL); int voffset = 0; if (!keepCenter && run) { /* Keep the first visible run visible at the same position. */ /* TODO: First *fully* visible run? */ - voffset = visibleRange_DocumentWidget_(d).start - top_Rect(run->visBounds); + voffset = visibleRange_DocumentView_(d).start - top_Rect(run->visBounds); } - setWidth_GmDocument(d->doc, newWidth, width_Widget(d)); - setWidth_Banner(d->banner, newWidth); - documentRunsInvalidated_DocumentWidget_(d); + setWidth_GmDocument(d->doc, newWidth, width_Widget(d->owner)); + setWidth_Banner(d->owner->banner, newWidth); + documentRunsInvalidated_DocumentWidget_(d->owner); if (runLoc && !keepCenter) { run = findRunAtLoc_GmDocument(d->doc, runLoc); if (run) { - scrollTo_DocumentWidget_(d, - top_Rect(run->visBounds) + - lineHeight_Text(paragraph_FontId) + voffset, - iFalse); + scrollTo_DocumentView_( + d, top_Rect(run->visBounds) + lineHeight_Text(paragraph_FontId) + voffset, iFalse); } } else if (runLoc && keepCenter) { run = findRunAtLoc_GmDocument(d->doc, runLoc); if (run) { - scrollTo_DocumentWidget_(d, mid_Rect(run->bounds).y, iTrue); + scrollTo_DocumentView_(d, mid_Rect(run->bounds).y, iTrue); } } return iTrue; @@ -2662,21 +2704,26 @@ static iBool handlePinch_DocumentWidget_(iDocumentWidget *d, const char *cmd) { return iTrue; } +static void swap_DocumentView_(iDocumentView *d, iDocumentView *swapBuffersWith) { + d->scrollY = swapBuffersWith->scrollY; + d->scrollY.widget = as_Widget(d->owner); + iSwap(iVisBuf *, d->visBuf, swapBuffersWith->visBuf); + iSwap(iVisBufMeta *, d->visBufMeta, swapBuffersWith->visBufMeta); + iSwap(iDrawBufs *, d->drawBufs, swapBuffersWith->drawBufs); + updateVisible_DocumentView_(d); + updateVisible_DocumentView_(swapBuffersWith); +} + static void swap_DocumentWidget_(iDocumentWidget *d, iGmDocument *doc, iDocumentWidget *swapBuffersWith) { if (doc) { iAssert(isInstance_Object(doc, &Class_GmDocument)); replaceDocument_DocumentWidget_(d, doc); - d->scrollY = swapBuffersWith->scrollY; - d->scrollY.widget = as_Widget(d); iSwap(iBanner *, d->banner, swapBuffersWith->banner); setOwner_Banner(d->banner, d); setOwner_Banner(swapBuffersWith->banner, swapBuffersWith); - iSwap(iVisBuf *, d->visBuf, swapBuffersWith->visBuf); - iSwap(iVisBufMeta *, d->visBufMeta, swapBuffersWith->visBufMeta); - iSwap(iDrawBufs *, d->drawBufs, swapBuffersWith->drawBufs); - updateVisible_DocumentWidget_(d); - invalidate_DocumentWidget_(swapBuffersWith); + swap_DocumentView_(&d->view, &swapBuffersWith->view); +// invalidate_DocumentWidget_(swapBuffersWith); } } @@ -2803,7 +2850,7 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { target->widget.rect.pos = windowToInner_Widget(swipeParent, localToWindow_Widget(w, w->rect.pos)); target->widget.rect.size = d->widget.rect.size; setFlags_Widget(as_Widget(target), fixedPosition_WidgetFlag | fixedSize_WidgetFlag, iTrue); - swap_DocumentWidget_(target, d->doc, d); + swap_DocumentWidget_(target, d->view.doc, d); addChildPos_Widget(swipeParent, iClob(target), front_WidgetAddPos); setFlags_Widget(as_Widget(target), refChildrenOffset_WidgetFlag, iTrue); as_Widget(target)->offsetRef = parent_Widget(w); @@ -2837,7 +2884,7 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { /* TODO: Currently not animated! What exactly is the appropriate thing to do here? */ iWidget *swipeParent = swipeParent_DocumentWidget_(d); iDocumentWidget *swipeOut = findChild_Widget(swipeParent, "swipeout"); - swap_DocumentWidget_(d, swipeOut->doc, swipeOut); + swap_DocumentWidget_(d, swipeOut->view.doc, swipeOut); // const int visOff = visualOffsetByReference_Widget(w); w->offsetRef = NULL; // setVisualOffset_Widget(w, visOff, 0, 0); @@ -2871,7 +2918,7 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { addChildPos_Widget(swipeParent, iClob(target), back_WidgetAddPos); setId_Widget(as_Widget(target), "swipeout"); setFlags_Widget(as_Widget(target), disabled_WidgetFlag, iTrue); - swap_DocumentWidget_(target, d->doc, d); + swap_DocumentWidget_(target, d->view.doc, d); setUrlAndSource_DocumentWidget(d, swipeIn->mod.url, collectNewCStr_String("text/gemini"), @@ -2924,23 +2971,23 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) return iFalse; } /* When any tab changes its document URL, update the open link indicators. */ - if (updateOpenURLs_GmDocument(d->doc)) { + if (updateOpenURLs_GmDocument(d->view.doc)) { invalidate_DocumentWidget_(d); refresh_Widget(d); } return iFalse; } if (equal_Command(cmd, "visited.changed")) { - updateVisitedLinks_GmDocument(d->doc); - invalidateVisibleLinks_DocumentWidget_(d); + updateVisitedLinks_GmDocument(d->view.doc); + invalidateVisibleLinks_DocumentView_(&d->view); return iFalse; } if (equal_Command(cmd, "document.render")) /* `Periodic` makes direct dispatch to here */ { // printf("%u: document.render\n", SDL_GetTicks()); - if (SDL_GetTicks() - d->drawBufs->lastRenderTime > 150) { + if (SDL_GetTicks() - d->view.drawBufs->lastRenderTime > 150) { remove_Periodic(periodic_App(), d); /* Scrolling has stopped, begin filling up the buffer. */ - if (d->visBuf->buffers[0].texture) { + if (d->view.visBuf->buffers[0].texture) { addTicker_App(prerender_DocumentWidget_, d); } } @@ -2955,11 +3002,11 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) setLinkNumberMode_DocumentWidget_(d, iFalse); d->phoneToolbar = findWidget_App("toolbar"); const iBool keepCenter = equal_Command(cmd, "font.changed"); - updateDocumentWidthRetainingScrollPosition_DocumentWidget_(d, keepCenter); - d->drawBufs->flags |= updateSideBuf_DrawBufsFlag; - updateVisible_DocumentWidget_(d); + updateDocumentWidthRetainingScrollPosition_DocumentView_(&d->view, keepCenter); + d->view.drawBufs->flags |= updateSideBuf_DrawBufsFlag; + updateVisible_DocumentView_(&d->view); invalidate_DocumentWidget_(d); - dealloc_VisBuf(d->visBuf); + dealloc_VisBuf(d->view.visBuf); updateWindowTitle_DocumentWidget_(d); showOrHidePinningIndicator_DocumentWidget_(d); refresh_Widget(w); @@ -2967,7 +3014,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) else if (equal_Command(cmd, "window.focus.lost")) { if (d->flags & showLinkNumbers_DocumentWidgetFlag) { setLinkNumberMode_DocumentWidget_(d, iFalse); - invalidateVisibleLinks_DocumentWidget_(d); + invalidateVisibleLinks_DocumentView_(&d->view); refresh_Widget(w); } return iFalse; @@ -2979,16 +3026,16 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) invalidateTheme_History(d->mod.history); /* forget cached color palettes */ if (document_App() == d) { updateTheme_DocumentWidget_(d); - updateVisible_DocumentWidget_(d); + updateVisible_DocumentView_(&d->view); updateTrust_DocumentWidget_(d, NULL); - d->drawBufs->flags |= updateSideBuf_DrawBufsFlag; + d->view.drawBufs->flags |= updateSideBuf_DrawBufsFlag; invalidate_DocumentWidget_(d); refresh_Widget(w); } } else if (equal_Command(cmd, "document.layout.changed") && document_Root(get_Root()) == d) { if (argLabel_Command(cmd, "redo")) { - redoLayout_GmDocument(d->doc); + redoLayout_GmDocument(d->view.doc); } updateSize_DocumentWidget(d); } @@ -3011,11 +3058,11 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) updateFetchProgress_DocumentWidget_(d); updateHover_Window(window_Widget(w)); } - init_Anim(&d->sideOpacity, 0); - init_Anim(&d->altTextOpacity, 0); - updateSideOpacity_DocumentWidget_(d, iFalse); + init_Anim(&d->view.sideOpacity, 0); + init_Anim(&d->view.altTextOpacity, 0); + updateSideOpacity_DocumentView_(&d->view, iFalse); updateWindowTitle_DocumentWidget_(d); - allocVisBuffer_DocumentWidget_(d); + allocVisBuffer_DocumentView_(&d->view); animateMedia_DocumentWidget_(d); remove_Periodic(periodic_App(), d); removeTicker_App(prerender_DocumentWidget_, d); @@ -3039,8 +3086,8 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) selectWords_DocumentWidgetFlag; /* finger-based selection is imprecise */ d->flags &= ~selectLines_DocumentWidgetFlag; setFadeEnabled_ScrollWidget(d->scroll, iFalse); - d->selectMark = sourceLoc_DocumentWidget_(d, d->contextPos); - extendRange_Rangecc(&d->selectMark, range_String(source_GmDocument(d->doc)), + d->selectMark = sourceLoc_DocumentView_(&d->view, d->contextPos); + extendRange_Rangecc(&d->selectMark, range_String(source_GmDocument(d->view.doc)), word_RangeExtension | bothStartAndEnd_RangeExtension); d->initialSelectMark = d->selectMark; } @@ -3183,7 +3230,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) } else { /* Full document. */ - copied = copy_String(source_GmDocument(d->doc)); + copied = copy_String(source_GmDocument(d->view.doc)); } SDL_SetClipboardText(cstr_String(copied)); delete_String(copied); @@ -3195,7 +3242,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) else if (equal_Command(cmd, "document.copylink") && document_App() == d) { if (d->contextLink) { SDL_SetClipboardText(cstr_String(canonicalUrl_String(absoluteUrl_String( - d->mod.url, linkUrl_GmDocument(d->doc, d->contextLink->linkId))))); + d->mod.url, linkUrl_GmDocument(d->view.doc, d->contextLink->linkId))))); } else { SDL_SetClipboardText(cstr_String(canonicalUrl_String(d->mod.url))); @@ -3205,13 +3252,13 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) else if (equalWidget_Command(cmd, w, "document.downloadlink")) { if (d->contextLink) { const iGmLinkId linkId = d->contextLink->linkId; - setUrl_Media(media_GmDocument(d->doc), + setUrl_Media(media_GmDocument(d->view.doc), linkId, download_MediaType, - linkUrl_GmDocument(d->doc, linkId)); + linkUrl_GmDocument(d->view.doc, linkId)); requestMedia_DocumentWidget_(d, linkId, iFalse /* no filters */); - redoLayout_GmDocument(d->doc); /* inline downloader becomes visible */ - updateVisible_DocumentWidget_(d); + redoLayout_GmDocument(d->view.doc); /* inline downloader becomes visible */ + updateVisible_DocumentView_(&d->view); invalidate_DocumentWidget_(d); refresh_Widget(w); } @@ -3256,7 +3303,8 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) updateFetchProgress_DocumentWidget_(d); checkResponse_DocumentWidget_(d); if (category_GmStatusCode(status_GmRequest(d->request)) == categorySuccess_GmStatusCode) { - init_Anim(&d->scrollY.pos, d->initNormScrollY * pageHeight_DocumentWidget_(d)); /* TODO: unless user already scrolled! */ + init_Anim(&d->view.scrollY.pos, d->initNormScrollY * pageHeight_DocumentView_(&d->view)); + /* TODO: unless user already scrolled! */ } addBannerWarnings_DocumentWidget_(d); iChangeFlags(d->flags, @@ -3276,8 +3324,8 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) } } iReleasePtr(&d->request); - updateVisible_DocumentWidget_(d); - d->drawBufs->flags |= updateSideBuf_DrawBufsFlag; + updateVisible_DocumentView_(&d->view); + d->view.drawBufs->flags |= updateSideBuf_DrawBufsFlag; postCommandf_Root(w->root, "document.changed doc:%p status:%d url:%s", d, @@ -3285,7 +3333,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) cstr_String(d->mod.url)); /* Check for a pending goto. */ if (!isEmpty_String(&d->pendingGotoHeading)) { - scrollToHeading_DocumentWidget_(d, cstr_String(&d->pendingGotoHeading)); + scrollToHeading_DocumentView_(&d->view, cstr_String(&d->pendingGotoHeading)); clear_String(&d->pendingGotoHeading); } cacheDocumentGlyphs_DocumentWidget_(d); @@ -3331,7 +3379,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) else if (equal_Command(cmd, "media.player.started")) { /* When one media player starts, pause the others that may be playing. */ const iPlayer *startedPlr = pointerLabel_Command(cmd, "player"); - const iMedia * media = media_GmDocument(d->doc); + const iMedia * media = media_GmDocument(d->view.doc); const size_t num = numAudio_Media(media); for (size_t id = 1; id <= num; id++) { iPlayer *plr = audioPlayer_Media(media, (iMediaId){ audio_MediaType, id }); @@ -3374,7 +3422,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) return iTrue; } else if (equal_Command(cmd, "document.reload") && document_Command(cmd) == d) { - d->initNormScrollY = normScrollPos_DocumentWidget_(d); + d->initNormScrollY = normScrollPos_DocumentView_(&d->view); if (equalCase_Rangecc(urlScheme_String(d->mod.url), "titan")) { /* Reopen so the Upload dialog gets shown. */ postCommandf_App("open url:%s", cstr_String(d->mod.url)); @@ -3391,13 +3439,13 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) if (d->flags & showLinkNumbers_DocumentWidgetFlag && d->ordinalMode == homeRow_DocumentLinkOrdinalMode) { const size_t numKeys = iElemCount(homeRowKeys_); - const iGmRun *last = lastVisibleLink_DocumentWidget_(d); + const iGmRun *last = lastVisibleLink_DocumentView_(&d->view); if (!last) { d->ordinalBase = 0; } else { d->ordinalBase += numKeys; - if (visibleLinkOrdinal_DocumentWidget_(d, last->linkId) < d->ordinalBase) { + if (visibleLinkOrdinal_DocumentView_(&d->view, last->linkId) < d->ordinalBase) { d->ordinalBase = 0; } } @@ -3417,25 +3465,11 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) iChangeFlags(d->flags, newTabViaHomeKeys_DocumentWidgetFlag, argLabel_Command(cmd, "newtab") != 0); } - invalidateVisibleLinks_DocumentWidget_(d); + invalidateVisibleLinks_DocumentView_(&d->view); refresh_Widget(d); return iTrue; } else if (equal_Command(cmd, "navigate.back") && document_App() == d) { -#if 0 - if (isPortraitPhone_App()) { - if (d->flags & openedFromSidebar_DocumentWidgetFlag && - !isVisible_Widget(findWidget_App("sidebar"))) { - postCommand_App("sidebar.toggle"); - showToolbar_Root(get_Root(), iTrue); -#if defined (iPlatformAppleMobile) - playHapticEffect_iOS(gentleTap_HapticEffect); -#endif - return iTrue; - } - d->flags &= ~openedFromSidebar_DocumentWidgetFlag; - } -#endif if (d->request) { postCommandf_Root(w->root, "document.request.cancelled doc:%p url:%s", d, cstr_String(d->mod.url)); @@ -3472,8 +3506,8 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) return iTrue; } else if (equalWidget_Command(cmd, w, "scroll.moved")) { - init_Anim(&d->scrollY.pos, arg_Command(cmd)); - updateVisible_DocumentWidget_(d); + init_Anim(&d->view.scrollY.pos, arg_Command(cmd)); + updateVisible_DocumentView_(&d->view); return iTrue; } else if (equal_Command(cmd, "scroll.page") && document_App() == d) { @@ -3484,25 +3518,26 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) return iTrue; } const float amount = argLabel_Command(cmd, "full") != 0 ? 1.0f : 0.5f; - smoothScroll_DocumentWidget_(d, - dir * amount * height_Rect(documentBounds_DocumentWidget_(d)), - smoothDuration_DocumentWidget_(keyboard_ScrollType)); + smoothScroll_DocumentView_(&d->view, + dir * amount * + height_Rect(documentBounds_DocumentView_(&d->view)), + smoothDuration_DocumentWidget_(keyboard_ScrollType)); return iTrue; } else if (equal_Command(cmd, "scroll.top") && document_App() == d) { - init_Anim(&d->scrollY.pos, 0); - invalidate_VisBuf(d->visBuf); - clampScroll_DocumentWidget_(d); - updateVisible_DocumentWidget_(d); + init_Anim(&d->view.scrollY.pos, 0); + invalidate_VisBuf(d->view.visBuf); + clampScroll_DocumentView_(&d->view); + updateVisible_DocumentView_(&d->view); refresh_Widget(w); return iTrue; } else if (equal_Command(cmd, "scroll.bottom") && document_App() == d) { - updateScrollMax_DocumentWidget_(d); /* scrollY.max might not be fully updated */ - init_Anim(&d->scrollY.pos, d->scrollY.max); - invalidate_VisBuf(d->visBuf); - clampScroll_DocumentWidget_(d); - updateVisible_DocumentWidget_(d); + updateScrollMax_DocumentView_(&d->view); /* scrollY.max might not be fully updated */ + init_Anim(&d->view.scrollY.pos, d->view.scrollY.max); + invalidate_VisBuf(d->view.visBuf); + clampScroll_DocumentView_(&d->view); + updateVisible_DocumentView_(&d->view); refresh_Widget(w); return iTrue; } @@ -3513,9 +3548,9 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) fetchNextUnfetchedImage_DocumentWidget_(d)) { return iTrue; } - smoothScroll_DocumentWidget_(d, - 3 * lineHeight_Text(paragraph_FontId) * dir, - smoothDuration_DocumentWidget_(keyboard_ScrollType)); + smoothScroll_DocumentView_(&d->view, + 3 * lineHeight_Text(paragraph_FontId) * dir, + smoothDuration_DocumentWidget_(keyboard_ScrollType)); return iTrue; } else if (equal_Command(cmd, "document.goto") && document_App() == d) { @@ -3526,13 +3561,13 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) setCStr_String(&d->pendingGotoHeading, heading); return iTrue; } - scrollToHeading_DocumentWidget_(d, heading); + scrollToHeading_DocumentView_(&d->view, heading); return iTrue; } const char *loc = pointerLabel_Command(cmd, "loc"); - const iGmRun *run = findRunAtLoc_GmDocument(d->doc, loc); + const iGmRun *run = findRunAtLoc_GmDocument(d->view.doc, loc); if (run) { - scrollTo_DocumentWidget_(d, run->visBounds.pos.y, iFalse); + scrollTo_DocumentView_(&d->view, run->visBounds.pos.y, iFalse); } return iTrue; } @@ -3547,24 +3582,24 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) } else { const iBool wrap = d->foundMark.start != NULL; - d->foundMark = finder(d->doc, text_InputWidget(find), dir > 0 ? d->foundMark.end + d->foundMark = finder(d->view.doc, text_InputWidget(find), dir > 0 ? d->foundMark.end : d->foundMark.start); if (!d->foundMark.start && wrap) { /* Wrap around. */ - d->foundMark = finder(d->doc, text_InputWidget(find), NULL); + d->foundMark = finder(d->view.doc, text_InputWidget(find), NULL); } if (d->foundMark.start) { const iGmRun *found; - if ((found = findRunAtLoc_GmDocument(d->doc, d->foundMark.start)) != NULL) { - scrollTo_DocumentWidget_(d, mid_Rect(found->bounds).y, iTrue); + if ((found = findRunAtLoc_GmDocument(d->view.doc, d->foundMark.start)) != NULL) { + scrollTo_DocumentView_(&d->view, mid_Rect(found->bounds).y, iTrue); } } } if (flags_Widget(w) & touchDrag_WidgetFlag) { postCommand_Root(w->root, "document.select arg:0"); /* we can't handle both at the same time */ } - invalidateWideRunsWithNonzeroOffset_DocumentWidget_(d); /* markers don't support offsets */ - resetWideRuns_DocumentWidget_(d); + invalidateWideRunsWithNonzeroOffset_DocumentView_(&d->view); /* markers don't support offsets */ + resetWideRuns_DocumentView_(&d->view); refresh_Widget(w); return iTrue; } @@ -3577,13 +3612,13 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) } else if (equal_Command(cmd, "bookmark.links") && document_App() == d) { iPtrArray *links = collectNew_PtrArray(); - render_GmDocument(d->doc, (iRangei){ 0, size_GmDocument(d->doc).y }, addAllLinks_, links); + render_GmDocument(d->view.doc, (iRangei){ 0, size_GmDocument(d->view.doc).y }, addAllLinks_, links); /* Find links that aren't already bookmarked. */ iForEach(PtrArray, i, links) { const iGmRun *run = i.ptr; uint32_t bmid; if ((bmid = findUrl_Bookmarks(bookmarks_App(), - linkUrl_GmDocument(d->doc, run->linkId))) != 0) { + linkUrl_GmDocument(d->view.doc, run->linkId))) != 0) { const iBookmark *bm = get_Bookmarks(bookmarks_App(), bmid); /* We can import local copies of remote bookmarks. */ if (~bm->flags & remote_BookmarkFlag) { @@ -3610,7 +3645,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) iConstForEach(PtrArray, j, links) { const iGmRun *run = j.ptr; add_Bookmarks(bookmarks_App(), - linkUrl_GmDocument(d->doc, run->linkId), + linkUrl_GmDocument(d->view.doc, run->linkId), collect_String(newRange_String(run->text)), NULL, 0x1f588 /* pin */); @@ -3625,7 +3660,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) return iTrue; } else if (equalWidget_Command(cmd, w, "menu.closed")) { - updateHover_DocumentWidget_(d, mouseCoord_Window(get_Window(), 0)); + updateHover_DocumentView_(&d->view, mouseCoord_Window(get_Window(), 0)); } else if (equal_Command(cmd, "document.autoreload")) { if (d->mod.reloadInterval) { @@ -3695,14 +3730,14 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) return iFalse; } -static iRect runRect_DocumentWidget_(const iDocumentWidget *d, const iGmRun *run) { - const iRect docBounds = documentBounds_DocumentWidget_(d); - return moved_Rect(run->bounds, addY_I2(topLeft_Rect(docBounds), viewPos_DocumentWidget_(d))); +static iRect runRect_DocumentView_(const iDocumentView *d, const iGmRun *run) { + const iRect docBounds = documentBounds_DocumentView_(d); + return moved_Rect(run->bounds, addY_I2(topLeft_Rect(docBounds), viewPos_DocumentView_(d))); } static void setGrabbedPlayer_DocumentWidget_(iDocumentWidget *d, const iGmRun *run) { if (run && run->mediaType == audio_MediaType) { - iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run)); + iPlayer *plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run)); setFlags_Player(plr, volumeGrabbed_PlayerFlag, iTrue); d->grabbedStartVolume = volume_Player(plr); d->grabbedPlayer = run; @@ -3710,7 +3745,7 @@ static void setGrabbedPlayer_DocumentWidget_(iDocumentWidget *d, const iGmRun *r } else if (d->grabbedPlayer) { setFlags_Player( - audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(d->grabbedPlayer)), + audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(d->grabbedPlayer)), volumeGrabbed_PlayerFlag, iFalse); d->grabbedPlayer = NULL; @@ -3736,14 +3771,14 @@ static iBool processMediaEvents_DocumentWidget_(iDocumentWidget *d, const SDL_Ev return iFalse; } const iInt2 mouse = init_I2(ev->button.x, ev->button.y); - iConstForEach(PtrArray, i, &d->visibleMedia) { + iConstForEach(PtrArray, i, &d->view.visibleMedia) { const iGmRun *run = i.ptr; if (run->mediaType != audio_MediaType) { continue; } /* TODO: move this to mediaui.c */ - const iRect rect = runRect_DocumentWidget_(d, run); - iPlayer * plr = audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run)); + const iRect rect = runRect_DocumentView_(&d->view, run); + iPlayer * plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run)); if (contains_Rect(rect, mouse)) { iPlayerUI ui; init_PlayerUI(&ui, plr, rect); @@ -3869,20 +3904,20 @@ static iChar linkOrdinalChar_DocumentWidget_(const iDocumentWidget *d, size_t or static void beginMarkingSelection_DocumentWidget_(iDocumentWidget *d, iInt2 pos) { setFocus_Widget(NULL); /* TODO: Focus this document? */ - invalidateWideRunsWithNonzeroOffset_DocumentWidget_(d); - resetWideRuns_DocumentWidget_(d); /* Selections don't support horizontal scrolling. */ + invalidateWideRunsWithNonzeroOffset_DocumentView_(&d->view); + resetWideRuns_DocumentView_(&d->view); /* Selections don't support horizontal scrolling. */ iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iTrue); - d->initialSelectMark = d->selectMark = sourceLoc_DocumentWidget_(d, pos); + d->initialSelectMark = d->selectMark = sourceLoc_DocumentView_(&d->view, pos); refresh_Widget(as_Widget(d)); } static void interactingWithLink_DocumentWidget_(iDocumentWidget *d, iGmLinkId id) { - iRangecc loc = linkUrlRange_GmDocument(d->doc, id); + iRangecc loc = linkUrlRange_GmDocument(d->view.doc, id); if (!loc.start) { clear_String(&d->linePrecedingLink); return; } - const char *start = range_String(source_GmDocument(d->doc)).start; + const char *start = range_String(source_GmDocument(d->view.doc)).start; /* Find the preceding line. This is offered as a prefill option for a possible input query. */ while (loc.start > start && *loc.start != '\n') { loc.start--; @@ -3987,11 +4022,12 @@ static iBool handleWheelSwipe_DocumentWidget_(iDocumentWidget *d, const SDL_Mous } static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) { - iWidget *w = as_Widget(d); + iWidget *w = as_Widget(d); + iDocumentView *view = &d->view; if (isMetricsChange_UserEvent(ev)) { updateSize_DocumentWidget(d); } - else if (processEvent_SmoothScroll(&d->scrollY, ev)) { + else if (processEvent_SmoothScroll(&d->view.scrollY, ev)) { return iTrue; } else if (ev->type == SDL_USEREVENT && ev->user.code == command_UserEventCode) { @@ -4010,28 +4046,28 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e if ((d->flags & showLinkNumbers_DocumentWidgetFlag) && ((key >= '1' && key <= '9') || (key >= 'a' && key <= 'z'))) { const size_t ord = linkOrdinalFromKey_DocumentWidget_(d, key) + d->ordinalBase; - iConstForEach(PtrArray, i, &d->visibleLinks) { + iConstForEach(PtrArray, i, &d->view.visibleLinks) { if (ord == iInvalidPos) break; const iGmRun *run = i.ptr; if (run->flags & decoration_GmRunFlag && - visibleLinkOrdinal_DocumentWidget_(d, run->linkId) == ord) { + visibleLinkOrdinal_DocumentView_(view, run->linkId) == ord) { if (d->flags & setHoverViaKeys_DocumentWidgetFlag) { - d->hoverLink = run; + view->hoverLink = run; } else { - postCommandf_Root(w->root, - "open newtab:%d url:%s", - (isPinned_DocumentWidget_(d) ? otherRoot_OpenTabFlag : 0) ^ - (d->ordinalMode == - numbersAndAlphabet_DocumentLinkOrdinalMode - ? openTabMode_Sym(modState_Keys()) - : (d->flags & newTabViaHomeKeys_DocumentWidgetFlag ? 1 : 0)), - cstr_String(absoluteUrl_String( - d->mod.url, linkUrl_GmDocument(d->doc, run->linkId)))); + postCommandf_Root( + w->root, + "open newtab:%d url:%s", + (isPinned_DocumentWidget_(d) ? otherRoot_OpenTabFlag : 0) ^ + (d->ordinalMode == numbersAndAlphabet_DocumentLinkOrdinalMode + ? openTabMode_Sym(modState_Keys()) + : (d->flags & newTabViaHomeKeys_DocumentWidgetFlag ? 1 : 0)), + cstr_String(absoluteUrl_String( + d->mod.url, linkUrl_GmDocument(view->doc, run->linkId)))); interactingWithLink_DocumentWidget_(d, run->linkId); } setLinkNumberMode_DocumentWidget_(d, iFalse); - invalidateVisibleLinks_DocumentWidget_(d); + invalidateVisibleLinks_DocumentView_(view); refresh_Widget(d); return iTrue; } @@ -4041,7 +4077,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e case SDLK_ESCAPE: if (d->flags & showLinkNumbers_DocumentWidgetFlag && document_App() == d) { setLinkNumberMode_DocumentWidget_(d, iFalse); - invalidateVisibleLinks_DocumentWidget_(d); + invalidateVisibleLinks_DocumentView_(view); refresh_Widget(d); return iTrue; } @@ -4053,7 +4089,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e for (size_t i = 0; i < 64; ++i) { setByte_Block(seed, i, iRandom(0, 256)); } - setThemeSeed_GmDocument(d->doc, seed); + setThemeSeed_GmDocument(view->doc, seed); delete_Block(seed); invalidate_DocumentWidget_(d); refresh_Widget(w); @@ -4095,9 +4131,9 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e const iInt2 mouseCoord = coord_MouseWheelEvent(&ev->wheel); if (isPerPixel_MouseWheelEvent(&ev->wheel)) { const iInt2 wheel = init_I2(ev->wheel.x, ev->wheel.y); - stop_Anim(&d->scrollY.pos); - immediateScroll_DocumentWidget_(d, -wheel.y); - if (!scrollWideBlock_DocumentWidget_(d, mouseCoord, -wheel.x, 0) && + stop_Anim(&d->view.scrollY.pos); + immediateScroll_DocumentView_(view, -wheel.y); + if (!scrollWideBlock_DocumentView_(view, mouseCoord, -wheel.x, 0) && wheel.x) { handleWheelSwipe_DocumentWidget_(d, &ev->wheel); } @@ -4109,16 +4145,11 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e postCommandf_App("zoom.delta arg:%d", amount > 0 ? 10 : -10); return iTrue; } - smoothScroll_DocumentWidget_( - d, - -3 * amount * lineHeight_Text(paragraph_FontId), - smoothDuration_DocumentWidget_(mouse_ScrollType)); - /* accelerated speed for repeated wheelings */ -// * (!isFinished_SmoothScroll(&d->scrollY) && pos_Anim(&d->scrollY.pos) < 0.25f -// ? 0.5f -// : 1.0f)); - scrollWideBlock_DocumentWidget_( - d, mouseCoord, -3 * ev->wheel.x * lineHeight_Text(paragraph_FontId), 167); + smoothScroll_DocumentView_(view, + -3 * amount * lineHeight_Text(paragraph_FontId), + smoothDuration_DocumentWidget_(mouse_ScrollType)); + scrollWideBlock_DocumentView_( + view, mouseCoord, -3 * ev->wheel.x * lineHeight_Text(paragraph_FontId), 167); } iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iTrue); return iTrue; @@ -4137,10 +4168,10 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e } #endif else { - if (value_Anim(&d->altTextOpacity) < 0.833f) { - setValue_Anim(&d->altTextOpacity, 0, 0); /* keep it hidden while moving */ + if (value_Anim(&view->altTextOpacity) < 0.833f) { + setValue_Anim(&view->altTextOpacity, 0, 0); /* keep it hidden while moving */ } - updateHover_DocumentWidget_(d, mpos); + updateHover_DocumentView_(view, mpos); } } if (ev->type == SDL_USEREVENT && ev->user.code == widgetTapBegins_UserEventCode) { @@ -4156,18 +4187,18 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e postCommand_Root(w->root, "navigate.forward"); return iTrue; } - if (ev->button.button == SDL_BUTTON_MIDDLE && d->hoverLink) { - interactingWithLink_DocumentWidget_(d, d->hoverLink->linkId); + if (ev->button.button == SDL_BUTTON_MIDDLE && view->hoverLink) { + interactingWithLink_DocumentWidget_(d, view->hoverLink->linkId); postCommandf_Root(w->root, "open newtab:%d url:%s", (isPinned_DocumentWidget_(d) ? otherRoot_OpenTabFlag : 0) | (modState_Keys() & KMOD_SHIFT ? new_OpenTabFlag : newBackground_OpenTabFlag), - cstr_String(linkUrl_GmDocument(d->doc, d->hoverLink->linkId))); + cstr_String(linkUrl_GmDocument(view->doc, view->hoverLink->linkId))); return iTrue; } if (ev->button.button == SDL_BUTTON_RIGHT && contains_Widget(w, init_I2(ev->button.x, ev->button.y))) { if (!isVisible_Widget(d->menu)) { - d->contextLink = d->hoverLink; + d->contextLink = view->hoverLink; d->contextPos = init_I2(ev->button.x, ev->button.y); if (d->menu) { destroy_Widget(d->menu); @@ -4179,7 +4210,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e if (d->contextLink) { /* Context menu for a link. */ interactingWithLink_DocumentWidget_(d, d->contextLink->linkId); /* perhaps will be triggered */ - const iString *linkUrl = linkUrl_GmDocument(d->doc, d->contextLink->linkId); + const iString *linkUrl = linkUrl_GmDocument(view->doc, d->contextLink->linkId); // const int linkFlags = linkFlags_GmDocument(d->doc, d->contextLink->linkId); const iRangecc scheme = urlScheme_String(linkUrl); const iBool isGemini = equalCase_Rangecc(scheme, "gemini"); @@ -4249,7 +4280,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e 2); } iString *linkLabel = collectNewRange_String( - linkLabel_GmDocument(d->doc, d->contextLink->linkId)); + linkLabel_GmDocument(view->doc, d->contextLink->linkId)); urlEncodeSpaces_String(linkLabel); pushBackN_Array(&items, (iMenuItem[]){ { "---" }, @@ -4370,7 +4401,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e /* Enable hover state now that scrolling has surely finished. */ if (d->flags & noHoverWhileScrolling_DocumentWidgetFlag) { d->flags &= ~noHoverWhileScrolling_DocumentWidgetFlag; - updateHover_DocumentWidget_(d, mouseCoord_Window(get_Window(), ev->button.which)); + updateHover_DocumentView_(view, mouseCoord_Window(get_Window(), ev->button.which)); } if (~flags_Widget(w) & touchDrag_WidgetFlag) { iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iFalse); @@ -4381,7 +4412,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e beginMarkingSelection_DocumentWidget_(d, d->click.startPos); extendRange_Rangecc( &d->selectMark, - range_String(source_GmDocument(d->doc)), + range_String(source_GmDocument(view->doc)), bothStartAndEnd_RangeExtension | (d->click.count == 2 ? word_RangeExtension : line_RangeExtension)); d->initialSelectMark = d->selectMark; @@ -4395,24 +4426,24 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e case drag_ClickResult: { if (d->grabbedPlayer) { iPlayer *plr = - audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(d->grabbedPlayer)); + audioPlayer_Media(media_GmDocument(view->doc), mediaId_GmRun(d->grabbedPlayer)); iPlayerUI ui; - init_PlayerUI(&ui, plr, runRect_DocumentWidget_(d, d->grabbedPlayer)); + init_PlayerUI(&ui, plr, runRect_DocumentView_(view, d->grabbedPlayer)); float off = (float) delta_Click(&d->click).x / (float) width_Rect(ui.volumeSlider); setVolume_Player(plr, d->grabbedStartVolume + off); refresh_Widget(w); return iTrue; } /* Fold/unfold a preformatted block. */ - if (~d->flags & selecting_DocumentWidgetFlag && d->hoverPre && - preIsFolded_GmDocument(d->doc, preId_GmRun(d->hoverPre))) { + if (~d->flags & selecting_DocumentWidgetFlag && view->hoverPre && + preIsFolded_GmDocument(view->doc, preId_GmRun(view->hoverPre))) { return iTrue; } /* Begin selecting a range of text. */ if (~d->flags & selecting_DocumentWidgetFlag) { beginMarkingSelection_DocumentWidget_(d, d->click.startPos); } - iRangecc loc = sourceLoc_DocumentWidget_(d, pos_Click(&d->click)); + iRangecc loc = sourceLoc_DocumentView_(view, pos_Click(&d->click)); if (d->selectMark.start == NULL) { d->selectMark = loc; } @@ -4423,7 +4454,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e movingSelectMarkEnd_DocumentWidgetFlag))) { const iRangecc mark = selectMark_DocumentWidget_(d); const char * midMark = mark.start + size_Range(&mark) / 2; - const iRangecc loc = sourceLoc_DocumentWidget_(d, pos_Click(&d->click)); + const iRangecc loc = sourceLoc_DocumentView_(view, pos_Click(&d->click)); const iBool isCloserToStart = d->selectMark.start > d->selectMark.end ? (loc.start > midMark) : (loc.start < midMark); iChangeFlags(d->flags, movingSelectMarkStart_DocumentWidgetFlag, isCloserToStart); @@ -4453,7 +4484,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e if (d->flags & (selectWords_DocumentWidgetFlag | selectLines_DocumentWidgetFlag)) { extendRange_Rangecc( &d->selectMark, - range_String(source_GmDocument(d->doc)), + range_String(source_GmDocument(view->doc)), (d->flags & movingSelectMarkStart_DocumentWidgetFlag ? moveStart_RangeExtension : moveEnd_RangeExtension) | (d->flags & selectWords_DocumentWidgetFlag ? word_RangeExtension @@ -4491,7 +4522,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e setFocus_Widget(NULL); /* Tap in tap selection mode. */ if (flags_Widget(w) & touchDrag_WidgetFlag) { - const iRangecc tapLoc = sourceLoc_DocumentWidget_(d, pos_Click(&d->click)); + const iRangecc tapLoc = sourceLoc_DocumentView_(view, pos_Click(&d->click)); /* Tapping on the selection will show a menu. */ const iRangecc mark = selectMark_DocumentWidget_(d); if (tapLoc.start >= mark.start && tapLoc.end <= mark.end) { @@ -4515,18 +4546,18 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e return iTrue; } } - if (d->hoverPre) { - togglePreFold_DocumentWidget_(d, preId_GmRun(d->hoverPre)); + if (view->hoverPre) { + togglePreFold_DocumentWidget_(d, preId_GmRun(view->hoverPre)); return iTrue; } - if (d->hoverLink) { + if (view->hoverLink) { /* TODO: Move this to a method. */ - const iGmLinkId linkId = d->hoverLink->linkId; - const iMediaId linkMedia = mediaId_GmRun(d->hoverLink); - const int linkFlags = linkFlags_GmDocument(d->doc, linkId); + const iGmLinkId linkId = view->hoverLink->linkId; + const iMediaId linkMedia = mediaId_GmRun(view->hoverLink); + const int linkFlags = linkFlags_GmDocument(view->doc, linkId); iAssert(linkId); /* Media links are opened inline by default. */ - if (isMediaLink_GmDocument(d->doc, linkId)) { + if (isMediaLink_GmDocument(view->doc, linkId)) { if (linkFlags & content_GmLinkFlag && linkFlags & permanent_GmLinkFlag) { /* We have the content and it cannot be dismissed, so nothing further to do. */ @@ -4535,7 +4566,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e if (!requestMedia_DocumentWidget_(d, linkId, iTrue)) { if (linkFlags & content_GmLinkFlag) { /* Dismiss shown content on click. */ - setData_Media(media_GmDocument(d->doc), + setData_Media(media_GmDocument(view->doc), linkId, NULL, NULL, @@ -4549,10 +4580,10 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e be redone. */ } } - redoLayout_GmDocument(d->doc); - d->hoverLink = NULL; - clampScroll_DocumentWidget_(d); - updateVisible_DocumentWidget_(d); + redoLayout_GmDocument(view->doc); + view->hoverLink = NULL; + clampScroll_DocumentView_(view); + updateVisible_DocumentView_(view); invalidate_DocumentWidget_(d); refresh_Widget(w); return iTrue; @@ -4561,13 +4592,13 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e /* Show the existing content again if we have it. */ iMediaRequest *req = findMediaRequest_DocumentWidget_(d, linkId); if (req) { - setData_Media(media_GmDocument(d->doc), + setData_Media(media_GmDocument(view->doc), linkId, meta_GmRequest(req->req), body_GmRequest(req->req), allowHide_MediaFlag); - redoLayout_GmDocument(d->doc); - updateVisible_DocumentWidget_(d); + redoLayout_GmDocument(view->doc); + updateVisible_DocumentView_(view); invalidate_DocumentWidget_(d); refresh_Widget(w); return iTrue; @@ -4591,11 +4622,11 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e postCommandf_Root(w->root, "open newtab:%d url:%s", tabMode, cstr_String(absoluteUrl_String( - d->mod.url, linkUrl_GmDocument(d->doc, linkId)))); + d->mod.url, linkUrl_GmDocument(view->doc, linkId)))); } else { const iString *url = absoluteUrl_String( - d->mod.url, linkUrl_GmDocument(d->doc, linkId)); + d->mod.url, linkUrl_GmDocument(view->doc, linkId)); makeQuestion_Widget( uiTextCaution_ColorEscape "${heading.openlink}", format_CStr( @@ -4633,7 +4664,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e iDeclareType(DrawContext) struct Impl_DrawContext { - const iDocumentWidget *widget; + const iDocumentView *view; iRect widgetBounds; iRect docBounds; iRangei vis; @@ -4677,7 +4708,7 @@ static void fillRange_DrawContext_(iDrawContext *d, const iGmRun *run, enum iCol } if (~run->flags & decoration_GmRunFlag) { const iInt2 visPos = - add_I2(run->bounds.pos, addY_I2(d->viewPos, viewPos_DocumentWidget_(d->widget))); + add_I2(run->bounds.pos, addY_I2(d->viewPos, viewPos_DocumentView_(d->view))); const iRect rangeRect = { addX_I2(visPos, x), init_I2(w, height_Rect(run->bounds)) }; if (rangeRect.size.x) { fillRect_Paint(&d->paint, rangeRect, color); @@ -4692,12 +4723,12 @@ static void fillRange_DrawContext_(iDrawContext *d, const iGmRun *run, enum iCol /* Link URLs are not part of the visible document, so they are ignored above. Handle these ranges as a special case. */ if (run->linkId && run->flags & decoration_GmRunFlag) { - const iRangecc url = linkUrlRange_GmDocument(d->widget->doc, run->linkId); + const iRangecc url = linkUrlRange_GmDocument(d->view->doc, run->linkId); if (contains_Range(&url, mark.start) && (contains_Range(&url, mark.end) || url.end == mark.end)) { fillRect_Paint( &d->paint, - moved_Rect(run->visBounds, addY_I2(d->viewPos, viewPos_DocumentWidget_(d->widget))), + moved_Rect(run->visBounds, addY_I2(d->viewPos, viewPos_DocumentView_(d->view))), color); } } @@ -4706,8 +4737,8 @@ static void fillRange_DrawContext_(iDrawContext *d, const iGmRun *run, enum iCol static void drawMark_DrawContext_(void *context, const iGmRun *run) { iDrawContext *d = context; if (!isMedia_GmRun(run)) { - fillRange_DrawContext_(d, run, uiMatching_ColorId, d->widget->foundMark, &d->inFoundMark); - fillRange_DrawContext_(d, run, uiMarked_ColorId, d->widget->selectMark, &d->inSelectMark); + fillRange_DrawContext_(d, run, uiMatching_ColorId, d->view->owner->foundMark, &d->inFoundMark); + fillRange_DrawContext_(d, run, uiMarked_ColorId, d->view->owner->selectMark, &d->inSelectMark); } } @@ -4723,7 +4754,7 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { } } if (run->mediaType == image_MediaType) { - SDL_Texture *tex = imageTexture_Media(media_GmDocument(d->widget->doc), mediaId_GmRun(run)); + SDL_Texture *tex = imageTexture_Media(media_GmDocument(d->view->doc), mediaId_GmRun(run)); const iRect dst = moved_Rect(run->visBounds, origin); if (tex) { fillRect_Paint(&d->paint, dst, tmBackground_ColorId); /* in case the image has alpha */ @@ -4745,16 +4776,16 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { return; } enum iColorId fg = run->color; - const iGmDocument *doc = d->widget->doc; + const iGmDocument *doc = d->view->doc; const int linkFlags = linkFlags_GmDocument(doc, run->linkId); /* Hover state of a link. */ iBool isHover = - (run->linkId && d->widget->hoverLink && run->linkId == d->widget->hoverLink->linkId && + (run->linkId && d->view->hoverLink && run->linkId == d->view->hoverLink->linkId && ~run->flags & decoration_GmRunFlag); /* Visible (scrolled) position of the run. */ const iInt2 visPos = addX_I2(add_I2(run->visBounds.pos, origin), /* Preformatted runs can be scrolled. */ - runOffset_DocumentWidget_(d->widget, run)); + runOffset_DocumentView_(d->view, run)); const iRect visRect = { visPos, run->visBounds.size }; /* Fill the background. */ { #if 0 @@ -4824,10 +4855,10 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { } else { if (d->showLinkNumbers && run->linkId && run->flags & decoration_GmRunFlag) { - const size_t ord = visibleLinkOrdinal_DocumentWidget_(d->widget, run->linkId); - if (ord >= d->widget->ordinalBase) { + const size_t ord = visibleLinkOrdinal_DocumentView_(d->view, run->linkId); + if (ord >= d->view->owner->ordinalBase) { const iChar ordChar = - linkOrdinalChar_DocumentWidget_(d->widget, ord - d->widget->ordinalBase); + linkOrdinalChar_DocumentWidget_(d->view->owner, ord - d->view->owner->ordinalBase); if (ordChar) { const char *circle = "\u25ef"; /* Large Circle */ const int circleFont = FONT_ID(default_FontId, regular_FontStyle, contentRegular_FontSize); @@ -4901,8 +4932,8 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { break; } if (linkMedia.type != download_MediaType && /* can't cancel downloads currently */ - linkMedia.type != image_MediaType && - findMediaRequest_DocumentWidget_(d->widget, run->linkId)) { + linkMedia.type != image_MediaType && + findMediaRequest_DocumentWidget_(d->view->owner, run->linkId)) { appendFormat_String( &text, " %s" close_Icon, isHover ? escape_Color(tmLinkText_ColorId) : ""); } @@ -4922,7 +4953,7 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { deinit_String(&text); } else if (run->flags & endOfLine_GmRunFlag && - (mr = findMediaRequest_DocumentWidget_(d->widget, run->linkId)) != NULL) { + (mr = findMediaRequest_DocumentWidget_(d->view->owner, run->linkId)) != NULL) { if (!isFinished_GmRequest(mr->req)) { draw_Text(metaFont, topRight_Rect(linkRect), @@ -4931,87 +4962,6 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { (float) bodySize_GmRequest(mr->req) / 1.0e6f); } } -#if 0 - else if (isHover) { - /* TODO: Make this a dynamic overlay, not part of the VisBuf content. */ - const iGmLinkId linkId = d->widget->hoverLink->linkId; - const iString * url = linkUrl_GmDocument(doc, linkId); - const int flags = linkFlags; - iUrl parts; - init_Url(&parts, url); - fg = linkColor_GmDocument(doc, linkId, textHover_GmLinkPart); - const enum iGmLinkScheme scheme = scheme_GmLinkFlag(flags); - const iBool showHost = (flags & humanReadable_GmLinkFlag && - (!isEmpty_Range(&parts.host) || - scheme == mailto_GmLinkScheme)); - const iBool showImage = (flags & imageFileExtension_GmLinkFlag) != 0; - const iBool showAudio = (flags & audioFileExtension_GmLinkFlag) != 0; - iString str; - init_String(&str); - /* Show scheme and host. */ - if (run->flags & endOfLine_GmRunFlag && - (flags & (imageFileExtension_GmLinkFlag | audioFileExtension_GmLinkFlag) || - showHost)) { - format_String( - &str, - "%s%s%s%s%s", - showHost ? "" : "", - showHost - ? (scheme == mailto_GmLinkScheme ? cstr_String(url) - : scheme != gemini_GmLinkScheme ? format_CStr("%s://%s", - cstr_Rangecc(parts.scheme), - cstr_Rangecc(parts.host)) - : cstr_Rangecc(parts.host)) - : "", - showHost && (showImage || showAudio) ? " \u2014" : "", - showImage || showAudio - ? escape_Color(fg) - : escape_Color(linkColor_GmDocument(doc, run->linkId, domain_GmLinkPart)), - showImage || showAudio - ? format_CStr(showImage ? " %s " photo_Icon : " %s \U0001f3b5", - cstr_Lang(showImage ? "link.hint.image" : "link.hint.audio")) - : ""); - } - if (run->flags & endOfLine_GmRunFlag && flags & visited_GmLinkFlag) { - iDate date; - init_Date(&date, linkTime_GmDocument(doc, run->linkId)); - appendCStr_String(&str, " \u2014 "); - appendCStr_String( - &str, escape_Color(linkColor_GmDocument(doc, run->linkId, visited_GmLinkPart))); - iString *dateStr = format_Date(&date, "%b %d"); - append_String(&str, dateStr); - delete_String(dateStr); - } - if (!isEmpty_String(&str)) { - if (run->isRTL) { - appendCStr_String(&str, " \u2014 "); - } - else { - prependCStr_String(&str, " \u2014 "); - } - const iInt2 textSize = measure_Text(metaFont, cstr_String(&str)).bounds.size; - int tx = topRight_Rect(linkRect).x; - const char *msg = cstr_String(&str); - if (run->isRTL) { - tx = topLeft_Rect(linkRect).x - textSize.x; - } - if (tx + textSize.x > right_Rect(d->widgetBounds)) { - tx = right_Rect(d->widgetBounds) - textSize.x; - fillRect_Paint(&d->paint, (iRect){ init_I2(tx, top_Rect(linkRect)), textSize }, - uiBackground_ColorId); - msg += 4; /* skip the space and dash */ - tx += measure_Text(metaFont, " \u2014").advance.x / 2; - } - drawAlign_Text(metaFont, - init_I2(tx, top_Rect(linkRect)), - linkColor_GmDocument(doc, run->linkId, domain_GmLinkPart), - left_Alignment, - "%s", - msg); - deinit_String(&str); - } - } -#endif } if (0) { drawRect_Paint(&d->paint, (iRect){ visPos, run->bounds.size }, green_ColorId); @@ -5030,16 +4980,16 @@ static int drawSideRect_(iPaint *p, iRect rect) { return fg; } -static int sideElementAvailWidth_DocumentWidget_(const iDocumentWidget *d) { - return left_Rect(documentBounds_DocumentWidget_(d)) - - left_Rect(bounds_Widget(constAs_Widget(d))) - 2 * d->pageMargin * gap_UI; +static int sideElementAvailWidth_DocumentView_(const iDocumentView *d) { + return left_Rect(documentBounds_DocumentView_(d)) - + left_Rect(bounds_Widget(constAs_Widget(d->owner))) - 2 * d->pageMargin * gap_UI; } -static iBool isSideHeadingVisible_DocumentWidget_(const iDocumentWidget *d) { - return sideElementAvailWidth_DocumentWidget_(d) >= lineHeight_Text(banner_FontId) * 4.5f; +static iBool isSideHeadingVisible_DocumentView_(const iDocumentView *d) { + return sideElementAvailWidth_DocumentView_(d) >= lineHeight_Text(banner_FontId) * 4.5f; } -static void updateSideIconBuf_DocumentWidget_(const iDocumentWidget *d) { +static void updateSideIconBuf_DocumentView_(const iDocumentView *d) { if (!isExposed_Window(get_Window())) { return; } @@ -5050,20 +5000,20 @@ static void updateSideIconBuf_DocumentWidget_(const iDocumentWidget *d) { dbuf->sideIconBuf = NULL; } // const iGmRun *banner = siteBanner_GmDocument(d->doc); - if (isEmpty_Banner(d->banner)) { + if (isEmpty_Banner(d->owner->banner)) { return; } const int margin = gap_UI * d->pageMargin; const int minBannerSize = lineHeight_Text(banner_FontId) * 2; const iChar icon = siteIcon_GmDocument(d->doc); - const int avail = sideElementAvailWidth_DocumentWidget_(d) - margin; - iBool isHeadingVisible = isSideHeadingVisible_DocumentWidget_(d); + const int avail = sideElementAvailWidth_DocumentView_(d) - margin; + iBool isHeadingVisible = isSideHeadingVisible_DocumentView_(d); /* Determine the required size. */ iInt2 bufSize = init1_I2(minBannerSize); const int sideHeadingFont = FONT_ID(documentHeading_FontId, regular_FontStyle, contentBig_FontSize); if (isHeadingVisible) { const iInt2 headingSize = measureWrapRange_Text(sideHeadingFont, avail, - currentHeading_DocumentWidget_(d)).bounds.size; + currentHeading_DocumentView_(d)).bounds.size; if (headingSize.x > 0) { bufSize.y += gap_Text + headingSize.y; bufSize.x = iMax(bufSize.x, headingSize.x); @@ -5090,7 +5040,7 @@ static void updateSideIconBuf_DocumentWidget_(const iDocumentWidget *d) { drawCentered_Text(banner_FontId, iconRect, iTrue, fg, "%s", cstr_String(&str)); deinit_String(&str); if (isHeadingVisible) { - iRangecc text = currentHeading_DocumentWidget_(d); + iRangecc text = currentHeading_DocumentView_(d); iInt2 pos = addY_I2(bottomLeft_Rect(iconRect), gap_Text); const int font = sideHeadingFont; drawWrapRange_Text(font, pos, avail, tmBannerSideTitle_ColorId, text); @@ -5099,10 +5049,10 @@ static void updateSideIconBuf_DocumentWidget_(const iDocumentWidget *d) { SDL_SetTextureBlendMode(dbuf->sideIconBuf, SDL_BLENDMODE_BLEND); } -static void drawSideElements_DocumentWidget_(const iDocumentWidget *d) { - const iWidget *w = constAs_Widget(d); +static void drawSideElements_DocumentView_(const iDocumentView *d) { + const iWidget *w = constAs_Widget(d->owner); const iRect bounds = bounds_Widget(w); - const iRect docBounds = documentBounds_DocumentWidget_(d); + const iRect docBounds = documentBounds_DocumentView_(d); const int margin = gap_UI * d->pageMargin; float opacity = value_Anim(&d->sideOpacity); const int avail = left_Rect(docBounds) - left_Rect(bounds) - 2 * margin; @@ -5140,20 +5090,20 @@ static void drawSideElements_DocumentWidget_(const iDocumentWidget *d) { unsetClip_Paint(&p); } -static void drawMedia_DocumentWidget_(const iDocumentWidget *d, iPaint *p) { +static void drawMedia_DocumentView_(const iDocumentView *d, iPaint *p) { iConstForEach(PtrArray, i, &d->visibleMedia) { const iGmRun * run = i.ptr; if (run->mediaType == audio_MediaType) { iPlayerUI ui; init_PlayerUI(&ui, audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run)), - runRect_DocumentWidget_(d, run)); + runRect_DocumentView_(d, run)); draw_PlayerUI(&ui, p); } else if (run->mediaType == download_MediaType) { iDownloadUI ui; init_DownloadUI(&ui, constMedia_GmDocument(d->doc), run->mediaId, - runRect_DocumentWidget_(d, run)); + runRect_DocumentView_(d, run)); draw_DownloadUI(&ui, p); } } @@ -5166,20 +5116,23 @@ static void extend_GmRunRange_(iGmRunRange *runs) { } } -static iBool render_DocumentWidget_(const iDocumentWidget *d, iDrawContext *ctx, iBool prerenderExtra) { - iBool didDraw = iFalse; - const iRect bounds = bounds_Widget(constAs_Widget(d)); - const iRect ctxWidgetBounds = init_Rect( - 0, 0, width_Rect(bounds) - constAs_Widget(d->scroll)->rect.size.x, height_Rect(bounds)); - const iRangei full = { 0, size_GmDocument(d->doc).y }; - const iRangei vis = ctx->vis; - iVisBuf *visBuf = d->visBuf; /* will be updated now */ +static iBool render_DocumentView_(const iDocumentView *d, iDrawContext *ctx, iBool prerenderExtra) { + iBool didDraw = iFalse; + const iRect bounds = bounds_Widget(constAs_Widget(d->owner)); + const iRect ctxWidgetBounds = + init_Rect(0, + 0, + width_Rect(bounds) - constAs_Widget(d->owner->scroll)->rect.size.x, + height_Rect(bounds)); + const iRangei full = { 0, size_GmDocument(d->doc).y }; + const iRangei vis = ctx->vis; + iVisBuf *visBuf = d->visBuf; /* will be updated now */ d->drawBufs->lastRenderTime = SDL_GetTicks(); /* Swap buffers around to have room available both before and after the visible region. */ - allocVisBuffer_DocumentWidget_(d); + allocVisBuffer_DocumentView_(d); reposition_VisBuf(visBuf, vis); /* Redraw the invalid ranges. */ - if (~flags_Widget(constAs_Widget(d)) & destroyPending_WidgetFlag) { + if (~flags_Widget(constAs_Widget(d->owner)) & destroyPending_WidgetFlag) { iPaint *p = &ctx->paint; init_Paint(p); iForIndices(i, visBuf->buffers) { @@ -5326,6 +5279,7 @@ static iBool render_DocumentWidget_(const iDocumentWidget *d, iDrawContext *ctx, } static void prerender_DocumentWidget_(iAny *context) { + iAssert(isInstance_Object(context, &Class_DocumentWidget)); if (current_Root() == NULL) { /* The widget has probably been removed from the widget tree, pending destruction. Tickers are not cancelled until the widget is actually destroyed. */ @@ -5333,16 +5287,16 @@ static void prerender_DocumentWidget_(iAny *context) { } const iDocumentWidget *d = context; iDrawContext ctx = { - .widget = d, - .docBounds = documentBounds_DocumentWidget_(d), - .vis = visibleRange_DocumentWidget_(d), + .view = &d->view, + .docBounds = documentBounds_DocumentView_(&d->view), + .vis = visibleRange_DocumentView_(&d->view), .showLinkNumbers = (d->flags & showLinkNumbers_DocumentWidgetFlag) != 0 }; // printf("%u prerendering\n", SDL_GetTicks()); - if (d->visBuf->buffers[0].texture) { - makePaletteGlobal_GmDocument(d->doc); - if (render_DocumentWidget_(d, &ctx, iTrue /* just fill up progressively */)) { - /* Something was drawn, should check if there is still more to do. */ + if (d->view.visBuf->buffers[0].texture) { + makePaletteGlobal_GmDocument(d->view.doc); + if (render_DocumentView_(&d->view, &ctx, iTrue /* just fill up progressively */)) { + /* Something was drawn, should check later if there is still more to do. */ addTicker_App(prerender_DocumentWidget_, context); } } @@ -5358,47 +5312,44 @@ static void checkPendingInvalidation_DocumentWidget_(const iDocumentWidget *d) { } } -static void draw_DocumentWidget_(const iDocumentWidget *d) { - const iWidget *w = constAs_Widget(d); +static void draw_DocumentView_(const iDocumentView *d) { + const iWidget *w = constAs_Widget(d->owner); const iRect bounds = bounds_Widget(w); const iRect boundsWithoutVisOff = boundsWithoutVisualOffset_Widget(w); const iRect clipBounds = intersect_Rect(bounds, boundsWithoutVisOff); - if (width_Rect(bounds) <= 0) { - return; - } - checkPendingInvalidation_DocumentWidget_(d); /* Each document has its own palette, but the drawing routines rely on a global one. As we're now drawing a document, ensure that the right palette is in effect. Document theme colors can be used elsewhere, too, but first a document's palette must be made global. */ makePaletteGlobal_GmDocument(d->doc); if (d->drawBufs->flags & updateTimestampBuf_DrawBufsFlag) { - updateTimestampBuf_DocumentWidget_(d); + updateTimestampBuf_DocumentView_(d); } if (d->drawBufs->flags & updateSideBuf_DrawBufsFlag) { - updateSideIconBuf_DocumentWidget_(d); + updateSideIconBuf_DocumentView_(d); } - const iRect docBounds = documentBounds_DocumentWidget_(d); - const iRangei vis = visibleRange_DocumentWidget_(d); - iDrawContext ctx = { - .widget = d, + const iRect docBounds = documentBounds_DocumentView_(d); + const iRangei vis = visibleRange_DocumentView_(d); + iDrawContext ctx = { + .view = d, .docBounds = docBounds, .vis = vis, - .showLinkNumbers = (d->flags & showLinkNumbers_DocumentWidgetFlag) != 0, + .showLinkNumbers = (d->owner->flags & showLinkNumbers_DocumentWidgetFlag) != 0, }; init_Paint(&ctx.paint); - render_DocumentWidget_(d, &ctx, iFalse /* just the mandatory parts */); - int yTop = docBounds.pos.y + viewPos_DocumentWidget_(d); + render_DocumentView_(d, &ctx, iFalse /* just the mandatory parts */); + iBanner *banner = d->owner->banner; + int yTop = docBounds.pos.y + viewPos_DocumentView_(d); const iBool isDocEmpty = size_GmDocument(d->doc).y == 0; const iBool isTouchSelecting = (flags_Widget(w) & touchDrag_WidgetFlag) != 0; - if (!isDocEmpty || !isEmpty_Banner(d->banner)) { + if (!isDocEmpty || !isEmpty_Banner(banner)) { const int docBgColor = isDocEmpty ? tmBannerBackground_ColorId : tmBackground_ColorId; setClip_Paint(&ctx.paint, clipBounds); if (!isDocEmpty) { draw_VisBuf(d->visBuf, init_I2(bounds.pos.x, yTop), ySpan_Rect(bounds)); } /* Text markers. */ - if (!isEmpty_Range(&d->foundMark) || !isEmpty_Range(&d->selectMark)) { + if (!isEmpty_Range(&d->owner->foundMark) || !isEmpty_Range(&d->owner->selectMark)) { SDL_Renderer *render = renderer_Window(get_Window()); ctx.firstMarkRect = zero_Rect(); ctx.lastMarkRect = zero_Rect(); @@ -5408,14 +5359,14 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) { ctx.viewPos = topLeft_Rect(docBounds); /* Marker starting outside the visible range? */ if (d->visibleRuns.start) { - if (!isEmpty_Range(&d->selectMark) && - d->selectMark.start < d->visibleRuns.start->text.start && - d->selectMark.end > d->visibleRuns.start->text.start) { + if (!isEmpty_Range(&d->owner->selectMark) && + d->owner->selectMark.start < d->visibleRuns.start->text.start && + d->owner->selectMark.end > d->visibleRuns.start->text.start) { ctx.inSelectMark = iTrue; } - if (isEmpty_Range(&d->foundMark) && - d->foundMark.start < d->visibleRuns.start->text.start && - d->foundMark.end > d->visibleRuns.start->text.start) { + if (isEmpty_Range(&d->owner->foundMark) && + d->owner->foundMark.start < d->visibleRuns.start->text.start && + d->owner->foundMark.end > d->visibleRuns.start->text.start) { ctx.inFoundMark = iTrue; } } @@ -5427,26 +5378,26 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) { drawPin_Paint(&ctx.paint, ctx.lastMarkRect, 1, tmQuote_ColorId); } } - drawMedia_DocumentWidget_(d, &ctx.paint); + drawMedia_DocumentView_(d, &ctx.paint); /* Fill the top and bottom, in case the document is short. */ if (yTop > top_Rect(bounds)) { fillRect_Paint(&ctx.paint, (iRect){ bounds.pos, init_I2(bounds.size.x, yTop - top_Rect(bounds)) }, - !isEmpty_Banner(d->banner) ? tmBannerBackground_ColorId - : docBgColor); + !isEmpty_Banner(banner) ? tmBannerBackground_ColorId + : docBgColor); } /* Banner. */ - if (!isDocEmpty || numItems_Banner(d->banner) > 0) { + if (!isDocEmpty || numItems_Banner(banner) > 0) { /* Fill the part between the banner and the top of the document. */ fillRect_Paint(&ctx.paint, (iRect){ init_I2(left_Rect(bounds), - top_Rect(docBounds) + viewPos_DocumentWidget_(d) - - documentTopPad_DocumentWidget_(d)), - init_I2(bounds.size.x, documentTopPad_DocumentWidget_(d)) }, + top_Rect(docBounds) + viewPos_DocumentView_(d) - + documentTopPad_DocumentView_(d)), + init_I2(bounds.size.x, documentTopPad_DocumentView_(d)) }, docBgColor); - setPos_Banner(d->banner, addY_I2(topLeft_Rect(docBounds), - -pos_SmoothScroll(&d->scrollY))); - draw_Banner(d->banner); + setPos_Banner(banner, addY_I2(topLeft_Rect(docBounds), + -pos_SmoothScroll(&d->scrollY))); + draw_Banner(banner); } const int yBottom = yTop + size_GmDocument(d->doc).y; if (yBottom < bottom_Rect(bounds)) { @@ -5455,60 +5406,107 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) { !isDocEmpty ? docBgColor : tmBannerBackground_ColorId); } unsetClip_Paint(&ctx.paint); - drawSideElements_DocumentWidget_(d); - if (deviceType_App() == desktop_AppDeviceType && prefs_App()->hoverLink && d->linkInfo) { - const int pad = gap_UI; - update_LinkInfo(d->linkInfo, - d->doc, - d->hoverLink ? d->hoverLink->linkId : 0, - width_Rect(bounds) - 2 * pad); - const iInt2 infoSize = size_LinkInfo(d->linkInfo); - iInt2 infoPos = add_I2(bottomLeft_Rect(bounds), init_I2(pad, -infoSize.y - pad)); - if (d->hoverLink) { - const iRect runRect = runRect_DocumentWidget_(d, d->hoverLink); - d->linkInfo->isAltPos = - (bottom_Rect(runRect) >= infoPos.y - lineHeight_Text(paragraph_FontId)); - } - if (d->linkInfo->isAltPos) { - infoPos.y = top_Rect(bounds) + pad; + drawSideElements_DocumentView_(d); + /* Alt text. */ + const float altTextOpacity = value_Anim(&d->altTextOpacity) * 6 - 5; + if (d->hoverAltPre && altTextOpacity > 0) { + const iGmPreMeta *meta = preMeta_GmDocument(d->doc, preId_GmRun(d->hoverAltPre)); + if (meta->flags & topLeft_GmPreMetaFlag && ~meta->flags & decoration_GmRunFlag && + !isEmpty_Range(&meta->altText)) { + const int margin = 3 * gap_UI / 2; + const int altFont = uiLabel_FontId; + const int wrap = docBounds.size.x - 2 * margin; + iInt2 pos = addY_I2(add_I2(docBounds.pos, meta->pixelRect.pos), + viewPos_DocumentView_(d)); + const iInt2 textSize = measureWrapRange_Text(altFont, wrap, meta->altText).bounds.size; + pos.y -= textSize.y + gap_UI; + pos.y = iMax(pos.y, top_Rect(bounds)); + const iRect altRect = { pos, init_I2(docBounds.size.x, textSize.y) }; + ctx.paint.alpha = altTextOpacity * 255; + if (altTextOpacity < 1) { + SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND); + } + fillRect_Paint(&ctx.paint, altRect, tmBackgroundAltText_ColorId); + drawRect_Paint(&ctx.paint, altRect, tmFrameAltText_ColorId); + setOpacity_Text(altTextOpacity); + drawWrapRange_Text(altFont, addX_I2(pos, margin), wrap, + tmQuote_ColorId, meta->altText); + SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE); + setOpacity_Text(1.0f); } - draw_LinkInfo(d->linkInfo, infoPos); } -#if 0 - if (prefs_App()->hoverLink && d->hoverLink) { - const int font = uiLabel_FontId; - const iRangecc linkUrl = range_String(linkUrl_GmDocument(d->doc, d->hoverLink->linkId)); - const iInt2 size = measureRange_Text(font, linkUrl).bounds.size; - const iRect linkRect = { addY_I2(bottomLeft_Rect(bounds), -size.y), - addX_I2(size, 2 * gap_UI) }; - fillRect_Paint(&ctx.paint, linkRect, tmBackground_ColorId); - drawRange_Text(font, addX_I2(topLeft_Rect(linkRect), gap_UI), tmParagraph_ColorId, linkUrl); + /* Touch selection indicator. */ + if (isTouchSelecting) { + iRect rect = { topLeft_Rect(bounds), + init_I2(width_Rect(bounds), lineHeight_Text(uiLabelBold_FontId)) }; + fillRect_Paint(&ctx.paint, rect, uiTextAction_ColorId); + const iRangecc mark = selectMark_DocumentWidget_(d->owner); + drawCentered_Text(uiLabelBold_FontId, + rect, + iFalse, + uiBackground_ColorId, + "%zu bytes selected", /* TODO: i18n */ + size_Range(&mark)); } -#endif } +} + +static void draw_DocumentWidget_(const iDocumentWidget *d) { + const iWidget *w = constAs_Widget(d); + const iRect bounds = bounds_Widget(w); + const iRect boundsWithoutVisOff = boundsWithoutVisualOffset_Widget(w); + const iRect clipBounds = intersect_Rect(bounds, boundsWithoutVisOff); + if (width_Rect(bounds) <= 0) { + return; + } + checkPendingInvalidation_DocumentWidget_(d); + draw_DocumentView_(&d->view); + iPaint p; + init_Paint(&p); if (colorTheme_App() == pureWhite_ColorTheme) { - drawHLine_Paint(&ctx.paint, topLeft_Rect(bounds), width_Rect(bounds), uiSeparator_ColorId); + drawHLine_Paint(&p, topLeft_Rect(bounds), width_Rect(bounds), uiSeparator_ColorId); } /* Pull action indicator. */ if (deviceType_App() != desktop_AppDeviceType) { - float pullPos = pullActionPos_SmoothScroll(&d->scrollY); + float pullPos = pullActionPos_SmoothScroll(&d->view.scrollY); /* Account for the part where the indicator isn't yet visible. */ pullPos = (pullPos - 0.2f) / 0.8f; iRect indRect = initCentered_Rect(init_I2(mid_Rect(bounds).x, top_Rect(bounds) - 5 * gap_UI - - pos_SmoothScroll(&d->scrollY)), + pos_SmoothScroll(&d->view.scrollY)), init_I2(20 * gap_UI, 2 * gap_UI)); - setClip_Paint(&ctx.paint, clipBounds); + setClip_Paint(&p, clipBounds); int color = pullPos < 1.0f ? tmBannerItemFrame_ColorId : tmBannerItemText_ColorId; - drawRect_Paint(&ctx.paint, indRect, color); + drawRect_Paint(&p, indRect, color); if (pullPos > 0) { shrink_Rect(&indRect, divi_I2(gap2_UI, 2)); indRect.size.x *= pullPos; - fillRect_Paint(&ctx.paint, indRect, color); + fillRect_Paint(&p, indRect, color); } - unsetClip_Paint(&ctx.paint); + unsetClip_Paint(&p); } + /* Scroll bar. */ drawChildren_Widget(w); + /* Information about the hovered link. */ + if (deviceType_App() == desktop_AppDeviceType && prefs_App()->hoverLink && d->linkInfo) { + const int pad = gap_UI; + update_LinkInfo(d->linkInfo, + d->view.doc, + d->view.hoverLink ? d->view.hoverLink->linkId : 0, + width_Rect(bounds) - 2 * pad); + const iInt2 infoSize = size_LinkInfo(d->linkInfo); + iInt2 infoPos = add_I2(bottomLeft_Rect(bounds), init_I2(pad, -infoSize.y - pad)); + if (d->view.hoverLink) { + const iRect runRect = runRect_DocumentView_(&d->view, d->view.hoverLink); + d->linkInfo->isAltPos = + (bottom_Rect(runRect) >= infoPos.y - lineHeight_Text(paragraph_FontId)); + } + if (d->linkInfo->isAltPos) { + infoPos.y = top_Rect(bounds) + pad; + } + draw_LinkInfo(d->linkInfo, infoPos); + } + /* Full-sized download indicator. */ if (d->flags & drawDownloadCounter_DocumentWidgetFlag && isRequestOngoing_DocumentWidget(d)) { const int font = uiLabelLarge_FontId; const iInt2 sevenSegWidth = measureRange_Text(font, range_CStr("\U0001fbf0")).bounds.size; @@ -5518,62 +5516,26 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) { tmQuote_ColorId, tmQuoteIcon_ColorId, bodySize_GmRequest(d->request)); } - /* Alt text. */ - const float altTextOpacity = value_Anim(&d->altTextOpacity) * 6 - 5; - if (d->hoverAltPre && altTextOpacity > 0) { - const iGmPreMeta *meta = preMeta_GmDocument(d->doc, preId_GmRun(d->hoverAltPre)); - if (meta->flags & topLeft_GmPreMetaFlag && ~meta->flags & decoration_GmRunFlag && - !isEmpty_Range(&meta->altText)) { - const int margin = 3 * gap_UI / 2; - const int altFont = uiLabel_FontId; - const int wrap = docBounds.size.x - 2 * margin; - iInt2 pos = addY_I2(add_I2(docBounds.pos, meta->pixelRect.pos), - viewPos_DocumentWidget_(d)); - const iInt2 textSize = measureWrapRange_Text(altFont, wrap, meta->altText).bounds.size; - pos.y -= textSize.y + gap_UI; - pos.y = iMax(pos.y, top_Rect(bounds)); - const iRect altRect = { pos, init_I2(docBounds.size.x, textSize.y) }; - ctx.paint.alpha = altTextOpacity * 255; - if (altTextOpacity < 1) { - SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND); - } - fillRect_Paint(&ctx.paint, altRect, tmBackgroundAltText_ColorId); - drawRect_Paint(&ctx.paint, altRect, tmFrameAltText_ColorId); - setOpacity_Text(altTextOpacity); - drawWrapRange_Text(altFont, addX_I2(pos, margin), wrap, - tmQuote_ColorId, meta->altText); - SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE); - setOpacity_Text(1.0f); - } - } /* Pinch zoom indicator. */ if (d->flags & pinchZoom_DocumentWidgetFlag) { const int font = uiLabelLargeBold_FontId; const int height = lineHeight_Text(font) * 2; const iInt2 size = init_I2(height * 2, height); const iRect rect = { sub_I2(mid_Rect(bounds), divi_I2(size, 2)), size }; - fillRect_Paint(&ctx.paint, rect, d->pinchZoomPosted == 100 ? uiTextCaution_ColorId : uiTextAction_ColorId); + fillRect_Paint(&p, rect, d->pinchZoomPosted == 100 ? uiTextCaution_ColorId : uiTextAction_ColorId); drawCentered_Text(font, bounds, iFalse, uiBackground_ColorId, "%d %%", d->pinchZoomPosted); } - /* Touch selection indicator. */ - if (isTouchSelecting) { - iRect rect = { topLeft_Rect(bounds), - init_I2(width_Rect(bounds), lineHeight_Text(uiLabelBold_FontId)) }; - fillRect_Paint(&ctx.paint, rect, uiTextAction_ColorId); - const iRangecc mark = selectMark_DocumentWidget_(d); - drawCentered_Text(uiLabelBold_FontId, rect, iFalse, uiBackground_ColorId, "%zu bytes selected", - size_Range(&mark)); - } + /* Dimming during swipe animation. */ if (w->offsetRef) { const int offX = visualOffsetByReference_Widget(w); if (offX) { - setClip_Paint(&ctx.paint, clipBounds); + setClip_Paint(&p, clipBounds); SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND); - ctx.paint.alpha = iAbs(offX) / (float) get_Window()->size.x * 300; - fillRect_Paint(&ctx.paint, bounds, backgroundFadeColor_Widget()); + p.alpha = iAbs(offX) / (float) get_Window()->size.x * 300; + fillRect_Paint(&p, bounds, backgroundFadeColor_Widget()); SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE); - unsetClip_Paint(&ctx.paint); + unsetClip_Paint(&p); } else { /* TODO: Should have a better place to do this; drawing is supposed to be immutable. */ @@ -5582,11 +5544,11 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) { mut->flags &= ~refChildrenOffset_WidgetFlag; } } -// drawRect_Paint(&ctx.paint, docBounds, red_ColorId); +// drawRect_Paint(&p, docBounds, red_ColorId); if (deviceType_App() == phone_AppDeviceType) { /* The phone toolbar uses the palette of the active tab, but there may be other documents drawn before the toolbar, causing the colors to be incorrect. */ - makePaletteGlobal_GmDocument(document_App()->doc); + makePaletteGlobal_GmDocument(document_App()->view.doc); } } @@ -5601,7 +5563,7 @@ const iString *url_DocumentWidget(const iDocumentWidget *d) { } const iGmDocument *document_DocumentWidget(const iDocumentWidget *d) { - return d->doc; + return d->view.doc; } const iBlock *sourceContent_DocumentWidget(const iDocumentWidget *d) { @@ -5609,20 +5571,20 @@ const iBlock *sourceContent_DocumentWidget(const iDocumentWidget *d) { } int documentWidth_DocumentWidget(const iDocumentWidget *d) { - return documentWidth_DocumentWidget_(d); + return documentWidth_DocumentView_(&d->view); } const iString *feedTitle_DocumentWidget(const iDocumentWidget *d) { - if (!isEmpty_String(title_GmDocument(d->doc))) { - return title_GmDocument(d->doc); + if (!isEmpty_String(title_GmDocument(d->view.doc))) { + return title_GmDocument(d->view.doc); } return bookmarkTitle_DocumentWidget(d); } const iString *bookmarkTitle_DocumentWidget(const iDocumentWidget *d) { iStringArray *title = iClob(new_StringArray()); - if (!isEmpty_String(title_GmDocument(d->doc))) { - pushBack_StringArray(title, title_GmDocument(d->doc)); + if (!isEmpty_String(title_GmDocument(d->view.doc))) { + pushBack_StringArray(title, title_GmDocument(d->view.doc)); } if (!isEmpty_String(d->titleUser)) { pushBack_StringArray(title, d->titleUser); @@ -5682,7 +5644,7 @@ void setUrlAndSource_DocumentWidget(iDocumentWidget *d, const iString *url, cons iDocumentWidget *duplicate_DocumentWidget(const iDocumentWidget *orig) { iDocumentWidget *d = new_DocumentWidget(); delete_History(d->mod.history); - d->initNormScrollY = normScrollPos_DocumentWidget_(d); + d->initNormScrollY = normScrollPos_DocumentView_(&d->view); d->mod.history = copy_History(orig->mod.history); setUrlFlags_DocumentWidget(d, orig->mod.url, useCachedContentIfAvailable_DocumentWidgetSetUrlFlag); return d; @@ -5733,11 +5695,12 @@ void takeRequest_DocumentWidget(iDocumentWidget *d, iGmRequest *finishedRequest) } void updateSize_DocumentWidget(iDocumentWidget *d) { - updateDocumentWidthRetainingScrollPosition_DocumentWidget_(d, iFalse); - resetWideRuns_DocumentWidget_(d); - d->drawBufs->flags |= updateSideBuf_DrawBufsFlag; - updateVisible_DocumentWidget_(d); - setWidth_Banner(d->banner, documentWidth_DocumentWidget(d)); + iDocumentView *view = &d->view; + updateDocumentWidthRetainingScrollPosition_DocumentView_(view, iFalse); + resetWideRuns_DocumentView_(view); + view->drawBufs->flags |= updateSideBuf_DrawBufsFlag; + updateVisible_DocumentView_(view); + setWidth_Banner(d->banner, documentWidth_DocumentView_(view)); invalidate_DocumentWidget_(d); arrange_Widget(d->footerButtons); } -- cgit v1.2.3 From b55e07bcc11237570a69695dbc617c3088b9306b Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 19 Dec 2021 15:18:59 +0200 Subject: Cleanup: Group together DocumentView methods --- src/ui/documentwidget.c | 8538 ++++++++++++++++++++++++----------------------- 1 file changed, 4271 insertions(+), 4267 deletions(-) diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index f5b9a4fc..4af3dd72 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -199,16 +199,6 @@ static void visBufInvalidated_(iVisBuf *d, size_t index) { /*----------------------------------------------------------------------------------------------*/ -static void animate_DocumentWidget_ (void *ticker); -static void animateMedia_DocumentWidget_ (iDocumentWidget *d); -static void updateSideIconBuf_DocumentWidget_ (const iDocumentWidget *d); -static void prerender_DocumentWidget_ (iAny *); -static void scrollBegan_DocumentWidget_ (iAnyObject *, int, uint32_t); - -static const int smoothDuration_DocumentWidget_(enum iScrollType type) { - return 600 /* milliseconds */ * scrollSpeedFactor_Prefs(prefs_App(), type); -} - enum iRequestState { blank_RequestState, fetching_RequestState, @@ -343,9 +333,148 @@ struct Impl_DocumentWidget { }; iDefineObjectConstruction(DocumentWidget) - + +/* Sorted by proximity to F and J. */ +static const int homeRowKeys_[] = { + 'f', 'd', 's', 'a', + 'j', 'k', 'l', + 'r', 'e', 'w', 'q', + 'u', 'i', 'o', 'p', + 'v', 'c', 'x', 'z', + 'm', 'n', + 'g', 'h', + 'b', + 't', 'y', +}; static int docEnum_ = 0; +static void animate_DocumentWidget_ (void *ticker); +static void animateMedia_DocumentWidget_ (iDocumentWidget *d); +static void updateSideIconBuf_DocumentWidget_ (const iDocumentWidget *d); +static void prerender_DocumentWidget_ (iAny *); +static void scrollBegan_DocumentWidget_ (iAnyObject *, int, uint32_t); +static void refreshWhileScrolling_DocumentWidget_ (iAny *); + +/* TODO: The following methods are called from DocumentView, which goes the wrong way. */ + +static iRangecc selectMark_DocumentWidget_(const iDocumentWidget *d) { + /* Normalize so start < end. */ + iRangecc norm = d->selectMark; + if (norm.start > norm.end) { + iSwap(const char *, norm.start, norm.end); + } + return norm; +} + +static int phoneToolbarHeight_DocumentWidget_(const iDocumentWidget *d) { + if (!d->phoneToolbar) { + return 0; + } + const iWidget *w = constAs_Widget(d); + return bottom_Rect(rect_Root(w->root)) - top_Rect(boundsWithoutVisualOffset_Widget(d->phoneToolbar)); +} + +static int footerHeight_DocumentWidget_(const iDocumentWidget *d) { + int hgt = height_Widget(d->footerButtons); + if (isPortraitPhone_App()) { + hgt += phoneToolbarHeight_DocumentWidget_(d); + } + return hgt; +} + +static iBool isHoverAllowed_DocumentWidget_(const iDocumentWidget *d) { + if (!isHover_Widget(d)) { + return iFalse; + } + if (!(d->state == ready_RequestState || d->state == receivedPartialResponse_RequestState)) { + return iFalse; + } + if (d->flags & noHoverWhileScrolling_DocumentWidgetFlag) { + return iFalse; + } + if (d->flags & pinchZoom_DocumentWidgetFlag) { + return iFalse; + } + if (flags_Widget(constAs_Widget(d)) & touchDrag_WidgetFlag) { + return iFalse; + } + if (flags_Widget(constAs_Widget(d->scroll)) & pressed_WidgetFlag) { + return iFalse; + } + return iTrue; +} + +static iMediaRequest *findMediaRequest_DocumentWidget_(const iDocumentWidget *d, iGmLinkId linkId) { + iConstForEach(ObjectList, i, d->media) { + const iMediaRequest *req = (const iMediaRequest *) i.object; + if (req->linkId == linkId) { + return iConstCast(iMediaRequest *, req); + } + } + return NULL; +} + +static size_t linkOrdinalFromKey_DocumentWidget_(const iDocumentWidget *d, int key) { + size_t ord = iInvalidPos; + if (d->ordinalMode == numbersAndAlphabet_DocumentLinkOrdinalMode) { + if (key >= '1' && key <= '9') { + return key - '1'; + } + if (key < 'a' || key > 'z') { + return iInvalidPos; + } + ord = key - 'a' + 9; +#if defined (iPlatformApple) + /* Skip keys that would conflict with default system shortcuts: hide, minimize, quit, close. */ + if (key == 'h' || key == 'm' || key == 'q' || key == 'w') { + return iInvalidPos; + } + if (key > 'h') ord--; + if (key > 'm') ord--; + if (key > 'q') ord--; + if (key > 'w') ord--; +#endif + } + else { + iForIndices(i, homeRowKeys_) { + if (homeRowKeys_[i] == key) { + return i; + } + } + } + return ord; +} + +static iChar linkOrdinalChar_DocumentWidget_(const iDocumentWidget *d, size_t ord) { + if (d->ordinalMode == numbersAndAlphabet_DocumentLinkOrdinalMode) { + if (ord < 9) { + return '1' + ord; + } +#if defined (iPlatformApple) + if (ord < 9 + 22) { + int key = 'a' + ord - 9; + if (key >= 'h') key++; + if (key >= 'm') key++; + if (key >= 'q') key++; + if (key >= 'w') key++; + return 'A' + key - 'a'; + } +#else + if (ord < 9 + 26) { + return 'A' + ord - 9; + } +#endif + } + else { + if (ord < iElemCount(homeRowKeys_)) { + return 'A' + homeRowKeys_[ord] - 'a'; + } + } + return 0; +} + +/*----------------------------------------------------------------------------------------------*/ + void init_DocumentView(iDocumentView *d) { d->owner = NULL; d->doc = new_GmDocument(); @@ -379,91 +508,6 @@ void init_DocumentView(iDocumentView *d) { init_PtrArray(&d->visibleMedia); } -static void setOwner_DocumentView_(iDocumentView *d, iDocumentWidget *doc) { - d->owner = doc; - init_SmoothScroll(&d->scrollY, as_Widget(doc), scrollBegan_DocumentWidget_); -} - -void init_DocumentWidget(iDocumentWidget *d) { - iWidget *w = as_Widget(d); - init_Widget(w); - setId_Widget(w, format_CStr("document%03d", ++docEnum_)); - setFlags_Widget(w, hover_WidgetFlag | noBackground_WidgetFlag, iTrue); -#if defined (iPlatformAppleDesktop) - iBool enableSwipeNavigation = iTrue; /* swipes on the trackpad */ -#else - iBool enableSwipeNavigation = (deviceType_App() != desktop_AppDeviceType); -#endif - if (enableSwipeNavigation) { - setFlags_Widget(w, leftEdgeDraggable_WidgetFlag | rightEdgeDraggable_WidgetFlag | - horizontalOffset_WidgetFlag, iTrue); - } - init_PersistentDocumentState(&d->mod); - d->flags = 0; - d->phoneToolbar = findWidget_App("toolbar"); - d->footerButtons = NULL; - iZap(d->certExpiry); - d->certFingerprint = new_Block(0); - d->certFlags = 0; - d->certSubject = new_String(); - d->state = blank_RequestState; - d->titleUser = new_String(); - d->request = NULL; - d->isRequestUpdated = iFalse; - d->media = new_ObjectList(); - d->banner = new_Banner(); - setOwner_Banner(d->banner, d); - d->redirectCount = 0; - d->ordinalBase = 0; - d->wheelSwipeState = none_WheelSwipeState; - d->selectMark = iNullRange; - d->foundMark = iNullRange; - d->contextLink = NULL; - d->sourceStatus = none_GmStatusCode; - init_String(&d->sourceHeader); - init_String(&d->sourceMime); - init_Block(&d->sourceContent, 0); - iZap(d->sourceTime); - d->sourceGempub = NULL; - d->initNormScrollY = 0; - d->grabbedPlayer = NULL; - d->mediaTimer = 0; - init_String(&d->pendingGotoHeading); - init_String(&d->linePrecedingLink); - init_Click(&d->click, d, SDL_BUTTON_LEFT); - d->linkInfo = (deviceType_App() == desktop_AppDeviceType ? new_LinkInfo() : NULL); - init_DocumentView(&d->view); - setOwner_DocumentView_(&d->view, d); - addChild_Widget(w, iClob(d->scroll = new_ScrollWidget())); - d->menu = NULL; /* created when clicking */ - d->playerMenu = NULL; - d->copyMenu = NULL; - d->translation = NULL; - addChildFlags_Widget(w, - iClob(new_IndicatorWidget()), - resizeToParentWidth_WidgetFlag | resizeToParentHeight_WidgetFlag); -#if !defined (iPlatformAppleDesktop) /* in system menu */ - addAction_Widget(w, reload_KeyShortcut, "navigate.reload"); - addAction_Widget(w, closeTab_KeyShortcut, "tabs.close"); - addAction_Widget(w, SDLK_d, KMOD_PRIMARY, "bookmark.add"); - addAction_Widget(w, subscribeToPage_KeyModifier, "feeds.subscribe"); -#endif - addAction_Widget(w, navigateBack_KeyShortcut, "navigate.back"); - addAction_Widget(w, navigateForward_KeyShortcut, "navigate.forward"); - addAction_Widget(w, navigateParent_KeyShortcut, "navigate.parent"); - addAction_Widget(w, navigateRoot_KeyShortcut, "navigate.root"); -} - -void cancelAllRequests_DocumentWidget(iDocumentWidget *d) { - iForEach(ObjectList, i, d->media) { - iMediaRequest *mr = i.object; - cancel_GmRequest(mr->req); - } - if (d->request) { - cancel_GmRequest(d->request); -} - } - void deinit_DocumentView(iDocumentView *d) { delete_DrawBufs(d->drawBufs); delete_VisBuf(d->visBuf); @@ -477,60 +521,9 @@ void deinit_DocumentView(iDocumentView *d) { iReleasePtr(&d->doc); } -void deinit_DocumentWidget(iDocumentWidget *d) { -// printf("\n* * * * * * * *\nDEINIT DOCUMENT: %s\n* * * * * * * *\n\n", -// cstr_String(&d->widget.id)); fflush(stdout); - cancelAllRequests_DocumentWidget(d); - pauseAllPlayers_Media(media_GmDocument(d->view.doc), iTrue); - removeTicker_App(animate_DocumentWidget_, d); - removeTicker_App(prerender_DocumentWidget_, d); - remove_Periodic(periodic_App(), d); - delete_Translation(d->translation); - deinit_DocumentView(&d->view); - delete_LinkInfo(d->linkInfo); - iRelease(d->media); - iRelease(d->request); - delete_Gempub(d->sourceGempub); - deinit_String(&d->linePrecedingLink); - deinit_String(&d->pendingGotoHeading); - deinit_Block(&d->sourceContent); - deinit_String(&d->sourceMime); - deinit_String(&d->sourceHeader); - delete_Banner(d->banner); - if (d->mediaTimer) { - SDL_RemoveTimer(d->mediaTimer); - } - delete_Block(d->certFingerprint); - delete_String(d->certSubject); - delete_String(d->titleUser); - deinit_PersistentDocumentState(&d->mod); -} - -static iRangecc selectMark_DocumentWidget_(const iDocumentWidget *d) { - /* Normalize so start < end. */ - iRangecc norm = d->selectMark; - if (norm.start > norm.end) { - iSwap(const char *, norm.start, norm.end); - } - return norm; -} - -static void enableActions_DocumentWidget_(iDocumentWidget *d, iBool enable) { - /* Actions are invisible child widgets of the DocumentWidget. */ - iForEach(ObjectList, i, children_Widget(d)) { - if (isAction_Widget(i.object)) { - setFlags_Widget(i.object, disabled_WidgetFlag, !enable); - } - } -} - -static void setLinkNumberMode_DocumentWidget_(iDocumentWidget *d, iBool set) { - iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, set); - /* Children have priority when handling events. */ - enableActions_DocumentWidget_(d, !set); - if (d->menu) { - setFlags_Widget(d->menu, disabled_WidgetFlag, set); - } +static void setOwner_DocumentView_(iDocumentView *d, iDocumentWidget *doc) { + d->owner = doc; + init_SmoothScroll(&d->scrollY, as_Widget(doc), scrollBegan_DocumentWidget_); } static void resetWideRuns_DocumentView_(iDocumentView *d) { @@ -540,38 +533,17 @@ static void resetWideRuns_DocumentView_(iDocumentView *d) { iZap(d->animWideRunRange); } -static void requestUpdated_DocumentWidget_(iAnyObject *obj) { - iDocumentWidget *d = obj; - const int wasUpdated = exchange_Atomic(&d->isRequestUpdated, iTrue); - if (!wasUpdated) { - postCommand_Widget(obj, - "document.request.updated doc:%p reqid:%u request:%p", - d, - id_GmRequest(d->request), - d->request); - } -} - -static void requestFinished_DocumentWidget_(iAnyObject *obj) { - iDocumentWidget *d = obj; - postCommand_Widget(obj, - "document.request.finished doc:%p reqid:%u request:%p", - d, - id_GmRequest(d->request), - d->request); -} - static int documentWidth_DocumentView_(const iDocumentView *d) { const iWidget *w = constAs_Widget(d->owner); const iRect bounds = bounds_Widget(w); const iPrefs * prefs = prefs_App(); const int minWidth = 50 * gap_UI; /* lines must fit a word at least */ const float adjust = iClamp((float) bounds.size.x / gap_UI / 11 - 12, - -1.0f, 10.0f); /* adapt to width */ + -1.0f, 10.0f); /* adapt to width */ //printf("%f\n", adjust); fflush(stdout); return iMini(iMax(minWidth, bounds.size.x - gap_UI * (d->pageMargin + adjust) * 2), fontSize_UI * //emRatio_Text(paragraph_FontId) * /* dependent on avg. glyph width */ - prefs->lineWidth * prefs->zoomPercent / 100); + prefs->lineWidth * prefs->zoomPercent / 100); } static int documentTopPad_DocumentView_(const iDocumentView *d) { @@ -588,22 +560,6 @@ static int pageHeight_DocumentView_(const iDocumentView *d) { return height_Banner(d->owner->banner) + documentTopPad_DocumentView_(d) + size_GmDocument(d->doc).y; } -static int phoneToolbarHeight_DocumentWidget_(const iDocumentWidget *d) { - if (!d->phoneToolbar) { - return 0; - } - const iWidget *w = constAs_Widget(d); - return bottom_Rect(rect_Root(w->root)) - top_Rect(boundsWithoutVisualOffset_Widget(d->phoneToolbar)); -} - -static int footerHeight_DocumentWidget_(const iDocumentWidget *d) { - int hgt = height_Widget(d->footerButtons); - if (isPortraitPhone_App()) { - hgt += phoneToolbarHeight_DocumentWidget_(d); - } - return hgt; -} - static iRect documentBounds_DocumentView_(const iDocumentView *d) { const iRect bounds = bounds_Widget(constAs_Widget(d->owner)); const int margin = gap_UI * d->pageMargin; @@ -761,38 +717,6 @@ static void invalidateWideRunsWithNonzeroOffset_DocumentView_(iDocumentView *d) } } -static void animate_DocumentWidget_(void *ticker) { - iDocumentWidget *d = ticker; - iAssert(isInstance_Object(d, &Class_DocumentWidget)); - refresh_Widget(d); - if (!isFinished_Anim(&d->view.sideOpacity) || !isFinished_Anim(&d->view.altTextOpacity) || - (d->linkInfo && !isFinished_Anim(&d->linkInfo->opacity))) { - addTicker_App(animate_DocumentWidget_, d); - } -} - -static iBool isHoverAllowed_DocumentWidget_(const iDocumentWidget *d) { - if (!isHover_Widget(d)) { - return iFalse; - } - if (!(d->state == ready_RequestState || d->state == receivedPartialResponse_RequestState)) { - return iFalse; - } - if (d->flags & noHoverWhileScrolling_DocumentWidgetFlag) { - return iFalse; - } - if (d->flags & pinchZoom_DocumentWidgetFlag) { - return iFalse; - } - if (flags_Widget(constAs_Widget(d)) & touchDrag_WidgetFlag) { - return iFalse; - } - if (flags_Widget(constAs_Widget(d->scroll)) & pressed_WidgetFlag) { - return iFalse; - } - return iTrue; -} - static void updateHover_DocumentView_(iDocumentView *d, iInt2 mouse) { const iWidget *w = constAs_Widget(d->owner); const iRect docBounds = documentBounds_DocumentView_(d); @@ -872,72 +796,6 @@ static void updateSideOpacity_DocumentView_(iDocumentView *d, iBool isAnimated) animate_DocumentWidget_(d->owner); } -static uint32_t mediaUpdateInterval_DocumentWidget_(const iDocumentWidget *d) { - if (document_App() != d) { - return 0; - } - if (as_MainWindow(window_Widget(d))->isDrawFrozen) { - return 0; - } - static const uint32_t invalidInterval_ = ~0u; - uint32_t interval = invalidInterval_; - iConstForEach(PtrArray, i, &d->view.visibleMedia) { - const iGmRun *run = i.ptr; - if (run->mediaType == audio_MediaType) { - iPlayer *plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run)); - if (flags_Player(plr) & adjustingVolume_PlayerFlag || - (isStarted_Player(plr) && !isPaused_Player(plr))) { - interval = iMin(interval, 1000 / 15); - } - } - else if (run->mediaType == download_MediaType) { - interval = iMin(interval, 1000); - } - } - return interval != invalidInterval_ ? interval : 0; -} - -static uint32_t postMediaUpdate_DocumentWidget_(uint32_t interval, void *context) { - /* Called in timer thread; don't access the widget. */ - iUnused(context); - postCommand_App("media.player.update"); - return interval; -} - -static void updateMedia_DocumentWidget_(iDocumentWidget *d) { - if (document_App() == d) { - refresh_Widget(d); - iConstForEach(PtrArray, i, &d->view.visibleMedia) { - const iGmRun *run = i.ptr; - if (run->mediaType == audio_MediaType) { - iPlayer *plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run)); - if (idleTimeMs_Player(plr) > 3000 && ~flags_Player(plr) & volumeGrabbed_PlayerFlag && - flags_Player(plr) & adjustingVolume_PlayerFlag) { - setFlags_Player(plr, adjustingVolume_PlayerFlag, iFalse); - } - } - } - } - if (d->mediaTimer && mediaUpdateInterval_DocumentWidget_(d) == 0) { - SDL_RemoveTimer(d->mediaTimer); - d->mediaTimer = 0; - } -} - -static void animateMedia_DocumentWidget_(iDocumentWidget *d) { - if (document_App() != d) { - if (d->mediaTimer) { - SDL_RemoveTimer(d->mediaTimer); - d->mediaTimer = 0; - } - return; - } - uint32_t interval = mediaUpdateInterval_DocumentWidget_(d); - if (interval && !d->mediaTimer) { - d->mediaTimer = SDL_AddTimer(interval, postMediaUpdate_DocumentWidget_, d); - } -} - static iRangecc currentHeading_DocumentView_(const iDocumentView *d) { iRangecc heading = iNullRange; if (d->visibleRuns.start) { @@ -971,7 +829,7 @@ static void updateVisible_DocumentView_(iDocumentView *d) { !isSuccess_GmStatusCode(d->owner->sourceStatus)); iScrollWidget *scrollBar = d->owner->scroll; const iRangei visRange = visibleRange_DocumentView_(d); -// printf("visRange: %d...%d\n", visRange.start, visRange.end); + // printf("visRange: %d...%d\n", visRange.start, visRange.end); const iRect bounds = bounds_Widget(as_Widget(d->owner)); const int scrollMax = updateScrollMax_DocumentView_(d); /* Reposition the footer buttons as appropriate. */ @@ -1021,84 +879,14 @@ static void updateVisible_DocumentView_(iDocumentView *d) { } } -static void updateWindowTitle_DocumentWidget_(const iDocumentWidget *d) { - iLabelWidget *tabButton = tabPageButton_Widget(findChild_Widget(root_Widget(constAs_Widget(d)), - "doctabs"), d); - if (!tabButton) { - /* Not part of the UI at the moment. */ - return; - } - iStringArray *title = iClob(new_StringArray()); - if (!isEmpty_String(title_GmDocument(d->view.doc))) { - pushBack_StringArray(title, title_GmDocument(d->view.doc)); - } - if (!isEmpty_String(d->titleUser)) { - pushBack_StringArray(title, d->titleUser); - } - else { - iUrl parts; - init_Url(&parts, d->mod.url); - if (equalCase_Rangecc(parts.scheme, "about")) { - if (!findWidget_App("winbar")) { - pushBackCStr_StringArray(title, "Lagrange"); - } - } - else if (!isEmpty_Range(&parts.host)) { - pushBackRange_StringArray(title, parts.host); - } - } - if (isEmpty_StringArray(title)) { - pushBackCStr_StringArray(title, "Lagrange"); - } - /* Take away parts if it doesn't fit. */ - const int avail = bounds_Widget(as_Widget(tabButton)).size.x - 7 * gap_UI; - iBool setWindow = (document_App() == d && isUnderKeyRoot_Widget(d)); - const int font = uiLabel_FontId; - for (;;) { - iString *text = collect_String(joinCStr_StringArray(title, " \u2014 ")); - if (setWindow) { - /* Longest version for the window title, and omit the icon. */ - setTitle_MainWindow(get_MainWindow(), text); - setWindow = iFalse; - } - const iChar siteIcon = siteIcon_GmDocument(d->view.doc); - if (siteIcon) { - if (!isEmpty_String(text)) { - prependCStr_String(text, " " restore_ColorEscape); - } - prependChar_String(text, siteIcon); - prependCStr_String(text, escape_Color(uiIcon_ColorId)); - } - const int width = measureRange_Text(font, range_String(text)).advance.x; - const int ellipsisWidth = measure_Text(font, "...").advance.x; - setTextColor_LabelWidget(tabButton, none_ColorId); - iWidget *tabCloseButton = child_Widget(as_Widget(tabButton), 0); - setFlags_Widget(tabCloseButton, visibleOnParentHover_WidgetFlag, - avail > width_Widget(tabCloseButton)); - if (width <= avail || isEmpty_StringArray(title)) { - updateText_LabelWidget(tabButton, text); - break; - } - if (size_StringArray(title) == 1) { - /* Just truncate to fit. */ - if (siteIcon && avail <= 4 * ellipsisWidth) { - updateText_LabelWidget(tabButton, collect_String(newUnicodeN_String(&siteIcon, 1))); - setTextColor_LabelWidget(tabButton, uiIcon_ColorId); - break; - } - const char *endPos; - tryAdvanceNoWrap_Text(font, - range_String(text), - avail - ellipsisWidth, - &endPos); - updateText_LabelWidget( - tabButton, - collectNewFormat_String( - "%s...", cstr_Rangecc((iRangecc){ constBegin_String(text), endPos }))); - break; - } - remove_StringArray(title, size_StringArray(title) - 1); - } +static void swap_DocumentView_(iDocumentView *d, iDocumentView *swapBuffersWith) { + d->scrollY = swapBuffersWith->scrollY; + d->scrollY.widget = as_Widget(d->owner); + iSwap(iVisBuf *, d->visBuf, swapBuffersWith->visBuf); + iSwap(iVisBufMeta *, d->visBufMeta, swapBuffersWith->visBufMeta); + iSwap(iDrawBufs *, d->drawBufs, swapBuffersWith->drawBufs); + updateVisible_DocumentView_(d); + updateVisible_DocumentView_(swapBuffersWith); } static void updateTimestampBuf_DocumentView_(const iDocumentView *d) { @@ -1125,27 +913,6 @@ static void invalidate_DocumentView_(iDocumentView *d) { clear_PtrSet(d->invalidRuns); } -static void invalidate_DocumentWidget_(iDocumentWidget *d) { - if (flags_Widget(as_Widget(d)) & destroyPending_WidgetFlag) { - return; - } - if (d->flags & invalidationPending_DocumentWidgetFlag) { - return; - } - if (isAffectedByVisualOffset_Widget(as_Widget(d))) { - d->flags |= invalidationPending_DocumentWidgetFlag; - return; - } - d->flags &= ~invalidationPending_DocumentWidgetFlag; - invalidate_DocumentView_(&d->view); -// printf("[%p] '%s' invalidated\n", d, cstr_String(id_Widget(as_Widget(d)))); -} - -static iRangecc siteText_DocumentWidget_(const iDocumentWidget *d) { - return isEmpty_String(d->titleUser) ? urlHost_String(d->mod.url) - : range_String(d->titleUser); -} - static void documentRunsInvalidated_DocumentView_(iDocumentView *d) { d->hoverPre = NULL; d->hoverAltPre = NULL; @@ -1154,4299 +921,4414 @@ static void documentRunsInvalidated_DocumentView_(iDocumentView *d) { iZap(d->renderRuns); } -static void documentRunsInvalidated_DocumentWidget_(iDocumentWidget *d) { - d->foundMark = iNullRange; - d->selectMark = iNullRange; - d->contextLink = NULL; - documentRunsInvalidated_DocumentView_(&d->view); +static void resetScroll_DocumentView_(iDocumentView *d) { + reset_SmoothScroll(&d->scrollY); + init_Anim(&d->sideOpacity, 0); + init_Anim(&d->altTextOpacity, 0); + resetWideRuns_DocumentView_(d); } -iBool isPinned_DocumentWidget_(const iDocumentWidget *d) { - if (deviceType_App() == phone_AppDeviceType) { - return iFalse; - } - if (d->flags & otherRootByDefault_DocumentWidgetFlag) { - return iTrue; - } - const iWidget *w = constAs_Widget(d); - const iWindow *win = get_Window(); - if (numRoots_Window(win) == 1) { - return iFalse; - } - const iPrefs *prefs = prefs_App(); - return (prefs->pinSplit == 1 && w->root == win->roots[0]) || - (prefs->pinSplit == 2 && w->root == win->roots[1]); +static void updateWidth_DocumentView_(iDocumentView *d) { + updateWidth_GmDocument(d->doc, documentWidth_DocumentView_(d), width_Widget(d->owner)); } -static void showOrHidePinningIndicator_DocumentWidget_(iDocumentWidget *d) { - iWidget *w = as_Widget(d); - showCollapsed_Widget(findChild_Widget(root_Widget(w), "document.pinned"), - isPinned_DocumentWidget_(d)); +static void updateWidthAndRedoLayout_DocumentView_(iDocumentView *d) { + setWidth_GmDocument(d->doc, documentWidth_DocumentView_(d), width_Widget(d->owner)); } -static void documentWasChanged_DocumentWidget_(iDocumentWidget *d) { - iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iFalse); - setFlags_Widget(as_Widget(d), touchDrag_WidgetFlag, iFalse); - updateVisitedLinks_GmDocument(d->view.doc); - documentRunsInvalidated_DocumentWidget_(d); - updateWindowTitle_DocumentWidget_(d); - updateVisible_DocumentView_(&d->view); - d->view.drawBufs->flags |= updateSideBuf_DrawBufsFlag; - invalidate_DocumentWidget_(d); - refresh_Widget(as_Widget(d)); - /* Check for special bookmark tags. */ - d->flags &= ~otherRootByDefault_DocumentWidgetFlag; - const uint16_t bmid = findUrl_Bookmarks(bookmarks_App(), d->mod.url); - if (bmid) { - const iBookmark *bm = get_Bookmarks(bookmarks_App(), bmid); - if (bm->flags & linkSplit_BookmarkFlag) { - d->flags |= otherRootByDefault_DocumentWidgetFlag; - } +static void clampScroll_DocumentView_(iDocumentView *d) { + move_SmoothScroll(&d->scrollY, 0); +} + +static void immediateScroll_DocumentView_(iDocumentView *d, int offset) { + move_SmoothScroll(&d->scrollY, offset); +} + +static void smoothScroll_DocumentView_(iDocumentView *d, int offset, int duration) { + moveSpan_SmoothScroll(&d->scrollY, offset, duration); +} + +static void scrollTo_DocumentView_(iDocumentView *d, int documentY, iBool centered) { + if (!isEmpty_Banner(d->owner->banner)) { + documentY += height_Banner(d->owner->banner) + documentTopPad_DocumentView_(d); } - showOrHidePinningIndicator_DocumentWidget_(d); - if (~d->flags & fromCache_DocumentWidgetFlag) { - setCachedDocument_History(d->mod.history, - d->view.doc, /* keeps a ref */ - (d->flags & openedFromSidebar_DocumentWidgetFlag) != 0); + else { + documentY += documentTopPad_DocumentView_(d) + d->pageMargin * gap_UI; } + init_Anim(&d->scrollY.pos, + documentY - (centered ? documentBounds_DocumentView_(d).size.y / 2 + : lineHeight_Text(paragraph_FontId))); + clampScroll_DocumentView_(d); } -void setSource_DocumentWidget(iDocumentWidget *d, const iString *source) { - setUrl_GmDocument(d->view.doc, d->mod.url); - const int docWidth = documentWidth_DocumentView_(&d->view); - setSource_GmDocument(d->view.doc, - source, - docWidth, - width_Widget(d), - isFinished_GmRequest(d->request) ? final_GmDocumentUpdate - : partial_GmDocumentUpdate); - setWidth_Banner(d->banner, docWidth); - documentWasChanged_DocumentWidget_(d); +static void scrollToHeading_DocumentView_(iDocumentView *d, const char *heading) { + iConstForEach(Array, h, headings_GmDocument(d->doc)) { + const iGmHeading *head = h.value; + if (startsWithCase_Rangecc(head->text, heading)) { + postCommandf_Root(as_Widget(d->owner)->root, "document.goto loc:%p", head->text.start); + break; + } + } } -static void replaceDocument_DocumentWidget_(iDocumentWidget *d, iGmDocument *newDoc) { - pauseAllPlayers_Media(media_GmDocument(d->view.doc), iTrue); - iRelease(d->view.doc); - d->view.doc = ref_Object(newDoc); - documentWasChanged_DocumentWidget_(d); +static iBool scrollWideBlock_DocumentView_(iDocumentView *d, iInt2 mousePos, int delta, + int duration) { + if (delta == 0 || d->owner->flags & eitherWheelSwipe_DocumentWidgetFlag) { + return iFalse; + } + const iInt2 docPos = documentPos_DocumentView_(d, mousePos); + iConstForEach(PtrArray, i, &d->visibleWideRuns) { + const iGmRun *run = i.ptr; + if (docPos.y >= top_Rect(run->bounds) && docPos.y <= bottom_Rect(run->bounds)) { + /* We can scroll this run. First find out how much is allowed. */ + const iGmRunRange range = findPreformattedRange_GmDocument(d->doc, run); + int maxWidth = 0; + for (const iGmRun *r = range.start; r != range.end; r++) { + maxWidth = iMax(maxWidth, width_Rect(r->visBounds)); + } + const int maxOffset = maxWidth - documentWidth_DocumentView_(d) + d->pageMargin * gap_UI; + if (size_Array(&d->wideRunOffsets) <= preId_GmRun(run)) { + resize_Array(&d->wideRunOffsets, preId_GmRun(run) + 1); + } + int *offset = at_Array(&d->wideRunOffsets, preId_GmRun(run) - 1); + const int oldOffset = *offset; + *offset = iClamp(*offset + delta, 0, maxOffset); + /* Make sure the whole block gets redraw. */ + if (oldOffset != *offset) { + for (const iGmRun *r = range.start; r != range.end; r++) { + insert_PtrSet(d->invalidRuns, r); + } + refresh_Widget(d); + d->owner->selectMark = iNullRange; + d->owner->foundMark = iNullRange; + } + if (duration) { + if (d->animWideRunId != preId_GmRun(run) || isFinished_Anim(&d->animWideRunOffset)) { + d->animWideRunId = preId_GmRun(run); + init_Anim(&d->animWideRunOffset, oldOffset); + } + setValueEased_Anim(&d->animWideRunOffset, *offset, duration); + d->animWideRunRange = range; + addTicker_App(refreshWhileScrolling_DocumentWidget_, d); + } + else { + d->animWideRunId = 0; + init_Anim(&d->animWideRunOffset, 0); + } + return iTrue; + } + } + return iFalse; } -static void updateBanner_DocumentWidget_(iDocumentWidget *d) { - setSite_Banner(d->banner, siteText_DocumentWidget_(d), siteIcon_GmDocument(d->view.doc)); +static iRangecc sourceLoc_DocumentView_(const iDocumentView *d, iInt2 pos) { + return findLoc_GmDocument(d->doc, documentPos_DocumentView_(d, pos)); } -static void updateTheme_DocumentWidget_(iDocumentWidget *d) { - if (document_App() != d || category_GmStatusCode(d->sourceStatus) == categoryInput_GmStatusCode) { +iDeclareType(MiddleRunParams) + +struct Impl_MiddleRunParams { + int midY; + const iGmRun *closest; + int distance; +}; + +static void find_MiddleRunParams_(void *params, const iGmRun *run) { + iMiddleRunParams *d = params; + if (isEmpty_Rect(run->bounds)) { return; - } - d->view.drawBufs->flags |= updateTimestampBuf_DrawBufsFlag; - updateBanner_DocumentWidget_(d); + } + const int distance = iAbs(mid_Rect(run->bounds).y - d->midY); + if (!d->closest || distance < d->distance) { + d->closest = run; + d->distance = distance; + } } -static void makeFooterButtons_DocumentWidget_(iDocumentWidget *d, const iMenuItem *items, size_t count) { - iWidget *w = as_Widget(d); - destroy_Widget(d->footerButtons); - d->footerButtons = NULL; - if (count == 0) { - return; +static const iGmRun *middleRun_DocumentView_(const iDocumentView *d) { + iRangei visRange = visibleRange_DocumentView_(d); + iMiddleRunParams params = { (visRange.start + visRange.end) / 2, NULL, 0 }; + render_GmDocument(d->doc, visRange, find_MiddleRunParams_, ¶ms); + return params.closest; +} + +static void allocVisBuffer_DocumentView_(const iDocumentView *d) { + const iWidget *w = constAs_Widget(d->owner); + const iBool isVisible = isVisible_Widget(w); + const iInt2 size = bounds_Widget(w).size; + if (isVisible) { + alloc_VisBuf(d->visBuf, size, 1); } - d->footerButtons = new_Widget(); - setFlags_Widget(d->footerButtons, - unhittable_WidgetFlag | arrangeVertical_WidgetFlag | - resizeWidthOfChildren_WidgetFlag | arrangeHeight_WidgetFlag | - fixedPosition_WidgetFlag | resizeToParentWidth_WidgetFlag, - iTrue); - for (size_t i = 0; i < count; ++i) { - iLabelWidget *button = addChildFlags_Widget( - d->footerButtons, - iClob(newKeyMods_LabelWidget( - items[i].label, items[i].key, items[i].kmods, items[i].command)), - alignLeft_WidgetFlag | drawKey_WidgetFlag | extraPadding_WidgetFlag); - setPadding1_Widget(as_Widget(button), gap_UI / 2); - checkIcon_LabelWidget(button); - setFont_LabelWidget(button, uiContent_FontId); - setBackgroundColor_Widget(as_Widget(button), uiBackgroundSidebar_ColorId); + else { + dealloc_VisBuf(d->visBuf); } - addChild_Widget(as_Widget(d), iClob(d->footerButtons)); - arrange_Widget(d->footerButtons); - arrange_Widget(w); - updateVisible_DocumentView_(&d->view); /* final placement for the buttons */ } -static void resetScroll_DocumentView_(iDocumentView *d) { - reset_SmoothScroll(&d->scrollY); - init_Anim(&d->sideOpacity, 0); - init_Anim(&d->altTextOpacity, 0); - resetWideRuns_DocumentView_(d); +static size_t visibleLinkOrdinal_DocumentView_(const iDocumentView *d, iGmLinkId linkId) { + size_t ord = 0; + const iRangei visRange = visibleRange_DocumentView_(d); + iConstForEach(PtrArray, i, &d->visibleLinks) { + const iGmRun *run = i.ptr; + if (top_Rect(run->visBounds) >= visRange.start + gap_UI * d->pageMargin * 4 / 5) { + if (run->flags & decoration_GmRunFlag && run->linkId) { + if (run->linkId == linkId) return ord; + ord++; + } + } + } + return iInvalidPos; } -static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode code, - const iString *meta) { - iString *src = collectNew_String(); - const iGmError *msg = get_GmError(code); - destroy_Widget(d->footerButtons); - d->footerButtons = NULL; - const iString *serverErrorMsg = NULL; - if (meta) { - switch (code) { - case schemeChangeRedirect_GmStatusCode: - case tooManyRedirects_GmStatusCode: - appendFormat_String(src, "=> %s\n", cstr_String(meta)); - break; - case tlsFailure_GmStatusCode: -// useBanner = iFalse; /* valid data wasn't received from host */ -// appendFormat_String(src, ">%s\n", cstr_String(meta)); - break; - case tlsServerCertificateExpired_GmStatusCode: - makeFooterButtons_DocumentWidget_( - d, - (iMenuItem[]){ { rightArrowhead_Icon " ${menu.unexpire}", - SDLK_RETURN, 0, "server.unexpire" - }, - { info_Icon " ${menu.pageinfo}", - SDLK_i, - KMOD_PRIMARY, - "document.info" } }, - 2); - break; - case tlsServerCertificateNotVerified_GmStatusCode: - makeFooterButtons_DocumentWidget_( - d, - (iMenuItem[]){ { info_Icon " ${menu.pageinfo}", - SDLK_i, - KMOD_PRIMARY, - "document.info" } }, - 1); - break; - case failedToOpenFile_GmStatusCode: - case certificateNotValid_GmStatusCode: -// appendFormat_String(src, "%s", cstr_String(meta)); - break; - case unsupportedMimeType_GmStatusCode: { - iString *key = collectNew_String(); - toString_Sym(SDLK_s, KMOD_PRIMARY, key); -// appendFormat_String(src, "\n```\n%s\n```\n", cstr_String(meta)); - const char *mtype = mediaTypeFromFileExtension_String(d->mod.url); - iArray items; - init_Array(&items, sizeof(iMenuItem)); - if (iCmpStr(mtype, "application/octet-stream")) { - pushBack_Array( - &items, - &(iMenuItem){ translateCStr_Lang(format_CStr("View as \"%s\"", mtype)), - SDLK_RETURN, - 0, - format_CStr("document.setmediatype mime:%s", mtype) }); - } - pushBack_Array( - &items, - &(iMenuItem){ translateCStr_Lang(download_Icon " " saveToDownloads_Label), - 0, - 0, - "document.save" }); - makeFooterButtons_DocumentWidget_(d, data_Array(&items), size_Array(&items)); - deinit_Array(&items); - serverErrorMsg = collectNewFormat_String("%s (%s)", msg->title, cstr_String(meta)); - break; - } - default: - if (!isEmpty_String(meta)) { - serverErrorMsg = meta; - } - break; +static void documentRunsInvalidated_DocumentWidget_(iDocumentWidget *d) { + d->foundMark = iNullRange; + d->selectMark = iNullRange; + d->contextLink = NULL; + documentRunsInvalidated_DocumentView_(&d->view); +} + +static iBool updateDocumentWidthRetainingScrollPosition_DocumentView_(iDocumentView *d, + iBool keepCenter) { + const int newWidth = documentWidth_DocumentView_(d); + if (newWidth == size_GmDocument(d->doc).x && !keepCenter /* not a font change */) { + return iFalse; + } + /* Font changes (i.e., zooming) will keep the view centered, otherwise keep the top + of the visible area fixed. */ + const iGmRun *run = keepCenter ? middleRun_DocumentView_(d) : d->visibleRuns.start; + const char * runLoc = (run ? run->text.start : NULL); + int voffset = 0; + if (!keepCenter && run) { + /* Keep the first visible run visible at the same position. */ + /* TODO: First *fully* visible run? */ + voffset = visibleRange_DocumentView_(d).start - top_Rect(run->visBounds); + } + setWidth_GmDocument(d->doc, newWidth, width_Widget(d->owner)); + setWidth_Banner(d->owner->banner, newWidth); + documentRunsInvalidated_DocumentWidget_(d->owner); + if (runLoc && !keepCenter) { + run = findRunAtLoc_GmDocument(d->doc, runLoc); + if (run) { + scrollTo_DocumentView_( + d, top_Rect(run->visBounds) + lineHeight_Text(paragraph_FontId) + voffset, iFalse); } } - if (category_GmStatusCode(code) == categoryClientCertificate_GmStatus) { - makeFooterButtons_DocumentWidget_( - d, - (iMenuItem[]){ - { leftHalf_Icon " ${menu.show.identities}", - '4', - KMOD_PRIMARY, - deviceType_App() == desktop_AppDeviceType ? "sidebar.mode arg:3 show:1" - : "preferences idents:1" }, - { person_Icon " ${menu.identity.new}", newIdentity_KeyShortcut, "ident.new" } }, - 2); + else if (runLoc && keepCenter) { + run = findRunAtLoc_GmDocument(d->doc, runLoc); + if (run) { + scrollTo_DocumentView_(d, mid_Rect(run->bounds).y, iTrue); + } } - /* Make a new document for the error page.*/ - iGmDocument *errorDoc = new_GmDocument(); - setWidth_GmDocument(errorDoc, documentWidth_DocumentView_(&d->view), width_Widget(d)); - setUrl_GmDocument(errorDoc, d->mod.url); - setFormat_GmDocument(errorDoc, gemini_SourceFormat); - replaceDocument_DocumentWidget_(d, errorDoc); - iRelease(errorDoc); - clear_Banner(d->banner); - add_Banner(d->banner, error_BannerType, code, serverErrorMsg, NULL); - d->state = ready_RequestState; - setSource_DocumentWidget(d, src); - updateTheme_DocumentWidget_(d); - resetScroll_DocumentView_(&d->view); + return iTrue; } -static void updateFetchProgress_DocumentWidget_(iDocumentWidget *d) { - iLabelWidget *prog = findChild_Widget(root_Widget(as_Widget(d)), "document.progress"); - const size_t dlSize = d->request ? bodySize_GmRequest(d->request) : 0; - showCollapsed_Widget(as_Widget(prog), dlSize >= 250000); - if (isVisible_Widget(prog)) { - updateText_LabelWidget(prog, - collectNewFormat_String("%s%.3f ${mb}", - isFinished_GmRequest(d->request) - ? uiHeading_ColorEscape - : uiTextCaution_ColorEscape, - dlSize / 1.0e6f)); - } +static iRect runRect_DocumentView_(const iDocumentView *d, const iGmRun *run) { + const iRect docBounds = documentBounds_DocumentView_(d); + return moved_Rect(run->bounds, addY_I2(topLeft_Rect(docBounds), viewPos_DocumentView_(d))); } -static const char *zipPageHeading_(const iRangecc mime) { - if (equalCase_Rangecc(mime, "application/gpub+zip")) { - return book_Icon " Gempub"; +iDeclareType(DrawContext) + +struct Impl_DrawContext { + const iDocumentView *view; + iRect widgetBounds; + iRect docBounds; + iRangei vis; + iInt2 viewPos; /* document area origin */ + iPaint paint; + iBool inSelectMark; + iBool inFoundMark; + iBool showLinkNumbers; + iRect firstMarkRect; + iRect lastMarkRect; + iGmRunRange runsDrawn; +}; + +static void fillRange_DrawContext_(iDrawContext *d, const iGmRun *run, enum iColorId color, + iRangecc mark, iBool *isInside) { + if (mark.start > mark.end) { + /* Selection may be done in either direction. */ + iSwap(const char *, mark.start, mark.end); } - else if (equalCase_Rangecc(mime, mimeType_FontPack)) { - return fontpack_Icon " Fontpack"; + if (*isInside || (contains_Range(&run->text, mark.start) || + contains_Range(&mark, run->text.start))) { + int x = 0; + if (!*isInside) { + x = measureRange_Text(run->font, + (iRangecc){ run->text.start, iMax(run->text.start, mark.start) }) + .advance.x; + } + int w = width_Rect(run->visBounds) - x; + if (contains_Range(&run->text, mark.end) || mark.end < run->text.start) { + iRangecc mk = !*isInside ? mark + : (iRangecc){ run->text.start, iMax(run->text.start, mark.end) }; + mk.start = iMax(mk.start, run->text.start); + w = measureRange_Text(run->font, mk).advance.x; + *isInside = iFalse; + } + else { + *isInside = iTrue; /* at least until the next run */ + } + if (w > width_Rect(run->visBounds) - x) { + w = width_Rect(run->visBounds) - x; + } + if (~run->flags & decoration_GmRunFlag) { + const iInt2 visPos = + add_I2(run->bounds.pos, addY_I2(d->viewPos, viewPos_DocumentView_(d->view))); + const iRect rangeRect = { addX_I2(visPos, x), init_I2(w, height_Rect(run->bounds)) }; + if (rangeRect.size.x) { + fillRect_Paint(&d->paint, rangeRect, color); + /* Keep track of the first and last marked rects. */ + if (d->firstMarkRect.size.x == 0) { + d->firstMarkRect = rangeRect; + } + d->lastMarkRect = rangeRect; + } + } } - iRangecc type = iNullRange; - nextSplit_Rangecc(mime, "/", &type); /* skip the part before the slash */ - nextSplit_Rangecc(mime, "/", &type); - if (startsWithCase_Rangecc(type, "x-")) { - type.start += 2; + /* Link URLs are not part of the visible document, so they are ignored above. Handle + these ranges as a special case. */ + if (run->linkId && run->flags & decoration_GmRunFlag) { + const iRangecc url = linkUrlRange_GmDocument(d->view->doc, run->linkId); + if (contains_Range(&url, mark.start) && + (contains_Range(&url, mark.end) || url.end == mark.end)) { + fillRect_Paint( + &d->paint, + moved_Rect(run->visBounds, addY_I2(d->viewPos, viewPos_DocumentView_(d->view))), + color); + } } - iString *heading = upper_String(collectNewRange_String(type)); - appendCStr_String(heading, " Archive"); - prependCStr_String(heading, folder_Icon " "); - return cstrCollect_String(heading); } -static void postProcessRequestContent_DocumentWidget_(iDocumentWidget *d, iBool isCached) { - iWidget *w = as_Widget(d); - /* Gempub page behavior and footer actions. */ { - /* TODO: move this to gempub.c */ - delete_Gempub(d->sourceGempub); - d->sourceGempub = NULL; - if (!cmpCase_String(&d->sourceMime, "application/octet-stream") || - !cmpCase_String(&d->sourceMime, mimeType_Gempub) || - endsWithCase_String(d->mod.url, ".gpub")) { - iGempub *gempub = new_Gempub(); - if (open_Gempub(gempub, &d->sourceContent)) { - setBaseUrl_Gempub(gempub, d->mod.url); - setSource_DocumentWidget(d, collect_String(coverPageSource_Gempub(gempub))); - setCStr_String(&d->sourceMime, mimeType_Gempub); - d->sourceGempub = gempub; - } - else { - delete_Gempub(gempub); - } +static void drawMark_DrawContext_(void *context, const iGmRun *run) { + iDrawContext *d = context; + if (!isMedia_GmRun(run)) { + fillRange_DrawContext_(d, run, uiMatching_ColorId, d->view->owner->foundMark, &d->inFoundMark); + fillRange_DrawContext_(d, run, uiMarked_ColorId, d->view->owner->selectMark, &d->inSelectMark); + } +} + +static void drawRun_DrawContext_(void *context, const iGmRun *run) { + iDrawContext *d = context; + const iInt2 origin = d->viewPos; + /* Keep track of the drawn visible runs. */ { + if (!d->runsDrawn.start || run < d->runsDrawn.start) { + d->runsDrawn.start = run; } - if (!d->sourceGempub) { - const iString *localPath = collect_String(localFilePathFromUrl_String(d->mod.url)); - iBool isInside = iFalse; - if (localPath && !fileExists_FileInfo(localPath)) { - /* This URL may refer to a file inside the archive. */ - localPath = findContainerArchive_Path(localPath); - isInside = iTrue; - } - if (localPath && equal_CStr(mediaType_Path(localPath), mimeType_Gempub)) { - iGempub *gempub = new_Gempub(); - if (openFile_Gempub(gempub, localPath)) { - setBaseUrl_Gempub(gempub, collect_String(makeFileUrl_String(localPath))); - if (!isInside) { - setSource_DocumentWidget(d, collect_String(coverPageSource_Gempub(gempub))); - setCStr_String(&d->sourceMime, mimeType_Gempub); - } - d->sourceGempub = gempub; - } - else { - delete_Gempub(gempub); - } - } + if (!d->runsDrawn.end || run > d->runsDrawn.end) { + d->runsDrawn.end = run; } - if (d->sourceGempub) { - if (equal_String(d->mod.url, coverPageUrl_Gempub(d->sourceGempub))) { - if (!isRemote_Gempub(d->sourceGempub)) { - iArray *items = collectNew_Array(sizeof(iMenuItem)); - pushBack_Array( - items, - &(iMenuItem){ book_Icon " ${gempub.cover.view}", - 0, - 0, - format_CStr("!open url:%s", - cstr_String(indexPageUrl_Gempub(d->sourceGempub))) }); - if (navSize_Gempub(d->sourceGempub) > 0) { - pushBack_Array( - items, - &(iMenuItem){ - format_CStr(forwardArrow_Icon " %s", - cstr_String(navLinkLabel_Gempub(d->sourceGempub, 0))), - SDLK_RIGHT, - 0, - format_CStr("!open url:%s", - cstr_String(navLinkUrl_Gempub(d->sourceGempub, 0))) }); - } - makeFooterButtons_DocumentWidget_(d, constData_Array(items), size_Array(items)); - } - else { - makeFooterButtons_DocumentWidget_( - d, - (iMenuItem[]){ { book_Icon " ${menu.save.downloads.open}", - SDLK_s, - KMOD_PRIMARY | KMOD_SHIFT, - "document.save open:1" }, - { download_Icon " " saveToDownloads_Label, - SDLK_s, - KMOD_PRIMARY, - "document.save" } }, - 2); - } - if (preloadCoverImage_Gempub(d->sourceGempub, d->view.doc)) { - redoLayout_GmDocument(d->view.doc); - updateVisible_DocumentView_(&d->view); - invalidate_DocumentWidget_(d); - } + } + if (run->mediaType == image_MediaType) { + SDL_Texture *tex = imageTexture_Media(media_GmDocument(d->view->doc), mediaId_GmRun(run)); + const iRect dst = moved_Rect(run->visBounds, origin); + if (tex) { + fillRect_Paint(&d->paint, dst, tmBackground_ColorId); /* in case the image has alpha */ + SDL_RenderCopy(d->paint.dst->render, tex, NULL, + &(SDL_Rect){ dst.pos.x, dst.pos.y, dst.size.x, dst.size.y }); + } + else { + drawRect_Paint(&d->paint, dst, tmQuoteIcon_ColorId); + drawCentered_Text(uiLabel_FontId, + dst, + iFalse, + tmQuote_ColorId, + explosion_Icon " Error Loading Image"); + } + return; + } + else if (isMedia_GmRun(run)) { + /* Media UIs are drawn afterwards as a dynamic overlay. */ + return; + } + enum iColorId fg = run->color; + const iGmDocument *doc = d->view->doc; + const int linkFlags = linkFlags_GmDocument(doc, run->linkId); + /* Hover state of a link. */ + iBool isHover = + (run->linkId && d->view->hoverLink && run->linkId == d->view->hoverLink->linkId && + ~run->flags & decoration_GmRunFlag); + /* Visible (scrolled) position of the run. */ + const iInt2 visPos = addX_I2(add_I2(run->visBounds.pos, origin), + /* Preformatted runs can be scrolled. */ + runOffset_DocumentView_(d->view, run)); + const iRect visRect = { visPos, run->visBounds.size }; + /* Fill the background. */ { +#if 0 + iBool isInlineImageCaption = run->linkId && linkFlags & content_GmLinkFlag && + ~linkFlags & permanent_GmLinkFlag; + if (run->flags & decoration_GmRunFlag && ~run->flags & startOfLine_GmRunFlag) { + /* This is the metadata. */ + isInlineImageCaption = iFalse; + } +#endif + /* While this is consistent, it's a bit excessive to indicate that an inlined image + is open: the image itself is the indication. */ + const iBool isInlineImageCaption = iFalse; + if (run->linkId && (linkFlags & isOpen_GmLinkFlag || isInlineImageCaption)) { + /* Open links get a highlighted background. */ + int bg = tmBackgroundOpenLink_ColorId; + const int frame = tmFrameOpenLink_ColorId; + const int pad = gap_Text; + iRect wideRect = { init_I2(origin.x - pad, visPos.y), + init_I2(d->docBounds.size.x + 2 * pad, + height_Rect(run->visBounds)) }; + adjustEdges_Rect(&wideRect, + run->flags & startOfLine_GmRunFlag ? -pad * 3 / 4 : 0, 0, + run->flags & endOfLine_GmRunFlag ? pad * 3 / 4 : 0, 0); + /* The first line is composed of two runs that may be drawn in either order, so + only draw half of the background. */ + if (run->flags & decoration_GmRunFlag) { + wideRect.size.x = right_Rect(visRect) - left_Rect(wideRect); } - else if (equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) { - makeFooterButtons_DocumentWidget_( - d, - (iMenuItem[]){ { format_CStr(book_Icon " %s", - cstr_String(property_Gempub(d->sourceGempub, - title_GempubProperty))), - SDLK_LEFT, - 0, - format_CStr("!open url:%s", - cstr_String(coverPageUrl_Gempub(d->sourceGempub))) } }, - 1); + else if (run->flags & startOfLine_GmRunFlag) { + wideRect.size.x = right_Rect(wideRect) - left_Rect(visRect); + wideRect.pos.x = left_Rect(visRect); } - else { - /* Navigation buttons. */ - iArray *items = collectNew_Array(sizeof(iMenuItem)); - const size_t navIndex = navIndex_Gempub(d->sourceGempub, d->mod.url); - if (navIndex != iInvalidPos) { - if (navIndex < navSize_Gempub(d->sourceGempub) - 1) { - pushBack_Array( - items, - &(iMenuItem){ - format_CStr(forwardArrow_Icon " %s", - cstr_String(navLinkLabel_Gempub(d->sourceGempub, navIndex + 1))), - SDLK_RIGHT, - 0, - format_CStr("!open url:%s", - cstr_String(navLinkUrl_Gempub(d->sourceGempub, navIndex + 1))) }); - } - if (navIndex > 0) { - pushBack_Array( - items, - &(iMenuItem){ - format_CStr(backArrow_Icon " %s", - cstr_String(navLinkLabel_Gempub(d->sourceGempub, navIndex - 1))), - SDLK_LEFT, - 0, - format_CStr("!open url:%s", - cstr_String(navLinkUrl_Gempub(d->sourceGempub, navIndex - 1))) }); - } - else if (!equalCase_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) { - pushBack_Array( - items, - &(iMenuItem){ - format_CStr(book_Icon " %s", - cstr_String(property_Gempub(d->sourceGempub, title_GempubProperty))), - SDLK_LEFT, - 0, - format_CStr("!open url:%s", - cstr_String(coverPageUrl_Gempub(d->sourceGempub))) }); - } - } - if (!isEmpty_Array(items)) { - makeFooterButtons_DocumentWidget_(d, constData_Array(items), size_Array(items)); - } + fillRect_Paint(&d->paint, wideRect, bg); + } + else { + /* Normal background for other runs. There are cases when runs get drawn multiple times, + e.g., at the buffer boundary, and there are slightly overlapping characters in + monospace blocks. Clearing the background here ensures a cleaner visual appearance + since only one glyph is visible at any given point. */ + fillRect_Paint(&d->paint, visRect, tmBackground_ColorId); + } + } + if (run->linkId) { + if (run->flags & decoration_GmRunFlag && run->flags & startOfLine_GmRunFlag) { + /* Link icon. */ + if (linkFlags & content_GmLinkFlag) { + fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart); } - if (!isCached && prefs_App()->pinSplit && - equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) { - const iString *navStart = navStartLinkUrl_Gempub(d->sourceGempub); - if (navStart) { - iWindow *win = get_Window(); - /* Auto-split to show index and the first navigation link. */ - if (numRoots_Window(win) == 2) { - /* This document is showing the index page. */ - iRoot *other = otherRoot_Window(win, w->root); - postCommandf_Root(other, "open url:%s", cstr_String(navStart)); - if (prefs_App()->pinSplit == 1 && w->root == win->roots[1]) { - /* On the wrong side. */ - postCommand_App("ui.split swap:1"); - } - } - else { - postCommandf_App( - "open splitmode:1 newtab:%d url:%s", otherRoot_OpenTabFlag, cstr_String(navStart)); - } - } + } + else if (~run->flags & decoration_GmRunFlag) { + fg = linkColor_GmDocument(doc, run->linkId, isHover ? textHover_GmLinkPart : text_GmLinkPart); + if (linkFlags & content_GmLinkFlag) { + fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart); /* link is inactive */ } } } -} - -static void updateWidth_DocumentView_(iDocumentView *d) { - updateWidth_GmDocument(d->doc, documentWidth_DocumentView_(d), width_Widget(d->owner)); -} - -static void updateWidthAndRedoLayout_DocumentView_(iDocumentView *d) { - setWidth_GmDocument(d->doc, documentWidth_DocumentView_(d), width_Widget(d->owner)); -} - -static void updateDocument_DocumentWidget_(iDocumentWidget *d, - const iGmResponse *response, - iGmDocument *cachedDoc, - const iBool isInitialUpdate) { - if (d->state == ready_RequestState) { - return; + if (run->flags & altText_GmRunFlag) { + const iInt2 margin = preRunMargin_GmDocument(doc, preId_GmRun(run)); + fillRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmBackgroundAltText_ColorId); + drawRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmFrameAltText_ColorId); + drawWrapRange_Text(run->font, + add_I2(visPos, margin), + run->visBounds.size.x - 2 * margin.x, + run->color, + run->text); } - const iBool isRequestFinished = isFinished_GmRequest(d->request); - /* TODO: Do document update in the background. However, that requires a text metrics calculator - that does not try to cache the glyph bitmaps. */ - const enum iGmStatusCode statusCode = response->statusCode; - if (category_GmStatusCode(statusCode) != categoryInput_GmStatusCode) { - iBool setSource = iTrue; - iString str; - invalidate_DocumentWidget_(d); - if (document_App() == d) { - updateTheme_DocumentWidget_(d); - } - clear_String(&d->sourceMime); - d->sourceTime = response->when; - d->view.drawBufs->flags |= updateTimestampBuf_DrawBufsFlag; - initBlock_String(&str, &response->body); /* Note: Body may be megabytes in size. */ - if (isSuccess_GmStatusCode(statusCode)) { - /* Check the MIME type. */ - iRangecc charset = range_CStr("utf-8"); - enum iSourceFormat docFormat = undefined_SourceFormat; - const iString *mimeStr = collect_String(lower_String(&response->meta)); /* for convenience */ - set_String(&d->sourceMime, mimeStr); - iRangecc mime = range_String(mimeStr); - iRangecc seg = iNullRange; - while (nextSplit_Rangecc(mime, ";", &seg)) { - iRangecc param = seg; - trim_Rangecc(¶m); - /* Detect fontpacks even if the server doesn't use the right media type. */ - if (isRequestFinished && equal_Rangecc(param, "application/octet-stream")) { - if (detect_FontPack(&response->body)) { - param = range_CStr(mimeType_FontPack); - } - } - if (equal_Rangecc(param, "text/gemini")) { - docFormat = gemini_SourceFormat; - setRange_String(&d->sourceMime, param); - } - else if (equal_Rangecc(param, "text/markdown")) { - docFormat = markdown_SourceFormat; - setRange_String(&d->sourceMime, param); - } - else if (startsWith_Rangecc(param, "text/") || - equal_Rangecc(param, "application/json") || - equal_Rangecc(param, "application/x-pem-file") || - equal_Rangecc(param, "application/pem-certificate-chain")) { - docFormat = plainText_SourceFormat; - setRange_String(&d->sourceMime, param); - } - else if (isRequestFinished && equal_Rangecc(param, "font/ttf")) { - clear_String(&str); - docFormat = gemini_SourceFormat; - setRange_String(&d->sourceMime, param); - format_String(&str, "# TrueType Font\n"); - iString *decUrl = collect_String(urlDecode_String(d->mod.url)); - iRangecc name = baseName_Path(decUrl); - iBool isInstalled = iFalse; - if (startsWith_String(collect_String(localFilePathFromUrl_String(d->mod.url)), - cstr_String(dataDir_App()))) { - isInstalled = iTrue; - } - appendCStr_String(&str, "## "); - appendRange_String(&str, name); - appendCStr_String(&str, "\n\n"); - appendCStr_String( - &str, cstr_Lang(isInstalled ? "truetype.help.installed" : "truetype.help")); - appendCStr_String(&str, "\n"); - if (!isInstalled) { - makeFooterButtons_DocumentWidget_( - d, - (iMenuItem[]){ - { add_Icon " ${fontpack.install.ttf}", - SDLK_RETURN, - 0, - format_CStr("!fontpack.install ttf:1 name:%s", - cstr_Rangecc(name)) }, - { folder_Icon " ${fontpack.open.fontsdir}", - SDLK_d, - 0, - format_CStr("!open url:%s/fonts", - cstrCollect_String(makeFileUrl_String(dataDir_App()))) - } - }, 2); - } - } - else if (isRequestFinished && - (equal_Rangecc(param, "application/zip") || - (startsWith_Rangecc(param, "application/") && - endsWithCase_Rangecc(param, "+zip")))) { - clear_String(&str); - docFormat = gemini_SourceFormat; - setRange_String(&d->sourceMime, param); - if (equal_Rangecc(param, mimeType_FontPack)) { - /* Show some information about fontpacks, and set up footer actions. */ - iArchive *zip = iClob(new_Archive()); - if (openData_Archive(zip, &response->body)) { - iFontPack *fp = new_FontPack(); - setUrl_FontPack(fp, d->mod.url); - setStandalone_FontPack(fp, iTrue); - if (loadArchive_FontPack(fp, zip)) { - appendFormat_String(&str, "# " fontpack_Icon "%s\n%s", - cstr_String(id_FontPack(fp).id), - cstrCollect_String(infoText_FontPack(fp))); - } - appendCStr_String(&str, "\n"); - appendCStr_String(&str, cstr_Lang("fontpack.help")); - appendCStr_String(&str, "\n"); - const iArray *actions = actions_FontPack(fp, iTrue); - makeFooterButtons_DocumentWidget_(d, constData_Array(actions), - size_Array(actions)); - delete_FontPack(fp); - } - } - else { - format_String(&str, "# %s\n", zipPageHeading_(param)); - appendFormat_String(&str, - cstr_Lang("doc.archive"), - cstr_Rangecc(baseName_Path(d->mod.url))); - appendCStr_String(&str, "\n"); - } - appendCStr_String(&str, "\n"); - iString *localPath = localFilePathFromUrl_String(d->mod.url); - if (!localPath) { - iString *key = collectNew_String(); - toString_Sym(SDLK_s, KMOD_PRIMARY, key); - appendFormat_String(&str, "%s\n\n", - format_CStr(cstr_Lang("error.unsupported.suggestsave"), - cstr_String(key), - saveToDownloads_Label)); - } - delete_String(localPath); - if (equalCase_Rangecc(urlScheme_String(d->mod.url), "file")) { - appendFormat_String(&str, "=> %s/ " folder_Icon " ${doc.archive.view}\n", - cstr_String(withSpacesEncoded_String(d->mod.url))); - } - translate_Lang(&str); - } - else if (startsWith_Rangecc(param, "image/") || - startsWith_Rangecc(param, "audio/")) { - const iBool isAudio = startsWith_Rangecc(param, "audio/"); - /* Make a simple document with an image or audio player. */ - clear_String(&str); - docFormat = gemini_SourceFormat; - setRange_String(&d->sourceMime, param); - const iGmLinkId imgLinkId = 1; /* there's only the one link */ - /* TODO: Do the image loading in `postProcessRequestContent_DocumentWidget_()` */ - if ((isAudio && isInitialUpdate) || (!isAudio && isRequestFinished)) { - const char *linkTitle = cstr_Lang( - startsWith_String(mimeStr, "image/") ? "media.untitled.image" - : "media.untitled.audio"); - iUrl parts; - init_Url(&parts, d->mod.url); - if (!isEmpty_Range(&parts.path)) { - linkTitle = - baseName_Path(collect_String(newRange_String(parts.path))).start; - } - format_String(&str, "=> %s %s\n", - cstr_String(canonicalUrl_String(d->mod.url)), - linkTitle); - setData_Media(media_GmDocument(d->view.doc), - imgLinkId, - mimeStr, - &response->body, - !isRequestFinished ? partialData_MediaFlag : 0); - redoLayout_GmDocument(d->view.doc); - } - else if (isAudio && !isInitialUpdate) { - /* Update the audio content. */ - setData_Media(media_GmDocument(d->view.doc), - imgLinkId, - mimeStr, - &response->body, - !isRequestFinished ? partialData_MediaFlag : 0); - refresh_Widget(d); - setSource = iFalse; - } - else { - clear_String(&str); - } - } - else if (startsWith_Rangecc(param, "charset=")) { - charset = (iRangecc){ param.start + 8, param.end }; - /* Remove whitespace and quotes. */ - trim_Rangecc(&charset); - if (*charset.start == '"' && *charset.end == '"') { - charset.start++; - charset.end--; - } - } - } - if (docFormat == undefined_SourceFormat) { - if (isRequestFinished) { - d->flags &= ~drawDownloadCounter_DocumentWidgetFlag; - showErrorPage_DocumentWidget_(d, unsupportedMimeType_GmStatusCode, &response->meta); - deinit_String(&str); - return; - } - d->flags |= drawDownloadCounter_DocumentWidgetFlag; - clear_PtrSet(d->view.invalidRuns); - deinit_String(&str); - return; - } - setFormat_GmDocument(d->view.doc, docFormat); - /* Convert the source to UTF-8 if needed. */ - if (!equalCase_Rangecc(charset, "utf-8")) { - set_String(&str, - collect_String(decode_Block(&str.chars, cstr_Rangecc(charset)))); - } - } - if (cachedDoc) { - replaceDocument_DocumentWidget_(d, cachedDoc); - updateWidth_DocumentView_(&d->view); - } - else if (setSource) { - setSource_DocumentWidget(d, &str); - } - deinit_String(&str); - } -} - -static void fetch_DocumentWidget_(iDocumentWidget *d) { - iAssert(~d->flags & animationPlaceholder_DocumentWidgetFlag); - /* Forget the previous request. */ - if (d->request) { - iRelease(d->request); - d->request = NULL; - } - postCommandf_Root(as_Widget(d)->root, - "document.request.started doc:%p url:%s", - d, - cstr_String(d->mod.url)); - clear_ObjectList(d->media); - d->certFlags = 0; - setLinkNumberMode_DocumentWidget_(d, iFalse); - d->flags &= ~drawDownloadCounter_DocumentWidgetFlag; - d->flags &= ~fromCache_DocumentWidgetFlag; - d->state = fetching_RequestState; - set_Atomic(&d->isRequestUpdated, iFalse); - d->request = new_GmRequest(certs_App()); - setUrl_GmRequest(d->request, d->mod.url); - iConnect(GmRequest, d->request, updated, d, requestUpdated_DocumentWidget_); - iConnect(GmRequest, d->request, finished, d, requestFinished_DocumentWidget_); - submit_GmRequest(d->request); -} - -static void updateTrust_DocumentWidget_(iDocumentWidget *d, const iGmResponse *response) { - if (response) { - d->certFlags = response->certFlags; - d->certExpiry = response->certValidUntil; - set_Block(d->certFingerprint, &response->certFingerprint); - set_String(d->certSubject, &response->certSubject); - } - iLabelWidget *lock = findChild_Widget(root_Widget(as_Widget(d)), "navbar.lock"); - if (~d->certFlags & available_GmCertFlag) { - setFlags_Widget(as_Widget(lock), disabled_WidgetFlag, iTrue); - updateTextCStr_LabelWidget(lock, gray50_ColorEscape openLock_Icon); - return; - } - setFlags_Widget(as_Widget(lock), disabled_WidgetFlag, iFalse); - const iBool isDarkMode = isDark_ColorTheme(colorTheme_App()); - if (~d->certFlags & domainVerified_GmCertFlag || - ~d->certFlags & trusted_GmCertFlag) { - updateTextCStr_LabelWidget(lock, red_ColorEscape warning_Icon); - } - else if (~d->certFlags & timeVerified_GmCertFlag) { - updateTextCStr_LabelWidget(lock, isDarkMode ? orange_ColorEscape warning_Icon - : black_ColorEscape warning_Icon); - } - else { - updateTextCStr_LabelWidget(lock, green_ColorEscape closedLock_Icon); - } -} - -static void parseUser_DocumentWidget_(iDocumentWidget *d) { - setRange_String(d->titleUser, urlUser_String(d->mod.url)); -} - -static void cacheRunGlyphs_(void *data, const iGmRun *run) { - iUnused(data); - if (!isEmpty_Range(&run->text)) { - cache_Text(run->font, run->text); - } -} - -static void cacheDocumentGlyphs_DocumentWidget_(const iDocumentWidget *d) { - if (isFinishedLaunching_App() && isExposed_Window(get_Window()) && - ~d->flags & animationPlaceholder_DocumentWidgetFlag) { - /* Just cache the top of the document, since this is what we usually need. */ - int maxY = height_Widget(&d->widget) * 2; - if (maxY == 0) { - maxY = size_GmDocument(d->view.doc).y; - } - render_GmDocument(d->view.doc, (iRangei){ 0, maxY }, cacheRunGlyphs_, NULL); - } -} - -static void addBannerWarnings_DocumentWidget_(iDocumentWidget *d) { - /* Warnings related to certificates and trust. */ - const int certFlags = d->certFlags; - const int req = timeVerified_GmCertFlag | domainVerified_GmCertFlag | trusted_GmCertFlag; - if (certFlags & available_GmCertFlag && (certFlags & req) != req && - numItems_Banner(d->banner) == 0) { - iString *title = collectNewCStr_String(cstr_Lang("dlg.certwarn.title")); - iString *str = collectNew_String(); - if (certFlags & timeVerified_GmCertFlag && certFlags & domainVerified_GmCertFlag) { - iUrl parts; - init_Url(&parts, d->mod.url); - const iTime oldUntil = - domainValidUntil_GmCerts(certs_App(), parts.host, port_Url(&parts)); - iDate exp; - init_Date(&exp, &oldUntil); - iTime now; - initCurrent_Time(&now); - const int days = secondsSince_Time(&oldUntil, &now) / 3600 / 24; - if (days <= 30) { - appendCStr_String(str, - format_CStr(cstrCount_Lang("dlg.certwarn.mayberenewed.n", days), - cstrCollect_String(format_Date(&exp, "%Y-%m-%d")), - days)); - } - else { - appendCStr_String(str, cstr_Lang("dlg.certwarn.different")); + else { + if (d->showLinkNumbers && run->linkId && run->flags & decoration_GmRunFlag) { + const size_t ord = visibleLinkOrdinal_DocumentView_(d->view, run->linkId); + if (ord >= d->view->owner->ordinalBase) { + const iChar ordChar = + linkOrdinalChar_DocumentWidget_(d->view->owner, ord - d->view->owner->ordinalBase); + if (ordChar) { + const char *circle = "\u25ef"; /* Large Circle */ + const int circleFont = FONT_ID(default_FontId, regular_FontStyle, contentRegular_FontSize); + iRect nbArea = { init_I2(d->viewPos.x - gap_UI / 3, visPos.y), + init_I2(3.95f * gap_Text, 1.0f * lineHeight_Text(circleFont)) }; + drawRange_Text( + circleFont, topLeft_Rect(nbArea), tmQuote_ColorId, range_CStr(circle)); + iRect circleArea = visualBounds_Text(circleFont, range_CStr(circle)); + addv_I2(&circleArea.pos, topLeft_Rect(nbArea)); + drawCentered_Text(FONT_ID(default_FontId, regular_FontStyle, contentSmall_FontSize), + circleArea, + iTrue, + tmQuote_ColorId, + "%lc", + (int) ordChar); + goto runDrawn; + } } } - else if (certFlags & domainVerified_GmCertFlag) { - setCStr_String(title, get_GmError(tlsServerCertificateExpired_GmStatusCode)->title); - appendFormat_String(str, cstr_Lang("dlg.certwarn.expired"), - cstrCollect_String(format_Date(&d->certExpiry, "%Y-%m-%d"))); - } - else if (certFlags & timeVerified_GmCertFlag) { - appendFormat_String(str, cstr_Lang("dlg.certwarn.domain"), - cstr_String(d->certSubject)); - } - else { - appendCStr_String(str, cstr_Lang("dlg.certwarn.domain.expired")); - } - add_Banner(d->banner, warning_BannerType, none_GmStatusCode, title, str); - } - /* Warnings related to page contents. */ - const int dismissed = - value_SiteSpec(collectNewRange_String(urlRoot_String(d->mod.url)), - dismissWarnings_SiteSpecKey) | - (!prefs_App()->warnAboutMissingGlyphs ? missingGlyphs_GmDocumentWarning : 0); - const int warnings = warnings_GmDocument(d->view.doc) & ~dismissed; - if (warnings & missingGlyphs_GmDocumentWarning) { - add_Banner(d->banner, warning_BannerType, missingGlyphs_GmStatusCode, NULL, NULL); - /* TODO: List one or more of the missing characters and/or their Unicode blocks? */ - } - if (warnings & ansiEscapes_GmDocumentWarning) { - add_Banner(d->banner, warning_BannerType, ansiEscapes_GmStatusCode, NULL, NULL); - } -} - -static void updateFromCachedResponse_DocumentWidget_(iDocumentWidget *d, float normScrollY, - const iGmResponse *resp, iGmDocument *cachedDoc) { -// iAssert(width_Widget(d) > 0); /* must be laid out by now */ - setLinkNumberMode_DocumentWidget_(d, iFalse); - clear_ObjectList(d->media); - delete_Gempub(d->sourceGempub); - d->sourceGempub = NULL; - pauseAllPlayers_Media(media_GmDocument(d->view.doc), iTrue); - destroy_Widget(d->footerButtons); - d->footerButtons = NULL; - iRelease(d->view.doc); - d->view.doc = new_GmDocument(); - d->state = fetching_RequestState; - d->flags |= fromCache_DocumentWidgetFlag; - /* Do the fetch. */ { - d->initNormScrollY = normScrollY; - /* Use the cached response data. */ - updateTrust_DocumentWidget_(d, resp); - d->sourceTime = resp->when; - d->sourceStatus = success_GmStatusCode; - format_String(&d->sourceHeader, cstr_Lang("pageinfo.header.cached")); - set_Block(&d->sourceContent, &resp->body); - if (!cachedDoc) { - updateWidthAndRedoLayout_DocumentView_(&d->view); + if (run->flags & quoteBorder_GmRunFlag) { + drawVLine_Paint(&d->paint, + addX_I2(visPos, + !run->isRTL + ? -gap_Text * 5 / 2 + : (width_Rect(run->visBounds) + gap_Text * 5 / 2)), + height_Rect(run->visBounds), + tmQuoteIcon_ColorId); } - updateDocument_DocumentWidget_(d, resp, cachedDoc, iTrue); - clear_Banner(d->banner); - updateBanner_DocumentWidget_(d); - addBannerWarnings_DocumentWidget_(d); - } - d->state = ready_RequestState; - postProcessRequestContent_DocumentWidget_(d, iTrue); - resetScroll_DocumentView_(&d->view); - init_Anim(&d->view.scrollY.pos, d->initNormScrollY * pageHeight_DocumentView_(&d->view)); - updateVisible_DocumentView_(&d->view); - moveSpan_SmoothScroll(&d->view.scrollY, 0, 0); /* clamp position to new max */ - updateSideOpacity_DocumentView_(&d->view, iFalse); - cacheDocumentGlyphs_DocumentWidget_(d); - d->view.drawBufs->flags |= updateTimestampBuf_DrawBufsFlag | updateSideBuf_DrawBufsFlag; - d->flags &= ~(urlChanged_DocumentWidgetFlag | drawDownloadCounter_DocumentWidgetFlag); - postCommandf_Root( - as_Widget(d)->root, "document.changed doc:%p url:%s", d, cstr_String(d->mod.url)); -} - -static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) { - const iRecentUrl *recent = constMostRecentUrl_History(d->mod.history); - if (recent && recent->cachedResponse && equalCase_String(&recent->url, d->mod.url)) { - iChangeFlags(d->flags, - openedFromSidebar_DocumentWidgetFlag, - recent->flags.openedFromSidebar); - updateFromCachedResponse_DocumentWidget_( - d, recent->normScrollY, recent->cachedResponse, recent->cachedDoc); - if (!recent->cachedDoc) { - /* We have a cached copy now. */ - setCachedDocument_History(d->mod.history, d->view.doc, iFalse); + /* Base attributes. */ { + int f, c; + runBaseAttributes_GmDocument(doc, run, &f, &c); + setBaseAttributes_Text(f, c); } - return iTrue; - } - else if (!isEmpty_String(d->mod.url)) { - fetch_DocumentWidget_(d); - } - if (recent) { - /* Retain scroll position in refetched content as well. */ - d->initNormScrollY = recent->normScrollY; + drawBoundRange_Text(run->font, + visPos, + (run->isRTL ? -1 : 1) * width_Rect(run->visBounds), + fg, + run->text); + setBaseAttributes_Text(-1, -1); + runDrawn:; } - return iFalse; -} - -static void refreshWhileScrolling_DocumentWidget_(iAny *ptr) { - iAssert(isInstance_Object(ptr, &Class_DocumentWidget)); - iDocumentWidget *d = ptr; - iDocumentView *view = &d->view; - updateVisible_DocumentView_(view); - refresh_Widget(d); - if (view->animWideRunId) { - for (const iGmRun *r = view->animWideRunRange.start; r != view->animWideRunRange.end; r++) { - insert_PtrSet(view->invalidRuns, r); + /* Presentation of links. */ + if (run->linkId && ~run->flags & decoration_GmRunFlag) { + const int metaFont = paragraph_FontId; + /* TODO: Show status of an ongoing media request. */ + const int flags = linkFlags; + const iRect linkRect = moved_Rect(run->visBounds, origin); + iMediaRequest *mr = NULL; + /* Show metadata about inline content. */ + if (flags & content_GmLinkFlag && run->flags & endOfLine_GmRunFlag) { + fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart); + iString text; + init_String(&text); + const iMediaId linkMedia = findMediaForLink_Media(constMedia_GmDocument(doc), + run->linkId, none_MediaType); + iAssert(linkMedia.type != none_MediaType); + iGmMediaInfo info; + info_Media(constMedia_GmDocument(doc), linkMedia, &info); + switch (linkMedia.type) { + case image_MediaType: { + /* There's a separate decorative GmRun for the metadata. */ + break; + } + case audio_MediaType: + format_String(&text, "%s", info.type); + break; + case download_MediaType: + format_String(&text, "%s", info.type); + break; + default: + break; + } + if (linkMedia.type != download_MediaType && /* can't cancel downloads currently */ + linkMedia.type != image_MediaType && + findMediaRequest_DocumentWidget_(d->view->owner, run->linkId)) { + appendFormat_String( + &text, " %s" close_Icon, isHover ? escape_Color(tmLinkText_ColorId) : ""); + } + const iInt2 size = measureRange_Text(metaFont, range_String(&text)).bounds.size; + if (size.x) { + fillRect_Paint( + &d->paint, + (iRect){ add_I2(origin, addX_I2(topRight_Rect(run->bounds), -size.x - gap_UI)), + addX_I2(size, 2 * gap_UI) }, + tmBackground_ColorId); + drawAlign_Text(metaFont, + add_I2(topRight_Rect(run->bounds), origin), + fg, + right_Alignment, + "%s", cstr_String(&text)); + } + deinit_String(&text); } - } - if (isFinished_Anim(&view->animWideRunOffset)) { - view->animWideRunId = 0; - } - if (!isFinished_SmoothScroll(&view->scrollY) || !isFinished_Anim(&view->animWideRunOffset)) { - addTicker_App(refreshWhileScrolling_DocumentWidget_, d); - } - if (isFinished_SmoothScroll(&view->scrollY)) { - iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iFalse); - updateHover_DocumentView_(view, mouseCoord_Window(get_Window(), 0)); - } -} - -static void scrollBegan_DocumentWidget_(iAnyObject *any, int offset, uint32_t duration) { - iDocumentWidget *d = any; - /* Get rid of link numbers when scrolling. */ - if (offset && d->flags & showLinkNumbers_DocumentWidgetFlag) { - setLinkNumberMode_DocumentWidget_(d, iFalse); - invalidateVisibleLinks_DocumentView_(&d->view); - } - /* Show and hide toolbar on scroll. */ - if (deviceType_App() == phone_AppDeviceType) { - const float normPos = normScrollPos_DocumentView_(&d->view); - if (prefs_App()->hideToolbarOnScroll && iAbs(offset) > 5 && normPos >= 0) { - showToolbar_Root(as_Widget(d)->root, offset < 0); + else if (run->flags & endOfLine_GmRunFlag && + (mr = findMediaRequest_DocumentWidget_(d->view->owner, run->linkId)) != NULL) { + if (!isFinished_GmRequest(mr->req)) { + draw_Text(metaFont, + topRight_Rect(linkRect), + tmInlineContentMetadata_ColorId, + translateCStr_Lang(" \u2014 ${doc.fetching}\u2026 (%.1f ${mb})"), + (float) bodySize_GmRequest(mr->req) / 1.0e6f); + } } } - updateVisible_DocumentView_(&d->view); - refresh_Widget(as_Widget(d)); - if (duration > 0) { - iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iTrue); - addTicker_App(refreshWhileScrolling_DocumentWidget_, d); + if (0) { + drawRect_Paint(&d->paint, (iRect){ visPos, run->bounds.size }, green_ColorId); + drawRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, red_ColorId); } } -static void clampScroll_DocumentView_(iDocumentView *d) { - move_SmoothScroll(&d->scrollY, 0); +static int drawSideRect_(iPaint *p, iRect rect) { + int bg = tmBannerBackground_ColorId; + int fg = tmBannerIcon_ColorId; + if (equal_Color(get_Color(bg), get_Color(tmBackground_ColorId))) { + bg = tmBannerIcon_ColorId; + fg = tmBannerBackground_ColorId; + } + fillRect_Paint(p, rect, bg); + return fg; } -static void immediateScroll_DocumentView_(iDocumentView *d, int offset) { - move_SmoothScroll(&d->scrollY, offset); +static int sideElementAvailWidth_DocumentView_(const iDocumentView *d) { + return left_Rect(documentBounds_DocumentView_(d)) - + left_Rect(bounds_Widget(constAs_Widget(d->owner))) - 2 * d->pageMargin * gap_UI; } -static void smoothScroll_DocumentView_(iDocumentView *d, int offset, int duration) { - moveSpan_SmoothScroll(&d->scrollY, offset, duration); +static iBool isSideHeadingVisible_DocumentView_(const iDocumentView *d) { + return sideElementAvailWidth_DocumentView_(d) >= lineHeight_Text(banner_FontId) * 4.5f; } -static void scrollTo_DocumentView_(iDocumentView *d, int documentY, iBool centered) { - if (!isEmpty_Banner(d->owner->banner)) { - documentY += height_Banner(d->owner->banner) + documentTopPad_DocumentView_(d); - } - else { - documentY += documentTopPad_DocumentView_(d) + d->pageMargin * gap_UI; +static void updateSideIconBuf_DocumentView_(const iDocumentView *d) { + if (!isExposed_Window(get_Window())) { + return; } - init_Anim(&d->scrollY.pos, - documentY - (centered ? documentBounds_DocumentView_(d).size.y / 2 - : lineHeight_Text(paragraph_FontId))); - clampScroll_DocumentView_(d); -} - -static void scrollToHeading_DocumentView_(iDocumentView *d, const char *heading) { - iConstForEach(Array, h, headings_GmDocument(d->doc)) { - const iGmHeading *head = h.value; - if (startsWithCase_Rangecc(head->text, heading)) { - postCommandf_Root(as_Widget(d->owner)->root, "document.goto loc:%p", head->text.start); - break; - } + iDrawBufs *dbuf = d->drawBufs; + dbuf->flags &= ~updateSideBuf_DrawBufsFlag; + if (dbuf->sideIconBuf) { + SDL_DestroyTexture(dbuf->sideIconBuf); + dbuf->sideIconBuf = NULL; } -} - -static iBool scrollWideBlock_DocumentView_(iDocumentView *d, iInt2 mousePos, int delta, - int duration) { - if (delta == 0 || d->owner->flags & eitherWheelSwipe_DocumentWidgetFlag) { - return iFalse; + // const iGmRun *banner = siteBanner_GmDocument(d->doc); + if (isEmpty_Banner(d->owner->banner)) { + return; } - const iInt2 docPos = documentPos_DocumentView_(d, mousePos); - iConstForEach(PtrArray, i, &d->visibleWideRuns) { - const iGmRun *run = i.ptr; - if (docPos.y >= top_Rect(run->bounds) && docPos.y <= bottom_Rect(run->bounds)) { - /* We can scroll this run. First find out how much is allowed. */ - const iGmRunRange range = findPreformattedRange_GmDocument(d->doc, run); - int maxWidth = 0; - for (const iGmRun *r = range.start; r != range.end; r++) { - maxWidth = iMax(maxWidth, width_Rect(r->visBounds)); - } - const int maxOffset = maxWidth - documentWidth_DocumentView_(d) + d->pageMargin * gap_UI; - if (size_Array(&d->wideRunOffsets) <= preId_GmRun(run)) { - resize_Array(&d->wideRunOffsets, preId_GmRun(run) + 1); - } - int *offset = at_Array(&d->wideRunOffsets, preId_GmRun(run) - 1); - const int oldOffset = *offset; - *offset = iClamp(*offset + delta, 0, maxOffset); - /* Make sure the whole block gets redraw. */ - if (oldOffset != *offset) { - for (const iGmRun *r = range.start; r != range.end; r++) { - insert_PtrSet(d->invalidRuns, r); - } - refresh_Widget(d); - d->owner->selectMark = iNullRange; - d->owner->foundMark = iNullRange; - } - if (duration) { - if (d->animWideRunId != preId_GmRun(run) || isFinished_Anim(&d->animWideRunOffset)) { - d->animWideRunId = preId_GmRun(run); - init_Anim(&d->animWideRunOffset, oldOffset); - } - setValueEased_Anim(&d->animWideRunOffset, *offset, duration); - d->animWideRunRange = range; - addTicker_App(refreshWhileScrolling_DocumentWidget_, d); - } - else { - d->animWideRunId = 0; - init_Anim(&d->animWideRunOffset, 0); - } - return iTrue; + const int margin = gap_UI * d->pageMargin; + const int minBannerSize = lineHeight_Text(banner_FontId) * 2; + const iChar icon = siteIcon_GmDocument(d->doc); + const int avail = sideElementAvailWidth_DocumentView_(d) - margin; + iBool isHeadingVisible = isSideHeadingVisible_DocumentView_(d); + /* Determine the required size. */ + iInt2 bufSize = init1_I2(minBannerSize); + const int sideHeadingFont = FONT_ID(documentHeading_FontId, regular_FontStyle, contentBig_FontSize); + if (isHeadingVisible) { + const iInt2 headingSize = measureWrapRange_Text(sideHeadingFont, avail, + currentHeading_DocumentView_(d)).bounds.size; + if (headingSize.x > 0) { + bufSize.y += gap_Text + headingSize.y; + bufSize.x = iMax(bufSize.x, headingSize.x); + } + else { + isHeadingVisible = iFalse; } } - return iFalse; -} - -static void togglePreFold_DocumentWidget_(iDocumentWidget *d, uint16_t preId) { - d->view.hoverPre = NULL; - d->view.hoverAltPre = NULL; - d->selectMark = iNullRange; - foldPre_GmDocument(d->view.doc, preId); - redoLayout_GmDocument(d->view.doc); - clampScroll_DocumentView_(&d->view); - updateHover_DocumentView_(&d->view, mouseCoord_Window(get_Window(), 0)); - invalidate_DocumentWidget_(d); - refresh_Widget(as_Widget(d)); + SDL_Renderer *render = renderer_Window(get_Window()); + dbuf->sideIconBuf = SDL_CreateTexture(render, + SDL_PIXELFORMAT_RGBA4444, + SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET, + bufSize.x, bufSize.y); + iPaint p; + init_Paint(&p); + beginTarget_Paint(&p, dbuf->sideIconBuf); + const iColor back = get_Color(tmBannerSideTitle_ColorId); + SDL_SetRenderDrawColor(render, back.r, back.g, back.b, 0); /* better blending of the edge */ + SDL_RenderClear(render); + const iRect iconRect = { zero_I2(), init1_I2(minBannerSize) }; + int fg = drawSideRect_(&p, iconRect); + iString str; + initUnicodeN_String(&str, &icon, 1); + drawCentered_Text(banner_FontId, iconRect, iTrue, fg, "%s", cstr_String(&str)); + deinit_String(&str); + if (isHeadingVisible) { + iRangecc text = currentHeading_DocumentView_(d); + iInt2 pos = addY_I2(bottomLeft_Rect(iconRect), gap_Text); + const int font = sideHeadingFont; + drawWrapRange_Text(font, pos, avail, tmBannerSideTitle_ColorId, text); + } + endTarget_Paint(&p); + SDL_SetTextureBlendMode(dbuf->sideIconBuf, SDL_BLENDMODE_BLEND); } -static iString *makeQueryUrl_DocumentWidget_(const iDocumentWidget *d, - const iString *userEnteredText) { - iString *url = copy_String(d->mod.url); - /* Remove the existing query string. */ - const size_t qPos = indexOfCStr_String(url, "?"); - if (qPos != iInvalidPos) { - remove_Block(&url->chars, qPos, iInvalidSize); - } - appendCStr_String(url, "?"); - iString *cleaned = copy_String(userEnteredText); - if (deviceType_App() != desktop_AppDeviceType) { - trimEnd_String(cleaned); /* autocorrect may insert an extra space */ - if (isEmpty_String(cleaned)) { - set_String(cleaned, userEnteredText); /* user wanted just spaces? */ +static void drawSideElements_DocumentView_(const iDocumentView *d) { + const iWidget *w = constAs_Widget(d->owner); + const iRect bounds = bounds_Widget(w); + const iRect docBounds = documentBounds_DocumentView_(d); + const int margin = gap_UI * d->pageMargin; + float opacity = value_Anim(&d->sideOpacity); + const int avail = left_Rect(docBounds) - left_Rect(bounds) - 2 * margin; + iDrawBufs * dbuf = d->drawBufs; + iPaint p; + init_Paint(&p); + setClip_Paint(&p, boundsWithoutVisualOffset_Widget(w)); + /* Side icon and current heading. */ + if (prefs_App()->sideIcon && opacity > 0 && dbuf->sideIconBuf) { + const iInt2 texSize = size_SDLTexture(dbuf->sideIconBuf); + if (avail > texSize.x) { + const int minBannerSize = lineHeight_Text(banner_FontId) * 2; + iInt2 pos = addY_I2(add_I2(topLeft_Rect(bounds), init_I2(margin, 0)), + height_Rect(bounds) / 2 - minBannerSize / 2 - + (texSize.y > minBannerSize + ? (gap_Text + lineHeight_Text(heading3_FontId)) / 2 + : 0)); + SDL_SetTextureAlphaMod(dbuf->sideIconBuf, 255 * opacity); + SDL_RenderCopy(renderer_Window(get_Window()), + dbuf->sideIconBuf, NULL, + &(SDL_Rect){ pos.x, pos.y, texSize.x, texSize.y }); } } - append_String(url, collect_String(urlEncode_String(cleaned))); - delete_String(cleaned); - return url; -} - -static void inputQueryValidator_(iInputWidget *input, void *context) { - iDocumentWidget *d = context; - iString *url = makeQueryUrl_DocumentWidget_(d, text_InputWidget(input)); - iWidget *dlg = parent_Widget(input); - iLabelWidget *counter = findChild_Widget(dlg, "valueinput.counter"); - iAssert(counter); - int avail = 1024 - (int) size_String(url); - setFlags_Widget(findChild_Widget(dlg, "default"), disabled_WidgetFlag, avail < 0); - setEnterKeyEnabled_InputWidget(input, avail >= 0); - int len = length_String(text_InputWidget(input)); - if (len > 1024) { - iString *trunc = copy_String(text_InputWidget(input)); - truncate_String(trunc, 1024); - setText_InputWidget(input, trunc); - delete_String(trunc); + /* Reception timestamp. */ + if (dbuf->timestampBuf && dbuf->timestampBuf->size.x <= avail) { + draw_TextBuf( + dbuf->timestampBuf, + add_I2( + bottomLeft_Rect(bounds), + init_I2(margin, + -margin + -dbuf->timestampBuf->size.y + + iMax(0, d->scrollY.max - pos_SmoothScroll(&d->scrollY)))), + tmQuoteIcon_ColorId); } - setTextCStr_LabelWidget(counter, format_CStr("%d", avail)); /* Gemini URL maxlen */ - setTextColor_LabelWidget(counter, - avail < 0 ? uiTextCaution_ColorId : - avail < 128 ? uiTextStrong_ColorId - : uiTextDim_ColorId); - delete_String(url); - arrange_Widget(findChild_Widget(dlg, "dialogbuttons")); + unsetClip_Paint(&p); } -static const char *humanReadableStatusCode_(enum iGmStatusCode code) { - if (code <= 0) { - return ""; +static void drawMedia_DocumentView_(const iDocumentView *d, iPaint *p) { + iConstForEach(PtrArray, i, &d->visibleMedia) { + const iGmRun * run = i.ptr; + if (run->mediaType == audio_MediaType) { + iPlayerUI ui; + init_PlayerUI(&ui, + audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run)), + runRect_DocumentView_(d, run)); + draw_PlayerUI(&ui, p); + } + else if (run->mediaType == download_MediaType) { + iDownloadUI ui; + init_DownloadUI(&ui, constMedia_GmDocument(d->doc), run->mediaId, + runRect_DocumentView_(d, run)); + draw_DownloadUI(&ui, p); + } } - return format_CStr("%d ", code); } -static void checkResponse_DocumentWidget_(iDocumentWidget *d) { - if (!d->request) { - return; - } - enum iGmStatusCode statusCode = status_GmRequest(d->request); - if (statusCode == none_GmStatusCode) { - return; +static void extend_GmRunRange_(iGmRunRange *runs) { + if (runs->start) { + runs->start--; + runs->end++; } - iGmResponse *resp = lockResponse_GmRequest(d->request); - if (d->state == fetching_RequestState) { - d->state = receivedPartialResponse_RequestState; - d->flags &= ~fromCache_DocumentWidgetFlag; - updateTrust_DocumentWidget_(d, resp); - if (isSuccess_GmStatusCode(statusCode)) { - clear_Banner(d->banner); - updateTheme_DocumentWidget_(d); - } - if (~d->certFlags & trusted_GmCertFlag && - isSuccess_GmStatusCode(statusCode) && - equalCase_Rangecc(urlScheme_String(d->mod.url), "gemini")) { - statusCode = tlsServerCertificateNotVerified_GmStatusCode; - } - init_Anim(&d->view.sideOpacity, 0); - init_Anim(&d->view.altTextOpacity, 0); - format_String(&d->sourceHeader, - "%s%s", - humanReadableStatusCode_(statusCode), - isEmpty_String(&resp->meta) && !isSuccess_GmStatusCode(statusCode) - ? get_GmError(statusCode)->title - : cstr_String(&resp->meta)); - d->sourceStatus = statusCode; - switch (category_GmStatusCode(statusCode)) { - case categoryInput_GmStatusCode: { - /* Let the navigation history know that we have been to this URL even though - it is only displayed as an input dialog. */ - visitUrl_Visited(visited_App(), d->mod.url, transient_VisitedUrlFlag); - iUrl parts; - init_Url(&parts, d->mod.url); - iWidget *dlg = makeValueInput_Widget( - as_Widget(d), - NULL, - format_CStr(uiHeading_ColorEscape "%s", cstr_Rangecc(parts.host)), - isEmpty_String(&resp->meta) - ? format_CStr(cstr_Lang("dlg.input.prompt"), cstr_Rangecc(parts.path)) - : cstr_String(&resp->meta), - uiTextCaution_ColorEscape "${dlg.input.send}", - format_CStr("!document.input.submit doc:%p", d)); - iWidget *buttons = findChild_Widget(dlg, "dialogbuttons"); - iLabelWidget *lineBreak = NULL; - if (statusCode != sensitiveInput_GmStatusCode) { - /* The line break and URL length counters are positioned differently on mobile. - There is no line breaks in sensitive input. */ - if (deviceType_App() == desktop_AppDeviceType) { - iString *keyStr = collectNew_String(); - toString_Sym(SDLK_RETURN, - lineBreakKeyMod_ReturnKeyBehavior(prefs_App()->returnKey), - keyStr); - lineBreak = new_LabelWidget( - format_CStr("${dlg.input.linebreak}" uiTextAction_ColorEscape " %s", - cstr_String(keyStr)), - NULL); - insertChildAfter_Widget(buttons, iClob(lineBreak), 0); - } - else { -#if !defined (iPlatformAppleMobile) - lineBreak = new_LabelWidget("${dlg.input.linebreak}", "text.insert arg:10"); -#endif - } - if (lineBreak) { - setFlags_Widget(as_Widget(lineBreak), frameless_WidgetFlag, iTrue); - setTextColor_LabelWidget(lineBreak, uiTextDim_ColorId); +} + +static iBool render_DocumentView_(const iDocumentView *d, iDrawContext *ctx, iBool prerenderExtra) { + iBool didDraw = iFalse; + const iRect bounds = bounds_Widget(constAs_Widget(d->owner)); + const iRect ctxWidgetBounds = + init_Rect(0, + 0, + width_Rect(bounds) - constAs_Widget(d->owner->scroll)->rect.size.x, + height_Rect(bounds)); + const iRangei full = { 0, size_GmDocument(d->doc).y }; + const iRangei vis = ctx->vis; + iVisBuf *visBuf = d->visBuf; /* will be updated now */ + d->drawBufs->lastRenderTime = SDL_GetTicks(); + /* Swap buffers around to have room available both before and after the visible region. */ + allocVisBuffer_DocumentView_(d); + reposition_VisBuf(visBuf, vis); + /* Redraw the invalid ranges. */ + if (~flags_Widget(constAs_Widget(d->owner)) & destroyPending_WidgetFlag) { + iPaint *p = &ctx->paint; + init_Paint(p); + iForIndices(i, visBuf->buffers) { + iVisBufTexture *buf = &visBuf->buffers[i]; + iVisBufMeta *meta = buf->user; + const iRangei bufRange = intersect_Rangei(bufferRange_VisBuf(visBuf, i), full); + const iRangei bufVisRange = intersect_Rangei(bufRange, vis); + ctx->widgetBounds = moved_Rect(ctxWidgetBounds, init_I2(0, -buf->origin)); + ctx->viewPos = init_I2(left_Rect(ctx->docBounds) - left_Rect(bounds), -buf->origin); + // printf(" buffer %zu: buf vis range %d...%d\n", i, bufVisRange.start, bufVisRange.end); + if (!prerenderExtra && !isEmpty_Range(&bufVisRange)) { + didDraw = iTrue; + if (isEmpty_Rangei(buf->validRange)) { + /* Fill the required currently visible range (vis). */ + const iRangei bufVisRange = intersect_Rangei(bufRange, vis); + if (!isEmpty_Range(&bufVisRange)) { + beginTarget_Paint(p, buf->texture); + fillRect_Paint(p, (iRect){ zero_I2(), visBuf->texSize }, tmBackground_ColorId); + iZap(ctx->runsDrawn); + render_GmDocument(d->doc, bufVisRange, drawRun_DrawContext_, ctx); + meta->runsDrawn = ctx->runsDrawn; + extend_GmRunRange_(&meta->runsDrawn); + buf->validRange = bufVisRange; + // printf(" buffer %zu valid %d...%d\n", i, bufRange.start, bufRange.end); } } - iWidget *counter = (iWidget *) new_LabelWidget("", NULL); - setId_Widget(counter, "valueinput.counter"); - setFlags_Widget(counter, frameless_WidgetFlag | resizeToParentHeight_WidgetFlag, iTrue); - if (deviceType_App() == desktop_AppDeviceType) { - addChildPos_Widget(buttons, iClob(counter), front_WidgetAddPos); - } else { - insertChildAfter_Widget(buttons, iClob(counter), 1); - } - if (lineBreak && deviceType_App() != desktop_AppDeviceType) { - addChildPos_Widget(buttons, iClob(lineBreak), front_WidgetAddPos); - } - /* Menu for additional actions, past entries. */ { - iMenuItem items[] = { { "${menu.input.precedingline}", - SDLK_v, - KMOD_PRIMARY | KMOD_SHIFT, - format_CStr("!valueinput.set ptr:%p text:%s", - buttons, - cstr_String(&d->linePrecedingLink)) } }; - iLabelWidget *menu = makeMenuButton_LabelWidget(midEllipsis_Icon, items, 1); - if (deviceType_App() == desktop_AppDeviceType) { - addChildPos_Widget(buttons, iClob(menu), front_WidgetAddPos); + /* Progressively fill the required runs. */ + if (meta->runsDrawn.start) { + beginTarget_Paint(p, buf->texture); + meta->runsDrawn.start = renderProgressive_GmDocument(d->doc, meta->runsDrawn.start, + -1, iInvalidSize, + bufVisRange, + drawRun_DrawContext_, + ctx); + buf->validRange.start = bufVisRange.start; } - else { - insertChildAfterFlags_Widget(buttons, iClob(menu), 0, - frameless_WidgetFlag | noBackground_WidgetFlag); - setFont_LabelWidget(menu, font_LabelWidget((iLabelWidget *) lastChild_Widget(buttons))); - setTextColor_LabelWidget(menu, uiTextAction_ColorId); + if (meta->runsDrawn.end) { + beginTarget_Paint(p, buf->texture); + meta->runsDrawn.end = renderProgressive_GmDocument(d->doc, meta->runsDrawn.end, + +1, iInvalidSize, + bufVisRange, + drawRun_DrawContext_, + ctx); + buf->validRange.end = bufVisRange.end; } - } - setValidator_InputWidget(findChild_Widget(dlg, "input"), inputQueryValidator_, d); - setSensitiveContent_InputWidget(findChild_Widget(dlg, "input"), - statusCode == sensitiveInput_GmStatusCode); - if (document_App() != d) { - postCommandf_App("tabs.switch page:%p", d); } - else { - updateTheme_DocumentWidget_(d); - } - break; } - case categorySuccess_GmStatusCode: - if (d->flags & urlChanged_DocumentWidgetFlag) { - /* Keep scroll position when reloading the same page. */ - resetScroll_DocumentView_(&d->view); - } - d->view.scrollY.pullActionTriggered = 0; - pauseAllPlayers_Media(media_GmDocument(d->view.doc), iTrue); - iReleasePtr(&d->view.doc); /* new content incoming */ - delete_Gempub(d->sourceGempub); - d->sourceGempub = NULL; - destroy_Widget(d->footerButtons); - d->footerButtons = NULL; - d->view.doc = new_GmDocument(); - resetWideRuns_DocumentView_(&d->view); - updateDocument_DocumentWidget_(d, resp, NULL, iTrue); - break; - case categoryRedirect_GmStatusCode: - if (isEmpty_String(&resp->meta)) { - showErrorPage_DocumentWidget_(d, invalidRedirect_GmStatusCode, NULL); + /* Progressively draw the rest of the buffer if it isn't fully valid. */ + if (prerenderExtra && !equal_Rangei(bufRange, buf->validRange)) { + const iGmRun *next; + // printf("%zu: prerenderExtra (start:%p end:%p)\n", i, meta->runsDrawn.start, meta->runsDrawn.end); + if (meta->runsDrawn.start == NULL) { + /* Haven't drawn anything yet in this buffer, so let's try seeding it. */ + const int rh = lineHeight_Text(paragraph_FontId); + const int y = i >= iElemCount(visBuf->buffers) / 2 ? bufRange.start : (bufRange.end - rh); + beginTarget_Paint(p, buf->texture); + fillRect_Paint(p, (iRect){ zero_I2(), visBuf->texSize }, tmBackground_ColorId); + buf->validRange = (iRangei){ y, y + rh }; + iZap(ctx->runsDrawn); + render_GmDocument(d->doc, buf->validRange, drawRun_DrawContext_, ctx); + meta->runsDrawn = ctx->runsDrawn; + extend_GmRunRange_(&meta->runsDrawn); + // printf("%zu: seeded, next %p:%p\n", i, meta->runsDrawn.start, meta->runsDrawn.end); + didDraw = iTrue; } else { - /* Only accept redirects that use gemini scheme. */ - const iString *dstUrl = absoluteUrl_String(d->mod.url, &resp->meta); - const iRangecc srcScheme = urlScheme_String(d->mod.url); - const iRangecc dstScheme = urlScheme_String(dstUrl); - if (d->redirectCount >= 5) { - showErrorPage_DocumentWidget_(d, tooManyRedirects_GmStatusCode, dstUrl); - } - /* Redirects with the same scheme are automatic, and switching automatically - between "gemini" and "titan" is allowed. */ - else if (equalRangeCase_Rangecc(dstScheme, srcScheme) || - (equalCase_Rangecc(srcScheme, "titan") && - equalCase_Rangecc(dstScheme, "gemini")) || - (equalCase_Rangecc(srcScheme, "gemini") && - equalCase_Rangecc(dstScheme, "titan"))) { - visitUrl_Visited(visited_App(), d->mod.url, transient_VisitedUrlFlag); - postCommandf_Root(as_Widget(d)->root, - "open doc:%p redirect:%d url:%s", d, d->redirectCount + 1, cstr_String(dstUrl)); + if (meta->runsDrawn.start) { + const iRangei upper = intersect_Rangei(bufRange, (iRangei){ full.start, buf->validRange.start }); + if (upper.end > upper.start) { + beginTarget_Paint(p, buf->texture); + next = renderProgressive_GmDocument(d->doc, meta->runsDrawn.start, + -1, 1, upper, + drawRun_DrawContext_, + ctx); + if (next && meta->runsDrawn.start != next) { + meta->runsDrawn.start = next; + buf->validRange.start = bottom_Rect(next->visBounds); + didDraw = iTrue; + } + else { + buf->validRange.start = bufRange.start; + } + } } - else { - /* Scheme changes must be manually approved. */ - showErrorPage_DocumentWidget_(d, schemeChangeRedirect_GmStatusCode, dstUrl); + if (!didDraw && meta->runsDrawn.end) { + const iRangei lower = intersect_Rangei(bufRange, (iRangei){ buf->validRange.end, full.end }); + if (lower.end > lower.start) { + beginTarget_Paint(p, buf->texture); + next = renderProgressive_GmDocument(d->doc, meta->runsDrawn.end, + +1, 1, lower, + drawRun_DrawContext_, + ctx); + if (next && meta->runsDrawn.end != next) { + meta->runsDrawn.end = next; + buf->validRange.end = top_Rect(next->visBounds); + didDraw = iTrue; + } + else { + buf->validRange.end = bufRange.end; + } + } } - unlockResponse_GmRequest(d->request); - iReleasePtr(&d->request); - } - break; - default: - if (isDefined_GmError(statusCode)) { - showErrorPage_DocumentWidget_(d, statusCode, &resp->meta); - } - else if (category_GmStatusCode(statusCode) == - categoryTemporaryFailure_GmStatusCode) { - showErrorPage_DocumentWidget_( - d, temporaryFailure_GmStatusCode, &resp->meta); } - else if (category_GmStatusCode(statusCode) == - categoryPermanentFailure_GmStatusCode) { - showErrorPage_DocumentWidget_( - d, permanentFailure_GmStatusCode, &resp->meta); + } + /* Draw any invalidated runs that fall within this buffer. */ + if (!prerenderExtra) { + const iRangei bufRange = { buf->origin, buf->origin + visBuf->texSize.y }; + /* Clear full-width backgrounds first in case there are any dynamic elements. */ { + iConstForEach(PtrSet, r, d->invalidRuns) { + const iGmRun *run = *r.value; + if (isOverlapping_Rangei(bufRange, ySpan_Rect(run->visBounds))) { + beginTarget_Paint(p, buf->texture); + fillRect_Paint(p, + init_Rect(0, + run->visBounds.pos.y - buf->origin, + visBuf->texSize.x, + run->visBounds.size.y), + tmBackground_ColorId); + } + } } - else { - showErrorPage_DocumentWidget_(d, unknownStatusCode_GmStatusCode, &resp->meta); + setAnsiFlags_Text(ansiEscapes_GmDocument(d->doc)); + iConstForEach(PtrSet, r, d->invalidRuns) { + const iGmRun *run = *r.value; + if (isOverlapping_Rangei(bufRange, ySpan_Rect(run->visBounds))) { + beginTarget_Paint(p, buf->texture); + drawRun_DrawContext_(ctx, run); + } } + setAnsiFlags_Text(allowAll_AnsiFlag); + } + endTarget_Paint(p); + if (prerenderExtra && didDraw) { + /* Just a run at a time. */ break; + } } - } - else if (d->state == receivedPartialResponse_RequestState) { - d->flags &= ~fromCache_DocumentWidgetFlag; - switch (category_GmStatusCode(statusCode)) { - case categorySuccess_GmStatusCode: - /* More content available. */ - updateDocument_DocumentWidget_(d, resp, NULL, iFalse); - break; - default: - break; + if (!prerenderExtra) { + clear_PtrSet(d->invalidRuns); } } - unlockResponse_GmRequest(d->request); + return didDraw; } -static iRangecc sourceLoc_DocumentView_(const iDocumentView *d, iInt2 pos) { - return findLoc_GmDocument(d->doc, documentPos_DocumentView_(d, pos)); +static void draw_DocumentView_(const iDocumentView *d) { + const iWidget *w = constAs_Widget(d->owner); + const iRect bounds = bounds_Widget(w); + const iRect boundsWithoutVisOff = boundsWithoutVisualOffset_Widget(w); + const iRect clipBounds = intersect_Rect(bounds, boundsWithoutVisOff); + /* Each document has its own palette, but the drawing routines rely on a global one. + As we're now drawing a document, ensure that the right palette is in effect. + Document theme colors can be used elsewhere, too, but first a document's palette + must be made global. */ + makePaletteGlobal_GmDocument(d->doc); + if (d->drawBufs->flags & updateTimestampBuf_DrawBufsFlag) { + updateTimestampBuf_DocumentView_(d); + } + if (d->drawBufs->flags & updateSideBuf_DrawBufsFlag) { + updateSideIconBuf_DocumentView_(d); + } + const iRect docBounds = documentBounds_DocumentView_(d); + const iRangei vis = visibleRange_DocumentView_(d); + iDrawContext ctx = { + .view = d, + .docBounds = docBounds, + .vis = vis, + .showLinkNumbers = (d->owner->flags & showLinkNumbers_DocumentWidgetFlag) != 0, + }; + init_Paint(&ctx.paint); + render_DocumentView_(d, &ctx, iFalse /* just the mandatory parts */); + iBanner *banner = d->owner->banner; + int yTop = docBounds.pos.y + viewPos_DocumentView_(d); + const iBool isDocEmpty = size_GmDocument(d->doc).y == 0; + const iBool isTouchSelecting = (flags_Widget(w) & touchDrag_WidgetFlag) != 0; + if (!isDocEmpty || !isEmpty_Banner(banner)) { + const int docBgColor = isDocEmpty ? tmBannerBackground_ColorId : tmBackground_ColorId; + setClip_Paint(&ctx.paint, clipBounds); + if (!isDocEmpty) { + draw_VisBuf(d->visBuf, init_I2(bounds.pos.x, yTop), ySpan_Rect(bounds)); + } + /* Text markers. */ + if (!isEmpty_Range(&d->owner->foundMark) || !isEmpty_Range(&d->owner->selectMark)) { + SDL_Renderer *render = renderer_Window(get_Window()); + ctx.firstMarkRect = zero_Rect(); + ctx.lastMarkRect = zero_Rect(); + SDL_SetRenderDrawBlendMode(render, + isDark_ColorTheme(colorTheme_App()) ? SDL_BLENDMODE_ADD + : SDL_BLENDMODE_BLEND); + ctx.viewPos = topLeft_Rect(docBounds); + /* Marker starting outside the visible range? */ + if (d->visibleRuns.start) { + if (!isEmpty_Range(&d->owner->selectMark) && + d->owner->selectMark.start < d->visibleRuns.start->text.start && + d->owner->selectMark.end > d->visibleRuns.start->text.start) { + ctx.inSelectMark = iTrue; + } + if (isEmpty_Range(&d->owner->foundMark) && + d->owner->foundMark.start < d->visibleRuns.start->text.start && + d->owner->foundMark.end > d->visibleRuns.start->text.start) { + ctx.inFoundMark = iTrue; + } + } + render_GmDocument(d->doc, vis, drawMark_DrawContext_, &ctx); + SDL_SetRenderDrawBlendMode(render, SDL_BLENDMODE_NONE); + /* Selection range pins. */ + if (isTouchSelecting) { + drawPin_Paint(&ctx.paint, ctx.firstMarkRect, 0, tmQuote_ColorId); + drawPin_Paint(&ctx.paint, ctx.lastMarkRect, 1, tmQuote_ColorId); + } + } + drawMedia_DocumentView_(d, &ctx.paint); + /* Fill the top and bottom, in case the document is short. */ + if (yTop > top_Rect(bounds)) { + fillRect_Paint(&ctx.paint, + (iRect){ bounds.pos, init_I2(bounds.size.x, yTop - top_Rect(bounds)) }, + !isEmpty_Banner(banner) ? tmBannerBackground_ColorId + : docBgColor); + } + /* Banner. */ + if (!isDocEmpty || numItems_Banner(banner) > 0) { + /* Fill the part between the banner and the top of the document. */ + fillRect_Paint(&ctx.paint, + (iRect){ init_I2(left_Rect(bounds), + top_Rect(docBounds) + viewPos_DocumentView_(d) - + documentTopPad_DocumentView_(d)), + init_I2(bounds.size.x, documentTopPad_DocumentView_(d)) }, + docBgColor); + setPos_Banner(banner, addY_I2(topLeft_Rect(docBounds), + -pos_SmoothScroll(&d->scrollY))); + draw_Banner(banner); + } + const int yBottom = yTop + size_GmDocument(d->doc).y; + if (yBottom < bottom_Rect(bounds)) { + fillRect_Paint(&ctx.paint, + init_Rect(bounds.pos.x, yBottom, bounds.size.x, bottom_Rect(bounds) - yBottom), + !isDocEmpty ? docBgColor : tmBannerBackground_ColorId); + } + unsetClip_Paint(&ctx.paint); + drawSideElements_DocumentView_(d); + /* Alt text. */ + const float altTextOpacity = value_Anim(&d->altTextOpacity) * 6 - 5; + if (d->hoverAltPre && altTextOpacity > 0) { + const iGmPreMeta *meta = preMeta_GmDocument(d->doc, preId_GmRun(d->hoverAltPre)); + if (meta->flags & topLeft_GmPreMetaFlag && ~meta->flags & decoration_GmRunFlag && + !isEmpty_Range(&meta->altText)) { + const int margin = 3 * gap_UI / 2; + const int altFont = uiLabel_FontId; + const int wrap = docBounds.size.x - 2 * margin; + iInt2 pos = addY_I2(add_I2(docBounds.pos, meta->pixelRect.pos), + viewPos_DocumentView_(d)); + const iInt2 textSize = measureWrapRange_Text(altFont, wrap, meta->altText).bounds.size; + pos.y -= textSize.y + gap_UI; + pos.y = iMax(pos.y, top_Rect(bounds)); + const iRect altRect = { pos, init_I2(docBounds.size.x, textSize.y) }; + ctx.paint.alpha = altTextOpacity * 255; + if (altTextOpacity < 1) { + SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND); + } + fillRect_Paint(&ctx.paint, altRect, tmBackgroundAltText_ColorId); + drawRect_Paint(&ctx.paint, altRect, tmFrameAltText_ColorId); + setOpacity_Text(altTextOpacity); + drawWrapRange_Text(altFont, addX_I2(pos, margin), wrap, + tmQuote_ColorId, meta->altText); + SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE); + setOpacity_Text(1.0f); + } + } + /* Touch selection indicator. */ + if (isTouchSelecting) { + iRect rect = { topLeft_Rect(bounds), + init_I2(width_Rect(bounds), lineHeight_Text(uiLabelBold_FontId)) }; + fillRect_Paint(&ctx.paint, rect, uiTextAction_ColorId); + const iRangecc mark = selectMark_DocumentWidget_(d->owner); + drawCentered_Text(uiLabelBold_FontId, + rect, + iFalse, + uiBackground_ColorId, + "%zu bytes selected", /* TODO: i18n */ + size_Range(&mark)); + } + } } -iDeclareType(MiddleRunParams) +/*----------------------------------------------------------------------------------------------*/ -struct Impl_MiddleRunParams { - int midY; - const iGmRun *closest; - int distance; -}; +static void enableActions_DocumentWidget_(iDocumentWidget *d, iBool enable) { + /* Actions are invisible child widgets of the DocumentWidget. */ + iForEach(ObjectList, i, children_Widget(d)) { + if (isAction_Widget(i.object)) { + setFlags_Widget(i.object, disabled_WidgetFlag, !enable); + } + } +} -static void find_MiddleRunParams_(void *params, const iGmRun *run) { - iMiddleRunParams *d = params; - if (isEmpty_Rect(run->bounds)) { - return; +static void setLinkNumberMode_DocumentWidget_(iDocumentWidget *d, iBool set) { + iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, set); + /* Children have priority when handling events. */ + enableActions_DocumentWidget_(d, !set); + if (d->menu) { + setFlags_Widget(d->menu, disabled_WidgetFlag, set); } - const int distance = iAbs(mid_Rect(run->bounds).y - d->midY); - if (!d->closest || distance < d->distance) { - d->closest = run; - d->distance = distance; +} + +static void requestUpdated_DocumentWidget_(iAnyObject *obj) { + iDocumentWidget *d = obj; + const int wasUpdated = exchange_Atomic(&d->isRequestUpdated, iTrue); + if (!wasUpdated) { + postCommand_Widget(obj, + "document.request.updated doc:%p reqid:%u request:%p", + d, + id_GmRequest(d->request), + d->request); } } -static const iGmRun *middleRun_DocumentView_(const iDocumentView *d) { - iRangei visRange = visibleRange_DocumentView_(d); - iMiddleRunParams params = { (visRange.start + visRange.end) / 2, NULL, 0 }; - render_GmDocument(d->doc, visRange, find_MiddleRunParams_, ¶ms); - return params.closest; +static void requestFinished_DocumentWidget_(iAnyObject *obj) { + iDocumentWidget *d = obj; + postCommand_Widget(obj, + "document.request.finished doc:%p reqid:%u request:%p", + d, + id_GmRequest(d->request), + d->request); } -static void removeMediaRequest_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId) { - iForEach(ObjectList, i, d->media) { - iMediaRequest *req = (iMediaRequest *) i.object; - if (req->linkId == linkId) { - remove_ObjectListIterator(&i); - break; - } +static void animate_DocumentWidget_(void *ticker) { + iDocumentWidget *d = ticker; + iAssert(isInstance_Object(d, &Class_DocumentWidget)); + refresh_Widget(d); + if (!isFinished_Anim(&d->view.sideOpacity) || !isFinished_Anim(&d->view.altTextOpacity) || + (d->linkInfo && !isFinished_Anim(&d->linkInfo->opacity))) { + addTicker_App(animate_DocumentWidget_, d); } } -static iMediaRequest *findMediaRequest_DocumentWidget_(const iDocumentWidget *d, iGmLinkId linkId) { - iConstForEach(ObjectList, i, d->media) { - const iMediaRequest *req = (const iMediaRequest *) i.object; - if (req->linkId == linkId) { - return iConstCast(iMediaRequest *, req); - } +static uint32_t mediaUpdateInterval_DocumentWidget_(const iDocumentWidget *d) { + if (document_App() != d) { + return 0; } - return NULL; -} - -static iBool requestMedia_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId, iBool enableFilters) { - if (!findMediaRequest_DocumentWidget_(d, linkId)) { - const iString *mediaUrl = absoluteUrl_String(d->mod.url, linkUrl_GmDocument(d->view.doc, linkId)); - pushBack_ObjectList(d->media, iClob(new_MediaRequest(d, linkId, mediaUrl, enableFilters))); - invalidate_DocumentWidget_(d); - return iTrue; + if (as_MainWindow(window_Widget(d))->isDrawFrozen) { + return 0; } - return iFalse; + static const uint32_t invalidInterval_ = ~0u; + uint32_t interval = invalidInterval_; + iConstForEach(PtrArray, i, &d->view.visibleMedia) { + const iGmRun *run = i.ptr; + if (run->mediaType == audio_MediaType) { + iPlayer *plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run)); + if (flags_Player(plr) & adjustingVolume_PlayerFlag || + (isStarted_Player(plr) && !isPaused_Player(plr))) { + interval = iMin(interval, 1000 / 15); + } + } + else if (run->mediaType == download_MediaType) { + interval = iMin(interval, 1000); + } + } + return interval != invalidInterval_ ? interval : 0; } -static iBool isDownloadRequest_DocumentWidget(const iDocumentWidget *d, const iMediaRequest *req) { - return findMediaForLink_Media(constMedia_GmDocument(d->view.doc), req->linkId, download_MediaType).type != 0; +static uint32_t postMediaUpdate_DocumentWidget_(uint32_t interval, void *context) { + /* Called in timer thread; don't access the widget. */ + iUnused(context); + postCommand_App("media.player.update"); + return interval; } -static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) { - iMediaRequest *req = pointerLabel_Command(cmd, "request"); - iBool isOurRequest = iFalse; - /* This request may already be deleted so treat the pointer with caution. */ - iConstForEach(ObjectList, m, d->media) { - if (m.object == req) { - isOurRequest = iTrue; - break; - } - } - if (!isOurRequest) { - return iFalse; - } - if (equal_Command(cmd, "media.updated")) { - /* Pass new data to media players. */ - const enum iGmStatusCode code = status_GmRequest(req->req); - if (isSuccess_GmStatusCode(code)) { - iGmResponse *resp = lockResponse_GmRequest(req->req); - if (isDownloadRequest_DocumentWidget(d, req) || - startsWith_String(&resp->meta, "audio/")) { - /* TODO: Use a helper? This is same as below except for the partialData flag. */ - if (setData_Media(media_GmDocument(d->view.doc), - req->linkId, - &resp->meta, - &resp->body, - partialData_MediaFlag | allowHide_MediaFlag)) { - redoLayout_GmDocument(d->view.doc); +static void updateMedia_DocumentWidget_(iDocumentWidget *d) { + if (document_App() == d) { + refresh_Widget(d); + iConstForEach(PtrArray, i, &d->view.visibleMedia) { + const iGmRun *run = i.ptr; + if (run->mediaType == audio_MediaType) { + iPlayer *plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run)); + if (idleTimeMs_Player(plr) > 3000 && ~flags_Player(plr) & volumeGrabbed_PlayerFlag && + flags_Player(plr) & adjustingVolume_PlayerFlag) { + setFlags_Player(plr, adjustingVolume_PlayerFlag, iFalse); } - updateVisible_DocumentView_(&d->view); - invalidate_DocumentWidget_(d); - refresh_Widget(as_Widget(d)); } - unlockResponse_GmRequest(req->req); } - /* Update the link's progress. */ - invalidateLink_DocumentView_(&d->view, req->linkId); - refresh_Widget(d); - return iTrue; } - else if (equal_Command(cmd, "media.finished")) { - const enum iGmStatusCode code = status_GmRequest(req->req); - /* Give the media to the document for presentation. */ - if (isSuccess_GmStatusCode(code)) { - if (isDownloadRequest_DocumentWidget(d, req) || - startsWith_String(meta_GmRequest(req->req), "image/") || - startsWith_String(meta_GmRequest(req->req), "audio/")) { - setData_Media(media_GmDocument(d->view.doc), - req->linkId, - meta_GmRequest(req->req), - body_GmRequest(req->req), - allowHide_MediaFlag); - redoLayout_GmDocument(d->view.doc); - iZap(d->view.visibleRuns); /* pointers invalidated */ - updateVisible_DocumentView_(&d->view); - invalidate_DocumentWidget_(d); - refresh_Widget(as_Widget(d)); - } - } - else { - const iGmError *err = get_GmError(code); - makeSimpleMessage_Widget(format_CStr(uiTextCaution_ColorEscape "%s", err->title), err->info); - removeMediaRequest_DocumentWidget_(d, req->linkId); - } - return iTrue; + if (d->mediaTimer && mediaUpdateInterval_DocumentWidget_(d) == 0) { + SDL_RemoveTimer(d->mediaTimer); + d->mediaTimer = 0; } - return iFalse; } -static void allocVisBuffer_DocumentView_(const iDocumentView *d) { - const iWidget *w = constAs_Widget(d->owner); - const iBool isVisible = isVisible_Widget(w); - const iInt2 size = bounds_Widget(w).size; - if (isVisible) { - alloc_VisBuf(d->visBuf, size, 1); +static void animateMedia_DocumentWidget_(iDocumentWidget *d) { + if (document_App() != d) { + if (d->mediaTimer) { + SDL_RemoveTimer(d->mediaTimer); + d->mediaTimer = 0; + } + return; } - else { - dealloc_VisBuf(d->visBuf); + uint32_t interval = mediaUpdateInterval_DocumentWidget_(d); + if (interval && !d->mediaTimer) { + d->mediaTimer = SDL_AddTimer(interval, postMediaUpdate_DocumentWidget_, d); } } -static iBool fetchNextUnfetchedImage_DocumentWidget_(iDocumentWidget *d) { - iConstForEach(PtrArray, i, &d->view.visibleLinks) { - const iGmRun *run = i.ptr; - if (run->linkId && run->mediaType == none_MediaType && - ~run->flags & decoration_GmRunFlag) { - const int linkFlags = linkFlags_GmDocument(d->view.doc, run->linkId); - if (isMediaLink_GmDocument(d->view.doc, run->linkId) && - linkFlags & imageFileExtension_GmLinkFlag && - ~linkFlags & content_GmLinkFlag && ~linkFlags & permanent_GmLinkFlag ) { - if (requestMedia_DocumentWidget_(d, run->linkId, iTrue)) { - return iTrue; - } +static void updateWindowTitle_DocumentWidget_(const iDocumentWidget *d) { + iLabelWidget *tabButton = tabPageButton_Widget(findChild_Widget(root_Widget(constAs_Widget(d)), + "doctabs"), d); + if (!tabButton) { + /* Not part of the UI at the moment. */ + return; + } + iStringArray *title = iClob(new_StringArray()); + if (!isEmpty_String(title_GmDocument(d->view.doc))) { + pushBack_StringArray(title, title_GmDocument(d->view.doc)); + } + if (!isEmpty_String(d->titleUser)) { + pushBack_StringArray(title, d->titleUser); + } + else { + iUrl parts; + init_Url(&parts, d->mod.url); + if (equalCase_Rangecc(parts.scheme, "about")) { + if (!findWidget_App("winbar")) { + pushBackCStr_StringArray(title, "Lagrange"); } } + else if (!isEmpty_Range(&parts.host)) { + pushBackRange_StringArray(title, parts.host); + } } - return iFalse; -} - -static const iString *saveToDownloads_(const iString *url, const iString *mime, const iBlock *content, - iBool showDialog) { - const iString *savePath = downloadPathForUrl_App(url, mime); - /* Write the file. */ { - iFile *f = new_File(savePath); - if (open_File(f, writeOnly_FileMode)) { - write_File(f, content); - close_File(f); - const size_t size = size_Block(content); - const iBool isMega = size >= 1000000; -#if defined (iPlatformAppleMobile) - exportDownloadedFile_iOS(savePath); -#else - if (showDialog) { - const iMenuItem items[2] = { - { "${dlg.save.opendownload}", 0, 0, - format_CStr("!open url:%s", cstrCollect_String(makeFileUrl_String(savePath))) }, - { "${dlg.message.ok}", 0, 0, "message.ok" }, - }; - makeMessage_Widget(uiHeading_ColorEscape "${heading.save}", - format_CStr("%s\n${dlg.save.size} %.3f %s", - cstr_String(path_File(f)), - isMega ? size / 1.0e6f : (size / 1.0e3f), - isMega ? "${mb}" : "${kb}"), - items, - iElemCount(items)); + if (isEmpty_StringArray(title)) { + pushBackCStr_StringArray(title, "Lagrange"); + } + /* Take away parts if it doesn't fit. */ + const int avail = bounds_Widget(as_Widget(tabButton)).size.x - 7 * gap_UI; + iBool setWindow = (document_App() == d && isUnderKeyRoot_Widget(d)); + const int font = uiLabel_FontId; + for (;;) { + iString *text = collect_String(joinCStr_StringArray(title, " \u2014 ")); + if (setWindow) { + /* Longest version for the window title, and omit the icon. */ + setTitle_MainWindow(get_MainWindow(), text); + setWindow = iFalse; + } + const iChar siteIcon = siteIcon_GmDocument(d->view.doc); + if (siteIcon) { + if (!isEmpty_String(text)) { + prependCStr_String(text, " " restore_ColorEscape); } -#endif - return savePath; + prependChar_String(text, siteIcon); + prependCStr_String(text, escape_Color(uiIcon_ColorId)); } - else { - makeSimpleMessage_Widget(uiTextCaution_ColorEscape "${heading.save.error}", - strerror(errno)); + const int width = measureRange_Text(font, range_String(text)).advance.x; + const int ellipsisWidth = measure_Text(font, "...").advance.x; + setTextColor_LabelWidget(tabButton, none_ColorId); + iWidget *tabCloseButton = child_Widget(as_Widget(tabButton), 0); + setFlags_Widget(tabCloseButton, visibleOnParentHover_WidgetFlag, + avail > width_Widget(tabCloseButton)); + if (width <= avail || isEmpty_StringArray(title)) { + updateText_LabelWidget(tabButton, text); + break; + } + if (size_StringArray(title) == 1) { + /* Just truncate to fit. */ + if (siteIcon && avail <= 4 * ellipsisWidth) { + updateText_LabelWidget(tabButton, collect_String(newUnicodeN_String(&siteIcon, 1))); + setTextColor_LabelWidget(tabButton, uiIcon_ColorId); + break; + } + const char *endPos; + tryAdvanceNoWrap_Text(font, + range_String(text), + avail - ellipsisWidth, + &endPos); + updateText_LabelWidget( + tabButton, + collectNewFormat_String( + "%s...", cstr_Rangecc((iRangecc){ constBegin_String(text), endPos }))); + break; } - iRelease(f); + remove_StringArray(title, size_StringArray(title) - 1); } - return collectNew_String(); } -static void addAllLinks_(void *context, const iGmRun *run) { - iPtrArray *links = context; - if (~run->flags & decoration_GmRunFlag && run->linkId) { - pushBack_PtrArray(links, run); +static void invalidate_DocumentWidget_(iDocumentWidget *d) { + if (flags_Widget(as_Widget(d)) & destroyPending_WidgetFlag) { + return; } -} - -static size_t visibleLinkOrdinal_DocumentView_(const iDocumentView *d, iGmLinkId linkId) { - size_t ord = 0; - const iRangei visRange = visibleRange_DocumentView_(d); - iConstForEach(PtrArray, i, &d->visibleLinks) { - const iGmRun *run = i.ptr; - if (top_Rect(run->visBounds) >= visRange.start + gap_UI * d->pageMargin * 4 / 5) { - if (run->flags & decoration_GmRunFlag && run->linkId) { - if (run->linkId == linkId) return ord; - ord++; - } - } + if (d->flags & invalidationPending_DocumentWidgetFlag) { + return; } - return iInvalidPos; + if (isAffectedByVisualOffset_Widget(as_Widget(d))) { + d->flags |= invalidationPending_DocumentWidgetFlag; + return; + } + d->flags &= ~invalidationPending_DocumentWidgetFlag; + invalidate_DocumentView_(&d->view); +// printf("[%p] '%s' invalidated\n", d, cstr_String(id_Widget(as_Widget(d)))); } -/* Sorted by proximity to F and J. */ -static const int homeRowKeys_[] = { - 'f', 'd', 's', 'a', - 'j', 'k', 'l', - 'r', 'e', 'w', 'q', - 'u', 'i', 'o', 'p', - 'v', 'c', 'x', 'z', - 'm', 'n', - 'g', 'h', - 'b', - 't', 'y', -}; +static iRangecc siteText_DocumentWidget_(const iDocumentWidget *d) { + return isEmpty_String(d->titleUser) ? urlHost_String(d->mod.url) + : range_String(d->titleUser); +} -static iBool updateDocumentWidthRetainingScrollPosition_DocumentView_(iDocumentView *d, - iBool keepCenter) { - const int newWidth = documentWidth_DocumentView_(d); - if (newWidth == size_GmDocument(d->doc).x && !keepCenter /* not a font change */) { +static iBool isPinned_DocumentWidget_(const iDocumentWidget *d) { + if (deviceType_App() == phone_AppDeviceType) { return iFalse; } - /* Font changes (i.e., zooming) will keep the view centered, otherwise keep the top - of the visible area fixed. */ - const iGmRun *run = keepCenter ? middleRun_DocumentView_(d) : d->visibleRuns.start; - const char * runLoc = (run ? run->text.start : NULL); - int voffset = 0; - if (!keepCenter && run) { - /* Keep the first visible run visible at the same position. */ - /* TODO: First *fully* visible run? */ - voffset = visibleRange_DocumentView_(d).start - top_Rect(run->visBounds); + if (d->flags & otherRootByDefault_DocumentWidgetFlag) { + return iTrue; } - setWidth_GmDocument(d->doc, newWidth, width_Widget(d->owner)); - setWidth_Banner(d->owner->banner, newWidth); - documentRunsInvalidated_DocumentWidget_(d->owner); - if (runLoc && !keepCenter) { - run = findRunAtLoc_GmDocument(d->doc, runLoc); - if (run) { - scrollTo_DocumentView_( - d, top_Rect(run->visBounds) + lineHeight_Text(paragraph_FontId) + voffset, iFalse); - } + const iWidget *w = constAs_Widget(d); + const iWindow *win = get_Window(); + if (numRoots_Window(win) == 1) { + return iFalse; } - else if (runLoc && keepCenter) { - run = findRunAtLoc_GmDocument(d->doc, runLoc); - if (run) { - scrollTo_DocumentView_(d, mid_Rect(run->bounds).y, iTrue); + const iPrefs *prefs = prefs_App(); + return (prefs->pinSplit == 1 && w->root == win->roots[0]) || + (prefs->pinSplit == 2 && w->root == win->roots[1]); +} + +static void showOrHidePinningIndicator_DocumentWidget_(iDocumentWidget *d) { + iWidget *w = as_Widget(d); + showCollapsed_Widget(findChild_Widget(root_Widget(w), "document.pinned"), + isPinned_DocumentWidget_(d)); +} + +static void documentWasChanged_DocumentWidget_(iDocumentWidget *d) { + iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iFalse); + setFlags_Widget(as_Widget(d), touchDrag_WidgetFlag, iFalse); + updateVisitedLinks_GmDocument(d->view.doc); + documentRunsInvalidated_DocumentWidget_(d); + updateWindowTitle_DocumentWidget_(d); + updateVisible_DocumentView_(&d->view); + d->view.drawBufs->flags |= updateSideBuf_DrawBufsFlag; + invalidate_DocumentWidget_(d); + refresh_Widget(as_Widget(d)); + /* Check for special bookmark tags. */ + d->flags &= ~otherRootByDefault_DocumentWidgetFlag; + const uint16_t bmid = findUrl_Bookmarks(bookmarks_App(), d->mod.url); + if (bmid) { + const iBookmark *bm = get_Bookmarks(bookmarks_App(), bmid); + if (bm->flags & linkSplit_BookmarkFlag) { + d->flags |= otherRootByDefault_DocumentWidgetFlag; } } - return iTrue; + showOrHidePinningIndicator_DocumentWidget_(d); + if (~d->flags & fromCache_DocumentWidgetFlag) { + setCachedDocument_History(d->mod.history, + d->view.doc, /* keeps a ref */ + (d->flags & openedFromSidebar_DocumentWidgetFlag) != 0); + } } -static iBool handlePinch_DocumentWidget_(iDocumentWidget *d, const char *cmd) { - if (equal_Command(cmd, "pinch.began")) { - d->pinchZoomInitial = d->pinchZoomPosted = prefs_App()->zoomPercent; - d->flags |= pinchZoom_DocumentWidgetFlag; - refresh_Widget(d); +static void replaceDocument_DocumentWidget_(iDocumentWidget *d, iGmDocument *newDoc) { + pauseAllPlayers_Media(media_GmDocument(d->view.doc), iTrue); + iRelease(d->view.doc); + d->view.doc = ref_Object(newDoc); + documentWasChanged_DocumentWidget_(d); +} + +static void updateBanner_DocumentWidget_(iDocumentWidget *d) { + setSite_Banner(d->banner, siteText_DocumentWidget_(d), siteIcon_GmDocument(d->view.doc)); +} + +static void updateTheme_DocumentWidget_(iDocumentWidget *d) { + if (document_App() != d || category_GmStatusCode(d->sourceStatus) == categoryInput_GmStatusCode) { + return; + } + d->view.drawBufs->flags |= updateTimestampBuf_DrawBufsFlag; + updateBanner_DocumentWidget_(d); +} + +static void makeFooterButtons_DocumentWidget_(iDocumentWidget *d, const iMenuItem *items, size_t count) { + iWidget *w = as_Widget(d); + destroy_Widget(d->footerButtons); + d->footerButtons = NULL; + if (count == 0) { + return; } - else if (equal_Command(cmd, "pinch.moved")) { - const float rel = argf_Command(cmd); - int zoom = iRound(d->pinchZoomInitial * rel / 5.0f) * 5; - zoom = iClamp(zoom, 50, 200); - if (d->pinchZoomPosted != zoom) { -#if defined (iPlatformAppleMobile) - if (zoom == 100) { - playHapticEffect_iOS(tap_HapticEffect); + d->footerButtons = new_Widget(); + setFlags_Widget(d->footerButtons, + unhittable_WidgetFlag | arrangeVertical_WidgetFlag | + resizeWidthOfChildren_WidgetFlag | arrangeHeight_WidgetFlag | + fixedPosition_WidgetFlag | resizeToParentWidth_WidgetFlag, + iTrue); + for (size_t i = 0; i < count; ++i) { + iLabelWidget *button = addChildFlags_Widget( + d->footerButtons, + iClob(newKeyMods_LabelWidget( + items[i].label, items[i].key, items[i].kmods, items[i].command)), + alignLeft_WidgetFlag | drawKey_WidgetFlag | extraPadding_WidgetFlag); + setPadding1_Widget(as_Widget(button), gap_UI / 2); + checkIcon_LabelWidget(button); + setFont_LabelWidget(button, uiContent_FontId); + setBackgroundColor_Widget(as_Widget(button), uiBackgroundSidebar_ColorId); + } + addChild_Widget(as_Widget(d), iClob(d->footerButtons)); + arrange_Widget(d->footerButtons); + arrange_Widget(w); + updateVisible_DocumentView_(&d->view); /* final placement for the buttons */ +} + +static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode code, + const iString *meta) { + iString *src = collectNew_String(); + const iGmError *msg = get_GmError(code); + destroy_Widget(d->footerButtons); + d->footerButtons = NULL; + const iString *serverErrorMsg = NULL; + if (meta) { + switch (code) { + case schemeChangeRedirect_GmStatusCode: + case tooManyRedirects_GmStatusCode: + appendFormat_String(src, "=> %s\n", cstr_String(meta)); + break; + case tlsFailure_GmStatusCode: +// useBanner = iFalse; /* valid data wasn't received from host */ +// appendFormat_String(src, ">%s\n", cstr_String(meta)); + break; + case tlsServerCertificateExpired_GmStatusCode: + makeFooterButtons_DocumentWidget_( + d, + (iMenuItem[]){ { rightArrowhead_Icon " ${menu.unexpire}", + SDLK_RETURN, 0, "server.unexpire" + }, + { info_Icon " ${menu.pageinfo}", + SDLK_i, + KMOD_PRIMARY, + "document.info" } }, + 2); + break; + case tlsServerCertificateNotVerified_GmStatusCode: + makeFooterButtons_DocumentWidget_( + d, + (iMenuItem[]){ { info_Icon " ${menu.pageinfo}", + SDLK_i, + KMOD_PRIMARY, + "document.info" } }, + 1); + break; + case failedToOpenFile_GmStatusCode: + case certificateNotValid_GmStatusCode: +// appendFormat_String(src, "%s", cstr_String(meta)); + break; + case unsupportedMimeType_GmStatusCode: { + iString *key = collectNew_String(); + toString_Sym(SDLK_s, KMOD_PRIMARY, key); +// appendFormat_String(src, "\n```\n%s\n```\n", cstr_String(meta)); + const char *mtype = mediaTypeFromFileExtension_String(d->mod.url); + iArray items; + init_Array(&items, sizeof(iMenuItem)); + if (iCmpStr(mtype, "application/octet-stream")) { + pushBack_Array( + &items, + &(iMenuItem){ translateCStr_Lang(format_CStr("View as \"%s\"", mtype)), + SDLK_RETURN, + 0, + format_CStr("document.setmediatype mime:%s", mtype) }); + } + pushBack_Array( + &items, + &(iMenuItem){ translateCStr_Lang(download_Icon " " saveToDownloads_Label), + 0, + 0, + "document.save" }); + makeFooterButtons_DocumentWidget_(d, data_Array(&items), size_Array(&items)); + deinit_Array(&items); + serverErrorMsg = collectNewFormat_String("%s (%s)", msg->title, cstr_String(meta)); + break; } -#endif - d->pinchZoomPosted = zoom; - postCommandf_App("zoom.set arg:%d", zoom); + default: + if (!isEmpty_String(meta)) { + serverErrorMsg = meta; + } + break; } } - else if (equal_Command(cmd, "pinch.ended")) { - d->flags &= ~pinchZoom_DocumentWidgetFlag; - refresh_Widget(d); + if (category_GmStatusCode(code) == categoryClientCertificate_GmStatus) { + makeFooterButtons_DocumentWidget_( + d, + (iMenuItem[]){ + { leftHalf_Icon " ${menu.show.identities}", + '4', + KMOD_PRIMARY, + deviceType_App() == desktop_AppDeviceType ? "sidebar.mode arg:3 show:1" + : "preferences idents:1" }, + { person_Icon " ${menu.identity.new}", newIdentity_KeyShortcut, "ident.new" } }, + 2); } - return iTrue; -} - -static void swap_DocumentView_(iDocumentView *d, iDocumentView *swapBuffersWith) { - d->scrollY = swapBuffersWith->scrollY; - d->scrollY.widget = as_Widget(d->owner); - iSwap(iVisBuf *, d->visBuf, swapBuffersWith->visBuf); - iSwap(iVisBufMeta *, d->visBufMeta, swapBuffersWith->visBufMeta); - iSwap(iDrawBufs *, d->drawBufs, swapBuffersWith->drawBufs); - updateVisible_DocumentView_(d); - updateVisible_DocumentView_(swapBuffersWith); + /* Make a new document for the error page.*/ + iGmDocument *errorDoc = new_GmDocument(); + setWidth_GmDocument(errorDoc, documentWidth_DocumentView_(&d->view), width_Widget(d)); + setUrl_GmDocument(errorDoc, d->mod.url); + setFormat_GmDocument(errorDoc, gemini_SourceFormat); + replaceDocument_DocumentWidget_(d, errorDoc); + iRelease(errorDoc); + clear_Banner(d->banner); + add_Banner(d->banner, error_BannerType, code, serverErrorMsg, NULL); + d->state = ready_RequestState; + setSource_DocumentWidget(d, src); + updateTheme_DocumentWidget_(d); + resetScroll_DocumentView_(&d->view); } -static void swap_DocumentWidget_(iDocumentWidget *d, iGmDocument *doc, - iDocumentWidget *swapBuffersWith) { - if (doc) { - iAssert(isInstance_Object(doc, &Class_GmDocument)); - replaceDocument_DocumentWidget_(d, doc); - iSwap(iBanner *, d->banner, swapBuffersWith->banner); - setOwner_Banner(d->banner, d); - setOwner_Banner(swapBuffersWith->banner, swapBuffersWith); - swap_DocumentView_(&d->view, &swapBuffersWith->view); -// invalidate_DocumentWidget_(swapBuffersWith); +static void updateFetchProgress_DocumentWidget_(iDocumentWidget *d) { + iLabelWidget *prog = findChild_Widget(root_Widget(as_Widget(d)), "document.progress"); + const size_t dlSize = d->request ? bodySize_GmRequest(d->request) : 0; + showCollapsed_Widget(as_Widget(prog), dlSize >= 250000); + if (isVisible_Widget(prog)) { + updateText_LabelWidget(prog, + collectNewFormat_String("%s%.3f ${mb}", + isFinished_GmRequest(d->request) + ? uiHeading_ColorEscape + : uiTextCaution_ColorEscape, + dlSize / 1.0e6f)); } } -static iWidget *swipeParent_DocumentWidget_(iDocumentWidget *d) { - return findChild_Widget(as_Widget(d)->root->widget, "doctabs"); -} - -static void setUrl_DocumentWidget_(iDocumentWidget *d, const iString *url) { - url = canonicalUrl_String(url); - if (!equal_String(d->mod.url, url)) { - d->flags |= urlChanged_DocumentWidgetFlag; - set_String(d->mod.url, url); +static const char *zipPageHeading_(const iRangecc mime) { + if (equalCase_Rangecc(mime, "application/gpub+zip")) { + return book_Icon " Gempub"; } -} - -static void setupSwipeOverlay_DocumentWidget_(iDocumentWidget *d, iWidget *overlay) { - iWidget *w = as_Widget(d); - iWidget *swipeParent = swipeParent_DocumentWidget_(d); - /* The target takes the old document and jumps on top. */ - overlay->rect.pos = windowToInner_Widget(swipeParent, innerToWindow_Widget(w, zero_I2())); - /* Note: `innerToWindow_Widget` does not apply visual offset. */ - overlay->rect.size = w->rect.size; - setFlags_Widget(overlay, fixedPosition_WidgetFlag | fixedSize_WidgetFlag, iTrue); -// swap_DocumentWidget_(target, d->doc, d); - setFlags_Widget(as_Widget(d), refChildrenOffset_WidgetFlag, iTrue); - as_Widget(d)->offsetRef = swipeParent; - /* `overlay` animates off the screen to the right. */ - const int fromPos = value_Anim(&w->visualOffset); - const int toPos = width_Widget(overlay); - setVisualOffset_Widget(overlay, fromPos, 0, 0); - /* Bigger screen, faster swipes. */ - if (deviceType_App() == desktop_AppDeviceType) { - setVisualOffset_Widget(overlay, toPos, 250, easeOut_AnimFlag | softer_AnimFlag); + else if (equalCase_Rangecc(mime, mimeType_FontPack)) { + return fontpack_Icon " Fontpack"; } - else { - const float devFactor = (deviceType_App() == phone_AppDeviceType ? 1.0f : 2.0f); - float swipe = iClamp(d->swipeSpeed, devFactor * 400, devFactor * 1000) * gap_UI; - uint32_t span = ((toPos - fromPos) / swipe) * 1000; - // printf("from:%d to:%d swipe:%f span:%u\n", fromPos, toPos, d->swipeSpeed, span); - setVisualOffset_Widget(overlay, toPos, span, deviceType_App() == tablet_AppDeviceType ? - easeOut_AnimFlag : 0); + iRangecc type = iNullRange; + nextSplit_Rangecc(mime, "/", &type); /* skip the part before the slash */ + nextSplit_Rangecc(mime, "/", &type); + if (startsWithCase_Rangecc(type, "x-")) { + type.start += 2; } - setVisualOffset_Widget(w, 0, 0, 0); + iString *heading = upper_String(collectNewRange_String(type)); + appendCStr_String(heading, " Archive"); + prependCStr_String(heading, folder_Icon " "); + return cstrCollect_String(heading); } -static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { - /* TODO: Cleanup - If DocumentWidget is refactored to split the document presentation from state - and request management (a new DocumentView class), plain views could be used for this - animation without having to mess with the complete state of the DocumentWidget. That - seems like a less error-prone approach -- the current implementation will likely break - down (again) if anything is changed in the document internals. - */ +static void postProcessRequestContent_DocumentWidget_(iDocumentWidget *d, iBool isCached) { iWidget *w = as_Widget(d); - /* The swipe animation is implemented in a rather complex way. It utilizes both cached - GmDocument content and temporary underlay/overlay DocumentWidgets. Depending on the - swipe direction, the DocumentWidget `d` may wait until the finger is released to actually - perform the navigation action. */ - if (equal_Command(cmd, "edgeswipe.moved")) { - //printf("[%p] responds to edgeswipe.moved\n", d); - as_Widget(d)->offsetRef = NULL; - const int side = argLabel_Command(cmd, "side"); - const int offset = arg_Command(cmd); - if (side == 1) { /* left edge */ - if (atOldest_History(d->mod.history)) { - return iTrue; + /* Gempub page behavior and footer actions. */ { + /* TODO: move this to gempub.c */ + delete_Gempub(d->sourceGempub); + d->sourceGempub = NULL; + if (!cmpCase_String(&d->sourceMime, "application/octet-stream") || + !cmpCase_String(&d->sourceMime, mimeType_Gempub) || + endsWithCase_String(d->mod.url, ".gpub")) { + iGempub *gempub = new_Gempub(); + if (open_Gempub(gempub, &d->sourceContent)) { + setBaseUrl_Gempub(gempub, d->mod.url); + setSource_DocumentWidget(d, collect_String(coverPageSource_Gempub(gempub))); + setCStr_String(&d->sourceMime, mimeType_Gempub); + d->sourceGempub = gempub; } - iWidget *swipeParent = swipeParent_DocumentWidget_(d); - if (findChild_Widget(swipeParent, "swipeout")) { - return iTrue; /* too fast, previous animation hasn't finished */ + else { + delete_Gempub(gempub); + } + } + if (!d->sourceGempub) { + const iString *localPath = collect_String(localFilePathFromUrl_String(d->mod.url)); + iBool isInside = iFalse; + if (localPath && !fileExists_FileInfo(localPath)) { + /* This URL may refer to a file inside the archive. */ + localPath = findContainerArchive_Path(localPath); + isInside = iTrue; + } + if (localPath && equal_CStr(mediaType_Path(localPath), mimeType_Gempub)) { + iGempub *gempub = new_Gempub(); + if (openFile_Gempub(gempub, localPath)) { + setBaseUrl_Gempub(gempub, collect_String(makeFileUrl_String(localPath))); + if (!isInside) { + setSource_DocumentWidget(d, collect_String(coverPageSource_Gempub(gempub))); + setCStr_String(&d->sourceMime, mimeType_Gempub); + } + d->sourceGempub = gempub; + } + else { + delete_Gempub(gempub); + } + } + } + if (d->sourceGempub) { + if (equal_String(d->mod.url, coverPageUrl_Gempub(d->sourceGempub))) { + if (!isRemote_Gempub(d->sourceGempub)) { + iArray *items = collectNew_Array(sizeof(iMenuItem)); + pushBack_Array( + items, + &(iMenuItem){ book_Icon " ${gempub.cover.view}", + 0, + 0, + format_CStr("!open url:%s", + cstr_String(indexPageUrl_Gempub(d->sourceGempub))) }); + if (navSize_Gempub(d->sourceGempub) > 0) { + pushBack_Array( + items, + &(iMenuItem){ + format_CStr(forwardArrow_Icon " %s", + cstr_String(navLinkLabel_Gempub(d->sourceGempub, 0))), + SDLK_RIGHT, + 0, + format_CStr("!open url:%s", + cstr_String(navLinkUrl_Gempub(d->sourceGempub, 0))) }); + } + makeFooterButtons_DocumentWidget_(d, constData_Array(items), size_Array(items)); + } + else { + makeFooterButtons_DocumentWidget_( + d, + (iMenuItem[]){ { book_Icon " ${menu.save.downloads.open}", + SDLK_s, + KMOD_PRIMARY | KMOD_SHIFT, + "document.save open:1" }, + { download_Icon " " saveToDownloads_Label, + SDLK_s, + KMOD_PRIMARY, + "document.save" } }, + 2); + } + if (preloadCoverImage_Gempub(d->sourceGempub, d->view.doc)) { + redoLayout_GmDocument(d->view.doc); + updateVisible_DocumentView_(&d->view); + invalidate_DocumentWidget_(d); + } + } + else if (equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) { + makeFooterButtons_DocumentWidget_( + d, + (iMenuItem[]){ { format_CStr(book_Icon " %s", + cstr_String(property_Gempub(d->sourceGempub, + title_GempubProperty))), + SDLK_LEFT, + 0, + format_CStr("!open url:%s", + cstr_String(coverPageUrl_Gempub(d->sourceGempub))) } }, + 1); + } + else { + /* Navigation buttons. */ + iArray *items = collectNew_Array(sizeof(iMenuItem)); + const size_t navIndex = navIndex_Gempub(d->sourceGempub, d->mod.url); + if (navIndex != iInvalidPos) { + if (navIndex < navSize_Gempub(d->sourceGempub) - 1) { + pushBack_Array( + items, + &(iMenuItem){ + format_CStr(forwardArrow_Icon " %s", + cstr_String(navLinkLabel_Gempub(d->sourceGempub, navIndex + 1))), + SDLK_RIGHT, + 0, + format_CStr("!open url:%s", + cstr_String(navLinkUrl_Gempub(d->sourceGempub, navIndex + 1))) }); + } + if (navIndex > 0) { + pushBack_Array( + items, + &(iMenuItem){ + format_CStr(backArrow_Icon " %s", + cstr_String(navLinkLabel_Gempub(d->sourceGempub, navIndex - 1))), + SDLK_LEFT, + 0, + format_CStr("!open url:%s", + cstr_String(navLinkUrl_Gempub(d->sourceGempub, navIndex - 1))) }); + } + else if (!equalCase_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) { + pushBack_Array( + items, + &(iMenuItem){ + format_CStr(book_Icon " %s", + cstr_String(property_Gempub(d->sourceGempub, title_GempubProperty))), + SDLK_LEFT, + 0, + format_CStr("!open url:%s", + cstr_String(coverPageUrl_Gempub(d->sourceGempub))) }); + } + } + if (!isEmpty_Array(items)) { + makeFooterButtons_DocumentWidget_(d, constData_Array(items), size_Array(items)); + } } - /* The temporary "swipein" will display the previous page until the finger is lifted. */ - iDocumentWidget *swipeIn = findChild_Widget(swipeParent, "swipein"); - if (!swipeIn) { - swipeIn = new_DocumentWidget(); - swipeIn->flags |= animationPlaceholder_DocumentWidgetFlag; - setId_Widget(as_Widget(swipeIn), "swipein"); - setFlags_Widget(as_Widget(swipeIn), - disabled_WidgetFlag | refChildrenOffset_WidgetFlag | - fixedPosition_WidgetFlag | fixedSize_WidgetFlag, iTrue); - setFlags_Widget(findChild_Widget(as_Widget(swipeIn), "scroll"), hidden_WidgetFlag, iTrue); - swipeIn->widget.rect.pos = windowToInner_Widget(swipeParent, localToWindow_Widget(w, w->rect.pos)); - swipeIn->widget.rect.size = d->widget.rect.size; - swipeIn->widget.offsetRef = parent_Widget(w); - /* Use a cached document for the layer underneath. */ { - lock_History(d->mod.history); - iRecentUrl *recent = precedingLocked_History(d->mod.history); - if (recent && recent->cachedResponse) { - setUrl_DocumentWidget_(swipeIn, &recent->url); - updateFromCachedResponse_DocumentWidget_(swipeIn, - recent->normScrollY, - recent->cachedResponse, - recent->cachedDoc); - parseUser_DocumentWidget_(swipeIn); - updateBanner_DocumentWidget_(swipeIn); + if (!isCached && prefs_App()->pinSplit && + equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) { + const iString *navStart = navStartLinkUrl_Gempub(d->sourceGempub); + if (navStart) { + iWindow *win = get_Window(); + /* Auto-split to show index and the first navigation link. */ + if (numRoots_Window(win) == 2) { + /* This document is showing the index page. */ + iRoot *other = otherRoot_Window(win, w->root); + postCommandf_Root(other, "open url:%s", cstr_String(navStart)); + if (prefs_App()->pinSplit == 1 && w->root == win->roots[1]) { + /* On the wrong side. */ + postCommand_App("ui.split swap:1"); + } } else { - setUrlAndSource_DocumentWidget(swipeIn, &recent->url, - collectNewCStr_String("text/gemini"), - collect_Block(new_Block(0))); + postCommandf_App( + "open splitmode:1 newtab:%d url:%s", otherRoot_OpenTabFlag, cstr_String(navStart)); } - unlock_History(d->mod.history); } - addChildPos_Widget(swipeParent, iClob(swipeIn), front_WidgetAddPos); } } - if (side == 2) { /* right edge */ - if (offset < -get_Window()->pixelRatio * 10) { - int animSpan = 10; - if (!atNewest_History(d->mod.history) && ~flags_Widget(w) & dragged_WidgetFlag) { - iWidget *swipeParent = swipeParent_DocumentWidget_(d); - if (findChild_Widget(swipeParent, "swipeout")) { - return iTrue; /* too fast, previous animation hasn't finished */ - } - /* Setup the drag. `d` will be moving with the finger. */ - animSpan = 0; - postCommand_Widget(d, "navigate.forward"); - setFlags_Widget(w, dragged_WidgetFlag, iTrue); - /* Set up the swipe dummy. */ - iDocumentWidget *target = new_DocumentWidget(); - target->flags |= animationPlaceholder_DocumentWidgetFlag; - setId_Widget(as_Widget(target), "swipeout"); - /* "swipeout" takes `d`'s document and goes underneath. */ - target->widget.rect.pos = windowToInner_Widget(swipeParent, localToWindow_Widget(w, w->rect.pos)); - target->widget.rect.size = d->widget.rect.size; - setFlags_Widget(as_Widget(target), fixedPosition_WidgetFlag | fixedSize_WidgetFlag, iTrue); - swap_DocumentWidget_(target, d->view.doc, d); - addChildPos_Widget(swipeParent, iClob(target), front_WidgetAddPos); - setFlags_Widget(as_Widget(target), refChildrenOffset_WidgetFlag, iTrue); - as_Widget(target)->offsetRef = parent_Widget(w); - /* Mark it for deletion after animation finishes. */ - destroy_Widget(as_Widget(target)); - /* The `d` document will now navigate forward and be replaced with a cached - copy. However, if a cached response isn't available, we'll need to show a - blank page. */ - setUrlAndSource_DocumentWidget(d, - collectNewCStr_String("about:blank"), - collectNewCStr_String("text/gemini"), - collect_Block(new_Block(0))); + } +} + +static void updateDocument_DocumentWidget_(iDocumentWidget *d, + const iGmResponse *response, + iGmDocument *cachedDoc, + const iBool isInitialUpdate) { + if (d->state == ready_RequestState) { + return; + } + const iBool isRequestFinished = isFinished_GmRequest(d->request); + /* TODO: Do document update in the background. However, that requires a text metrics calculator + that does not try to cache the glyph bitmaps. */ + const enum iGmStatusCode statusCode = response->statusCode; + if (category_GmStatusCode(statusCode) != categoryInput_GmStatusCode) { + iBool setSource = iTrue; + iString str; + invalidate_DocumentWidget_(d); + if (document_App() == d) { + updateTheme_DocumentWidget_(d); + } + clear_String(&d->sourceMime); + d->sourceTime = response->when; + d->view.drawBufs->flags |= updateTimestampBuf_DrawBufsFlag; + initBlock_String(&str, &response->body); /* Note: Body may be megabytes in size. */ + if (isSuccess_GmStatusCode(statusCode)) { + /* Check the MIME type. */ + iRangecc charset = range_CStr("utf-8"); + enum iSourceFormat docFormat = undefined_SourceFormat; + const iString *mimeStr = collect_String(lower_String(&response->meta)); /* for convenience */ + set_String(&d->sourceMime, mimeStr); + iRangecc mime = range_String(mimeStr); + iRangecc seg = iNullRange; + while (nextSplit_Rangecc(mime, ";", &seg)) { + iRangecc param = seg; + trim_Rangecc(¶m); + /* Detect fontpacks even if the server doesn't use the right media type. */ + if (isRequestFinished && equal_Rangecc(param, "application/octet-stream")) { + if (detect_FontPack(&response->body)) { + param = range_CStr(mimeType_FontPack); + } + } + if (equal_Rangecc(param, "text/gemini")) { + docFormat = gemini_SourceFormat; + setRange_String(&d->sourceMime, param); + } + else if (equal_Rangecc(param, "text/markdown")) { + docFormat = markdown_SourceFormat; + setRange_String(&d->sourceMime, param); + } + else if (startsWith_Rangecc(param, "text/") || + equal_Rangecc(param, "application/json") || + equal_Rangecc(param, "application/x-pem-file") || + equal_Rangecc(param, "application/pem-certificate-chain")) { + docFormat = plainText_SourceFormat; + setRange_String(&d->sourceMime, param); + } + else if (isRequestFinished && equal_Rangecc(param, "font/ttf")) { + clear_String(&str); + docFormat = gemini_SourceFormat; + setRange_String(&d->sourceMime, param); + format_String(&str, "# TrueType Font\n"); + iString *decUrl = collect_String(urlDecode_String(d->mod.url)); + iRangecc name = baseName_Path(decUrl); + iBool isInstalled = iFalse; + if (startsWith_String(collect_String(localFilePathFromUrl_String(d->mod.url)), + cstr_String(dataDir_App()))) { + isInstalled = iTrue; + } + appendCStr_String(&str, "## "); + appendRange_String(&str, name); + appendCStr_String(&str, "\n\n"); + appendCStr_String( + &str, cstr_Lang(isInstalled ? "truetype.help.installed" : "truetype.help")); + appendCStr_String(&str, "\n"); + if (!isInstalled) { + makeFooterButtons_DocumentWidget_( + d, + (iMenuItem[]){ + { add_Icon " ${fontpack.install.ttf}", + SDLK_RETURN, + 0, + format_CStr("!fontpack.install ttf:1 name:%s", + cstr_Rangecc(name)) }, + { folder_Icon " ${fontpack.open.fontsdir}", + SDLK_d, + 0, + format_CStr("!open url:%s/fonts", + cstrCollect_String(makeFileUrl_String(dataDir_App()))) + } + }, 2); + } + } + else if (isRequestFinished && + (equal_Rangecc(param, "application/zip") || + (startsWith_Rangecc(param, "application/") && + endsWithCase_Rangecc(param, "+zip")))) { + clear_String(&str); + docFormat = gemini_SourceFormat; + setRange_String(&d->sourceMime, param); + if (equal_Rangecc(param, mimeType_FontPack)) { + /* Show some information about fontpacks, and set up footer actions. */ + iArchive *zip = iClob(new_Archive()); + if (openData_Archive(zip, &response->body)) { + iFontPack *fp = new_FontPack(); + setUrl_FontPack(fp, d->mod.url); + setStandalone_FontPack(fp, iTrue); + if (loadArchive_FontPack(fp, zip)) { + appendFormat_String(&str, "# " fontpack_Icon "%s\n%s", + cstr_String(id_FontPack(fp).id), + cstrCollect_String(infoText_FontPack(fp))); + } + appendCStr_String(&str, "\n"); + appendCStr_String(&str, cstr_Lang("fontpack.help")); + appendCStr_String(&str, "\n"); + const iArray *actions = actions_FontPack(fp, iTrue); + makeFooterButtons_DocumentWidget_(d, constData_Array(actions), + size_Array(actions)); + delete_FontPack(fp); + } + } + else { + format_String(&str, "# %s\n", zipPageHeading_(param)); + appendFormat_String(&str, + cstr_Lang("doc.archive"), + cstr_Rangecc(baseName_Path(d->mod.url))); + appendCStr_String(&str, "\n"); + } + appendCStr_String(&str, "\n"); + iString *localPath = localFilePathFromUrl_String(d->mod.url); + if (!localPath) { + iString *key = collectNew_String(); + toString_Sym(SDLK_s, KMOD_PRIMARY, key); + appendFormat_String(&str, "%s\n\n", + format_CStr(cstr_Lang("error.unsupported.suggestsave"), + cstr_String(key), + saveToDownloads_Label)); + } + delete_String(localPath); + if (equalCase_Rangecc(urlScheme_String(d->mod.url), "file")) { + appendFormat_String(&str, "=> %s/ " folder_Icon " ${doc.archive.view}\n", + cstr_String(withSpacesEncoded_String(d->mod.url))); + } + translate_Lang(&str); + } + else if (startsWith_Rangecc(param, "image/") || + startsWith_Rangecc(param, "audio/")) { + const iBool isAudio = startsWith_Rangecc(param, "audio/"); + /* Make a simple document with an image or audio player. */ + clear_String(&str); + docFormat = gemini_SourceFormat; + setRange_String(&d->sourceMime, param); + const iGmLinkId imgLinkId = 1; /* there's only the one link */ + /* TODO: Do the image loading in `postProcessRequestContent_DocumentWidget_()` */ + if ((isAudio && isInitialUpdate) || (!isAudio && isRequestFinished)) { + const char *linkTitle = cstr_Lang( + startsWith_String(mimeStr, "image/") ? "media.untitled.image" + : "media.untitled.audio"); + iUrl parts; + init_Url(&parts, d->mod.url); + if (!isEmpty_Range(&parts.path)) { + linkTitle = + baseName_Path(collect_String(newRange_String(parts.path))).start; + } + format_String(&str, "=> %s %s\n", + cstr_String(canonicalUrl_String(d->mod.url)), + linkTitle); + setData_Media(media_GmDocument(d->view.doc), + imgLinkId, + mimeStr, + &response->body, + !isRequestFinished ? partialData_MediaFlag : 0); + redoLayout_GmDocument(d->view.doc); + } + else if (isAudio && !isInitialUpdate) { + /* Update the audio content. */ + setData_Media(media_GmDocument(d->view.doc), + imgLinkId, + mimeStr, + &response->body, + !isRequestFinished ? partialData_MediaFlag : 0); + refresh_Widget(d); + setSource = iFalse; + } + else { + clear_String(&str); + } } - if (flags_Widget(w) & dragged_WidgetFlag) { - setVisualOffset_Widget(w, width_Widget(w) + - width_Widget(d) * offset / size_Root(w->root).x, - animSpan, 0); + else if (startsWith_Rangecc(param, "charset=")) { + charset = (iRangecc){ param.start + 8, param.end }; + /* Remove whitespace and quotes. */ + trim_Rangecc(&charset); + if (*charset.start == '"' && *charset.end == '"') { + charset.start++; + charset.end--; + } } - else { - setVisualOffset_Widget(w, offset / 4, animSpan, 0); + } + if (docFormat == undefined_SourceFormat) { + if (isRequestFinished) { + d->flags &= ~drawDownloadCounter_DocumentWidgetFlag; + showErrorPage_DocumentWidget_(d, unsupportedMimeType_GmStatusCode, &response->meta); + deinit_String(&str); + return; } + d->flags |= drawDownloadCounter_DocumentWidgetFlag; + clear_PtrSet(d->view.invalidRuns); + deinit_String(&str); + return; } - return iTrue; - } - } - if (equal_Command(cmd, "edgeswipe.ended") && argLabel_Command(cmd, "side") == 2) { - if (argLabel_Command(cmd, "abort") && flags_Widget(w) & dragged_WidgetFlag) { - setFlags_Widget(w, dragged_WidgetFlag, iFalse); - postCommand_Widget(d, "navigate.back"); - /* We must now undo the swap that was done when the drag started. */ - /* TODO: Currently not animated! What exactly is the appropriate thing to do here? */ - iWidget *swipeParent = swipeParent_DocumentWidget_(d); - iDocumentWidget *swipeOut = findChild_Widget(swipeParent, "swipeout"); - swap_DocumentWidget_(d, swipeOut->view.doc, swipeOut); -// const int visOff = visualOffsetByReference_Widget(w); - w->offsetRef = NULL; -// setVisualOffset_Widget(w, visOff, 0, 0); -// setVisualOffset_Widget(w, 0, 150, 0); - setVisualOffset_Widget(w, 0, 0, 0); - /* Make it an overlay instead. */ -// removeChild_Widget(swipeParent, swipeOut); -// addChildPos_Widget(swipeParent, iClob(swipeOut), back_WidgetAddPos); -// setupSwipeOverlay_DocumentWidget_(d, as_Widget(swipeOut)); - return iTrue; - } - iAssert(~d->flags & animationPlaceholder_DocumentWidgetFlag); - setFlags_Widget(w, dragged_WidgetFlag, iFalse); - setVisualOffset_Widget(w, 0, 250, easeOut_AnimFlag | softer_AnimFlag); - return iTrue; - } - if (equal_Command(cmd, "edgeswipe.ended") && argLabel_Command(cmd, "side") == 1) { - iWidget *swipeParent = swipeParent_DocumentWidget_(d); - iDocumentWidget *swipeIn = findChild_Widget(swipeParent, "swipein"); - d->swipeSpeed = argLabel_Command(cmd, "speed") / gap_UI; - /* "swipe.back" will soon follow. The `d` document will do the actual back navigation, - switching immediately to a cached page. However, if one is not available, we'll need - to show a blank page for a while. */ - if (swipeIn) { - if (!argLabel_Command(cmd, "abort")) { - iWidget *swipeParent = swipeParent_DocumentWidget_(d); - /* What was being shown in the `d` document is now being swapped to - the outgoing page animation. */ - iDocumentWidget *target = new_DocumentWidget(); - target->flags |= animationPlaceholder_DocumentWidgetFlag; - addChildPos_Widget(swipeParent, iClob(target), back_WidgetAddPos); - setId_Widget(as_Widget(target), "swipeout"); - setFlags_Widget(as_Widget(target), disabled_WidgetFlag, iTrue); - swap_DocumentWidget_(target, d->view.doc, d); - setUrlAndSource_DocumentWidget(d, - swipeIn->mod.url, - collectNewCStr_String("text/gemini"), - collect_Block(new_Block(0))); - as_Widget(swipeIn)->offsetRef = NULL; + setFormat_GmDocument(d->view.doc, docFormat); + /* Convert the source to UTF-8 if needed. */ + if (!equalCase_Rangecc(charset, "utf-8")) { + set_String(&str, + collect_String(decode_Block(&str.chars, cstr_Rangecc(charset)))); } - destroy_Widget(as_Widget(swipeIn)); } - } - if (equal_Command(cmd, "swipe.back")) { - iWidget *swipeParent = swipeParent_DocumentWidget_(d); - iDocumentWidget *target = findChild_Widget(swipeParent, "swipeout"); - if (atOldest_History(d->mod.history)) { - setVisualOffset_Widget(w, 0, 100, 0); - if (target) { - destroy_Widget(as_Widget(target)); /* didn't need it after all */ - } - return iTrue; + if (cachedDoc) { + replaceDocument_DocumentWidget_(d, cachedDoc); + updateWidth_DocumentView_(&d->view); } - setupSwipeOverlay_DocumentWidget_(d, as_Widget(target)); - destroy_Widget(as_Widget(target)); /* will be actually deleted after animation finishes */ - postCommand_Widget(d, "navigate.back"); - return iTrue; + else if (setSource) { + setSource_DocumentWidget(d, &str); + } + deinit_String(&str); } - return iFalse; } -static iBool cancelRequest_DocumentWidget_(iDocumentWidget *d, iBool postBack) { +static void fetch_DocumentWidget_(iDocumentWidget *d) { + iAssert(~d->flags & animationPlaceholder_DocumentWidgetFlag); + /* Forget the previous request. */ if (d->request) { - iWidget *w = as_Widget(d); - postCommandf_Root(w->root, - "document.request.cancelled doc:%p url:%s", d, cstr_String(d->mod.url)); - iReleasePtr(&d->request); - if (d->state != ready_RequestState) { - d->state = ready_RequestState; - if (postBack) { - postCommand_Root(w->root, "navigate.back"); - } - } - updateFetchProgress_DocumentWidget_(d); - return iTrue; + iRelease(d->request); + d->request = NULL; } - return iFalse; + postCommandf_Root(as_Widget(d)->root, + "document.request.started doc:%p url:%s", + d, + cstr_String(d->mod.url)); + clear_ObjectList(d->media); + d->certFlags = 0; + setLinkNumberMode_DocumentWidget_(d, iFalse); + d->flags &= ~drawDownloadCounter_DocumentWidgetFlag; + d->flags &= ~fromCache_DocumentWidgetFlag; + d->state = fetching_RequestState; + set_Atomic(&d->isRequestUpdated, iFalse); + d->request = new_GmRequest(certs_App()); + setUrl_GmRequest(d->request, d->mod.url); + iConnect(GmRequest, d->request, updated, d, requestUpdated_DocumentWidget_); + iConnect(GmRequest, d->request, finished, d, requestFinished_DocumentWidget_); + submit_GmRequest(d->request); } -static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) { - iWidget *w = as_Widget(d); - if (equal_Command(cmd, "document.openurls.changed")) { - if (d->flags & animationPlaceholder_DocumentWidgetFlag) { - return iFalse; - } - /* When any tab changes its document URL, update the open link indicators. */ - if (updateOpenURLs_GmDocument(d->view.doc)) { - invalidate_DocumentWidget_(d); - refresh_Widget(d); - } - return iFalse; - } - if (equal_Command(cmd, "visited.changed")) { - updateVisitedLinks_GmDocument(d->view.doc); - invalidateVisibleLinks_DocumentView_(&d->view); - return iFalse; - } - if (equal_Command(cmd, "document.render")) /* `Periodic` makes direct dispatch to here */ { -// printf("%u: document.render\n", SDL_GetTicks()); - if (SDL_GetTicks() - d->view.drawBufs->lastRenderTime > 150) { - remove_Periodic(periodic_App(), d); - /* Scrolling has stopped, begin filling up the buffer. */ - if (d->view.visBuf->buffers[0].texture) { - addTicker_App(prerender_DocumentWidget_, d); - } - } - return iTrue; - } - else if (equal_Command(cmd, "window.resized") || equal_Command(cmd, "font.changed") || - equal_Command(cmd, "keyroot.changed")) { - if (equal_Command(cmd, "font.changed")) { - invalidateCachedLayout_History(d->mod.history); - } - /* Alt/Option key may be involved in window size changes. */ - setLinkNumberMode_DocumentWidget_(d, iFalse); - d->phoneToolbar = findWidget_App("toolbar"); - const iBool keepCenter = equal_Command(cmd, "font.changed"); - updateDocumentWidthRetainingScrollPosition_DocumentView_(&d->view, keepCenter); - d->view.drawBufs->flags |= updateSideBuf_DrawBufsFlag; - updateVisible_DocumentView_(&d->view); - invalidate_DocumentWidget_(d); - dealloc_VisBuf(d->view.visBuf); - updateWindowTitle_DocumentWidget_(d); - showOrHidePinningIndicator_DocumentWidget_(d); - refresh_Widget(w); - } - else if (equal_Command(cmd, "window.focus.lost")) { - if (d->flags & showLinkNumbers_DocumentWidgetFlag) { - setLinkNumberMode_DocumentWidget_(d, iFalse); - invalidateVisibleLinks_DocumentView_(&d->view); - refresh_Widget(w); - } - return iFalse; - } - else if (equal_Command(cmd, "window.mouse.exited")) { - return iFalse; - } - else if (equal_Command(cmd, "theme.changed")) { - invalidateTheme_History(d->mod.history); /* forget cached color palettes */ - if (document_App() == d) { - updateTheme_DocumentWidget_(d); - updateVisible_DocumentView_(&d->view); - updateTrust_DocumentWidget_(d, NULL); - d->view.drawBufs->flags |= updateSideBuf_DrawBufsFlag; - invalidate_DocumentWidget_(d); - refresh_Widget(w); - } - } - else if (equal_Command(cmd, "document.layout.changed") && document_Root(get_Root()) == d) { - if (argLabel_Command(cmd, "redo")) { - redoLayout_GmDocument(d->view.doc); - } - updateSize_DocumentWidget(d); +static void updateTrust_DocumentWidget_(iDocumentWidget *d, const iGmResponse *response) { + if (response) { + d->certFlags = response->certFlags; + d->certExpiry = response->certValidUntil; + set_Block(d->certFingerprint, &response->certFingerprint); + set_String(d->certSubject, &response->certSubject); } - else if (equal_Command(cmd, "pinsplit.set")) { - postCommand_App("document.update.pin"); /* prefs value not set yet */ - return iFalse; + iLabelWidget *lock = findChild_Widget(root_Widget(as_Widget(d)), "navbar.lock"); + if (~d->certFlags & available_GmCertFlag) { + setFlags_Widget(as_Widget(lock), disabled_WidgetFlag, iTrue); + updateTextCStr_LabelWidget(lock, gray50_ColorEscape openLock_Icon); + return; } - else if (equal_Command(cmd, "document.update.pin")) { - showOrHidePinningIndicator_DocumentWidget_(d); - return iFalse; + setFlags_Widget(as_Widget(lock), disabled_WidgetFlag, iFalse); + const iBool isDarkMode = isDark_ColorTheme(colorTheme_App()); + if (~d->certFlags & domainVerified_GmCertFlag || + ~d->certFlags & trusted_GmCertFlag) { + updateTextCStr_LabelWidget(lock, red_ColorEscape warning_Icon); } - else if (equal_Command(cmd, "tabs.changed")) { - setLinkNumberMode_DocumentWidget_(d, iFalse); - if (cmp_String(id_Widget(w), suffixPtr_Command(cmd, "id")) == 0) { - /* Set palette for our document. */ - updateTheme_DocumentWidget_(d); - updateTrust_DocumentWidget_(d, NULL); - updateSize_DocumentWidget(d); - showOrHidePinningIndicator_DocumentWidget_(d); - updateFetchProgress_DocumentWidget_(d); - updateHover_Window(window_Widget(w)); - } - init_Anim(&d->view.sideOpacity, 0); - init_Anim(&d->view.altTextOpacity, 0); - updateSideOpacity_DocumentView_(&d->view, iFalse); - updateWindowTitle_DocumentWidget_(d); - allocVisBuffer_DocumentView_(&d->view); - animateMedia_DocumentWidget_(d); - remove_Periodic(periodic_App(), d); - removeTicker_App(prerender_DocumentWidget_, d); - return iFalse; + else if (~d->certFlags & timeVerified_GmCertFlag) { + updateTextCStr_LabelWidget(lock, isDarkMode ? orange_ColorEscape warning_Icon + : black_ColorEscape warning_Icon); } - else if (equal_Command(cmd, "tab.created")) { - /* Space for tab buttons has changed. */ - updateWindowTitle_DocumentWidget_(d); - return iFalse; + else { + updateTextCStr_LabelWidget(lock, green_ColorEscape closedLock_Icon); } - else if (equal_Command(cmd, "document.select") && d == document_App()) { - /* Touch selection mode. */ - if (!arg_Command(cmd)) { - d->selectMark = iNullRange; - setFlags_Widget(w, touchDrag_WidgetFlag, iFalse); - setFadeEnabled_ScrollWidget(d->scroll, iTrue); - } - else { - setFlags_Widget(w, touchDrag_WidgetFlag, iTrue); - d->flags |= movingSelectMarkEnd_DocumentWidgetFlag | - selectWords_DocumentWidgetFlag; /* finger-based selection is imprecise */ - d->flags &= ~selectLines_DocumentWidgetFlag; - setFadeEnabled_ScrollWidget(d->scroll, iFalse); - d->selectMark = sourceLoc_DocumentView_(&d->view, d->contextPos); - extendRange_Rangecc(&d->selectMark, range_String(source_GmDocument(d->view.doc)), - word_RangeExtension | bothStartAndEnd_RangeExtension); - d->initialSelectMark = d->selectMark; - } - return iTrue; +} + +static void parseUser_DocumentWidget_(iDocumentWidget *d) { + setRange_String(d->titleUser, urlUser_String(d->mod.url)); +} + +static void cacheRunGlyphs_(void *data, const iGmRun *run) { + iUnused(data); + if (!isEmpty_Range(&run->text)) { + cache_Text(run->font, run->text); } - else if (equal_Command(cmd, "document.info") && d == document_App()) { - const char *unchecked = red_ColorEscape "\u2610"; - const char *checked = green_ColorEscape "\u2611"; - const iBool haveFingerprint = (d->certFlags & haveFingerprint_GmCertFlag) != 0; - const int requiredForTrust = (available_GmCertFlag | haveFingerprint_GmCertFlag | - timeVerified_GmCertFlag); - const iBool canTrust = ~d->certFlags & trusted_GmCertFlag && - ((d->certFlags & requiredForTrust) == requiredForTrust); - const iRecentUrl *recent = constMostRecentUrl_History(d->mod.history); - const iString *meta = &d->sourceMime; - if (recent && recent->cachedResponse) { - meta = &recent->cachedResponse->meta; - } - iString *msg = collectNew_String(); - if (isEmpty_String(&d->sourceHeader)) { - appendFormat_String(msg, - "%s\n%s\n", - cstr_String(meta), - formatCStrs_Lang("num.bytes.n", size_Block(&d->sourceContent))); +} + +static void cacheDocumentGlyphs_DocumentWidget_(const iDocumentWidget *d) { + if (isFinishedLaunching_App() && isExposed_Window(get_Window()) && + ~d->flags & animationPlaceholder_DocumentWidgetFlag) { + /* Just cache the top of the document, since this is what we usually need. */ + int maxY = height_Widget(&d->widget) * 2; + if (maxY == 0) { + maxY = size_GmDocument(d->view.doc).y; } - else { - appendFormat_String(msg, "%s\n", cstr_String(&d->sourceHeader)); - if (size_Block(&d->sourceContent)) { - appendFormat_String( - msg, "%s\n", formatCStrs_Lang("num.bytes.n", size_Block(&d->sourceContent))); + render_GmDocument(d->view.doc, (iRangei){ 0, maxY }, cacheRunGlyphs_, NULL); + } +} + +static void addBannerWarnings_DocumentWidget_(iDocumentWidget *d) { + /* Warnings related to certificates and trust. */ + const int certFlags = d->certFlags; + const int req = timeVerified_GmCertFlag | domainVerified_GmCertFlag | trusted_GmCertFlag; + if (certFlags & available_GmCertFlag && (certFlags & req) != req && + numItems_Banner(d->banner) == 0) { + iString *title = collectNewCStr_String(cstr_Lang("dlg.certwarn.title")); + iString *str = collectNew_String(); + if (certFlags & timeVerified_GmCertFlag && certFlags & domainVerified_GmCertFlag) { + iUrl parts; + init_Url(&parts, d->mod.url); + const iTime oldUntil = + domainValidUntil_GmCerts(certs_App(), parts.host, port_Url(&parts)); + iDate exp; + init_Date(&exp, &oldUntil); + iTime now; + initCurrent_Time(&now); + const int days = secondsSince_Time(&oldUntil, &now) / 3600 / 24; + if (days <= 30) { + appendCStr_String(str, + format_CStr(cstrCount_Lang("dlg.certwarn.mayberenewed.n", days), + cstrCollect_String(format_Date(&exp, "%Y-%m-%d")), + days)); + } + else { + appendCStr_String(str, cstr_Lang("dlg.certwarn.different")); } } - /* TODO: On mobile, omit the CA status. */ - appendFormat_String( - msg, - "\n%s${pageinfo.cert.status}\n" - "%s%s %s\n" - "%s%s %s%s\n" - "%s%s %s (%04d-%02d-%02d %02d:%02d:%02d)\n" - "%s%s %s", - uiHeading_ColorEscape, - d->certFlags & authorityVerified_GmCertFlag ? checked - : uiTextAction_ColorEscape "\u2610", - uiText_ColorEscape, - d->certFlags & authorityVerified_GmCertFlag ? "${pageinfo.cert.ca.verified}" - : "${pageinfo.cert.ca.unverified}", - d->certFlags & domainVerified_GmCertFlag ? checked : unchecked, - uiText_ColorEscape, - d->certFlags & domainVerified_GmCertFlag ? "${pageinfo.domain.match}" - : "${pageinfo.domain.mismatch}", - ~d->certFlags & domainVerified_GmCertFlag - ? format_CStr(" (%s)", cstr_String(d->certSubject)) - : "", - d->certFlags & timeVerified_GmCertFlag ? checked : unchecked, - uiText_ColorEscape, - d->certFlags & timeVerified_GmCertFlag ? "${pageinfo.cert.notexpired}" - : "${pageinfo.cert.expired}", - d->certExpiry.year, - d->certExpiry.month, - d->certExpiry.day, - d->certExpiry.hour, - d->certExpiry.minute, - d->certExpiry.second, - d->certFlags & trusted_GmCertFlag ? checked : unchecked, - uiText_ColorEscape, - d->certFlags & trusted_GmCertFlag ? "${pageinfo.cert.trusted}" - : "${pageinfo.cert.untrusted}"); - setFocus_Widget(NULL); - iArray *items = new_Array(sizeof(iMenuItem)); - if (canTrust) { - pushBack_Array(items, - &(iMenuItem){ uiTextCaution_ColorEscape "${dlg.cert.trust}", - SDLK_u, - KMOD_PRIMARY | KMOD_SHIFT, - "server.trustcert" }); - } - if (haveFingerprint) { - pushBack_Array(items, &(iMenuItem){ "${dlg.cert.fingerprint}", 0, 0, "server.copycert" }); + else if (certFlags & domainVerified_GmCertFlag) { + setCStr_String(title, get_GmError(tlsServerCertificateExpired_GmStatusCode)->title); + appendFormat_String(str, cstr_Lang("dlg.certwarn.expired"), + cstrCollect_String(format_Date(&d->certExpiry, "%Y-%m-%d"))); } - if (!isEmpty_Array(items)) { - pushBack_Array(items, &(iMenuItem){ "---", 0, 0, 0 }); + else if (certFlags & timeVerified_GmCertFlag) { + appendFormat_String(str, cstr_Lang("dlg.certwarn.domain"), + cstr_String(d->certSubject)); } - pushBack_Array(items, &(iMenuItem){ "${close}", 0, 0, "message.ok" }); - iWidget *dlg = makeQuestion_Widget(uiHeading_ColorEscape "${heading.pageinfo}", - cstr_String(msg), - data_Array(items), - size_Array(items)); - delete_Array(items); - /* Enforce a minimum size. */ -// iWidget *sizer = new_Widget(); -// setFixedSize_Widget(sizer, init_I2(gap_UI * 65, 1)); -// addChildFlags_Widget(dlg, iClob(sizer), frameless_WidgetFlag); -// setFlags_Widget(dlg, centerHorizontal_WidgetFlag, iFalse); - if (deviceType_App() == desktop_AppDeviceType) { - const iWidget *lockButton = findWidget_Root("navbar.lock"); - setPos_Widget(dlg, windowToLocal_Widget(dlg, bottomLeft_Rect(bounds_Widget(lockButton)))); + else { + appendCStr_String(str, cstr_Lang("dlg.certwarn.domain.expired")); } - arrange_Widget(dlg); - addAction_Widget(dlg, SDLK_ESCAPE, 0, "message.ok"); - addAction_Widget(dlg, SDLK_SPACE, 0, "message.ok"); - return iTrue; + add_Banner(d->banner, warning_BannerType, none_GmStatusCode, title, str); } - else if (equal_Command(cmd, "server.unexpire") && document_App() == d) { - const iRangecc host = urlHost_String(d->mod.url); - const uint16_t port = urlPort_String(d->mod.url); - if (!isEmpty_Block(d->certFingerprint) && !isEmpty_Range(&host)) { - iTime expiry; - initCurrent_Time(&expiry); - iTime oneHour; /* One hour is long enough for a single visit (?). */ - initSeconds_Time(&oneHour, 3600); - add_Time(&expiry, &oneHour); - iDate expDate; - init_Date(&expDate, &expiry); - setTrusted_GmCerts(certs_App(), host, port, d->certFingerprint, &expDate); - postCommand_Widget(w, "navigate.reload"); - } - return iTrue; + /* Warnings related to page contents. */ + const int dismissed = + value_SiteSpec(collectNewRange_String(urlRoot_String(d->mod.url)), + dismissWarnings_SiteSpecKey) | + (!prefs_App()->warnAboutMissingGlyphs ? missingGlyphs_GmDocumentWarning : 0); + const int warnings = warnings_GmDocument(d->view.doc) & ~dismissed; + if (warnings & missingGlyphs_GmDocumentWarning) { + add_Banner(d->banner, warning_BannerType, missingGlyphs_GmStatusCode, NULL, NULL); + /* TODO: List one or more of the missing characters and/or their Unicode blocks? */ } - else if (equal_Command(cmd, "server.trustcert") && document_App() == d) { - const iRangecc host = urlHost_String(d->mod.url); - const uint16_t port = urlPort_String(d->mod.url); - if (!isEmpty_Block(d->certFingerprint) && !isEmpty_Range(&host)) { - setTrusted_GmCerts(certs_App(), host, port, d->certFingerprint, &d->certExpiry); - postCommand_Widget(w, "navigate.reload"); + if (warnings & ansiEscapes_GmDocumentWarning) { + add_Banner(d->banner, warning_BannerType, ansiEscapes_GmStatusCode, NULL, NULL); + } +} + +static void updateFromCachedResponse_DocumentWidget_(iDocumentWidget *d, float normScrollY, + const iGmResponse *resp, iGmDocument *cachedDoc) { +// iAssert(width_Widget(d) > 0); /* must be laid out by now */ + setLinkNumberMode_DocumentWidget_(d, iFalse); + clear_ObjectList(d->media); + delete_Gempub(d->sourceGempub); + d->sourceGempub = NULL; + pauseAllPlayers_Media(media_GmDocument(d->view.doc), iTrue); + destroy_Widget(d->footerButtons); + d->footerButtons = NULL; + iRelease(d->view.doc); + d->view.doc = new_GmDocument(); + d->state = fetching_RequestState; + d->flags |= fromCache_DocumentWidgetFlag; + /* Do the fetch. */ { + d->initNormScrollY = normScrollY; + /* Use the cached response data. */ + updateTrust_DocumentWidget_(d, resp); + d->sourceTime = resp->when; + d->sourceStatus = success_GmStatusCode; + format_String(&d->sourceHeader, cstr_Lang("pageinfo.header.cached")); + set_Block(&d->sourceContent, &resp->body); + if (!cachedDoc) { + updateWidthAndRedoLayout_DocumentView_(&d->view); } - return iTrue; - } - else if (equal_Command(cmd, "server.copycert") && document_App() == d) { - SDL_SetClipboardText(cstrCollect_String(hexEncode_Block(d->certFingerprint))); - return iTrue; + updateDocument_DocumentWidget_(d, resp, cachedDoc, iTrue); + clear_Banner(d->banner); + updateBanner_DocumentWidget_(d); + addBannerWarnings_DocumentWidget_(d); } - else if (equal_Command(cmd, "copy") && document_App() == d && !focus_Widget()) { - iString *copied; - if (d->selectMark.start) { - iRangecc mark = d->selectMark; - if (mark.start > mark.end) { - iSwap(const char *, mark.start, mark.end); - } - copied = newRange_String(mark); - } - else { - /* Full document. */ - copied = copy_String(source_GmDocument(d->view.doc)); - } - SDL_SetClipboardText(cstr_String(copied)); - delete_String(copied); - if (flags_Widget(w) & touchDrag_WidgetFlag) { - postCommand_Widget(w, "document.select arg:0"); + d->state = ready_RequestState; + postProcessRequestContent_DocumentWidget_(d, iTrue); + resetScroll_DocumentView_(&d->view); + init_Anim(&d->view.scrollY.pos, d->initNormScrollY * pageHeight_DocumentView_(&d->view)); + updateVisible_DocumentView_(&d->view); + moveSpan_SmoothScroll(&d->view.scrollY, 0, 0); /* clamp position to new max */ + updateSideOpacity_DocumentView_(&d->view, iFalse); + cacheDocumentGlyphs_DocumentWidget_(d); + d->view.drawBufs->flags |= updateTimestampBuf_DrawBufsFlag | updateSideBuf_DrawBufsFlag; + d->flags &= ~(urlChanged_DocumentWidgetFlag | drawDownloadCounter_DocumentWidgetFlag); + postCommandf_Root( + as_Widget(d)->root, "document.changed doc:%p url:%s", d, cstr_String(d->mod.url)); +} + +static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) { + const iRecentUrl *recent = constMostRecentUrl_History(d->mod.history); + if (recent && recent->cachedResponse && equalCase_String(&recent->url, d->mod.url)) { + iChangeFlags(d->flags, + openedFromSidebar_DocumentWidgetFlag, + recent->flags.openedFromSidebar); + updateFromCachedResponse_DocumentWidget_( + d, recent->normScrollY, recent->cachedResponse, recent->cachedDoc); + if (!recent->cachedDoc) { + /* We have a cached copy now. */ + setCachedDocument_History(d->mod.history, d->view.doc, iFalse); } return iTrue; } - else if (equal_Command(cmd, "document.copylink") && document_App() == d) { - if (d->contextLink) { - SDL_SetClipboardText(cstr_String(canonicalUrl_String(absoluteUrl_String( - d->mod.url, linkUrl_GmDocument(d->view.doc, d->contextLink->linkId))))); - } - else { - SDL_SetClipboardText(cstr_String(canonicalUrl_String(d->mod.url))); - } - return iTrue; + else if (!isEmpty_String(d->mod.url)) { + fetch_DocumentWidget_(d); } - else if (equalWidget_Command(cmd, w, "document.downloadlink")) { - if (d->contextLink) { - const iGmLinkId linkId = d->contextLink->linkId; - setUrl_Media(media_GmDocument(d->view.doc), - linkId, - download_MediaType, - linkUrl_GmDocument(d->view.doc, linkId)); - requestMedia_DocumentWidget_(d, linkId, iFalse /* no filters */); - redoLayout_GmDocument(d->view.doc); /* inline downloader becomes visible */ - updateVisible_DocumentView_(&d->view); - invalidate_DocumentWidget_(d); - refresh_Widget(w); - } - return iTrue; + if (recent) { + /* Retain scroll position in refetched content as well. */ + d->initNormScrollY = recent->normScrollY; } - else if (equal_Command(cmd, "document.input.submit") && document_Command(cmd) == d) { - postCommandf_Root(w->root, - /* use the `redirect:1` argument to cause the input query URL to be - replaced in History; we don't want to navigate onto it */ - "open redirect:1 url:%s", - cstrCollect_String(makeQueryUrl_DocumentWidget_ - (d, collect_String(suffix_Command(cmd, "value"))))); - return iTrue; + return iFalse; +} + +static void refreshWhileScrolling_DocumentWidget_(iAny *ptr) { + iAssert(isInstance_Object(ptr, &Class_DocumentWidget)); + iDocumentWidget *d = ptr; + iDocumentView *view = &d->view; + updateVisible_DocumentView_(view); + refresh_Widget(d); + if (view->animWideRunId) { + for (const iGmRun *r = view->animWideRunRange.start; r != view->animWideRunRange.end; r++) { + insert_PtrSet(view->invalidRuns, r); + } } - else if (equal_Command(cmd, "valueinput.cancelled") && - equal_Rangecc(range_Command(cmd, "id"), "document.input.submit") && document_App() == d) { - postCommand_Root(get_Root(), "navigate.back"); - return iTrue; + if (isFinished_Anim(&view->animWideRunOffset)) { + view->animWideRunId = 0; } - else if (equalWidget_Command(cmd, w, "document.request.updated") && - id_GmRequest(d->request) == argU32Label_Command(cmd, "reqid")) { -// set_Block(&d->sourceContent, &lockResponse_GmRequest(d->request)->body); -// unlockResponse_GmRequest(d->request); - if (document_App() == d) { - updateFetchProgress_DocumentWidget_(d); - } - checkResponse_DocumentWidget_(d); - set_Atomic(&d->isRequestUpdated, iFalse); /* ready to be notified again */ - return iFalse; + if (!isFinished_SmoothScroll(&view->scrollY) || !isFinished_Anim(&view->animWideRunOffset)) { + addTicker_App(refreshWhileScrolling_DocumentWidget_, d); } - else if (equalWidget_Command(cmd, w, "document.request.finished") && - id_GmRequest(d->request) == argU32Label_Command(cmd, "reqid")) { - d->flags &= ~fromCache_DocumentWidgetFlag; - set_Block(&d->sourceContent, body_GmRequest(d->request)); - if (!isSuccess_GmStatusCode(status_GmRequest(d->request))) { - /* TODO: Why is this here? Can it be removed? */ - format_String(&d->sourceHeader, - "%s%s", - humanReadableStatusCode_(status_GmRequest(d->request)), - cstr_String(meta_GmRequest(d->request))); - } - updateFetchProgress_DocumentWidget_(d); - checkResponse_DocumentWidget_(d); - if (category_GmStatusCode(status_GmRequest(d->request)) == categorySuccess_GmStatusCode) { - init_Anim(&d->view.scrollY.pos, d->initNormScrollY * pageHeight_DocumentView_(&d->view)); - /* TODO: unless user already scrolled! */ - } - addBannerWarnings_DocumentWidget_(d); - iChangeFlags(d->flags, - urlChanged_DocumentWidgetFlag | drawDownloadCounter_DocumentWidgetFlag, - iFalse); - d->state = ready_RequestState; - postProcessRequestContent_DocumentWidget_(d, iFalse); - /* The response may be cached. */ - if (d->request) { - iAssert(~d->flags & animationPlaceholder_DocumentWidgetFlag); - iAssert(~d->flags & fromCache_DocumentWidgetFlag); - if (!equal_Rangecc(urlScheme_String(d->mod.url), "about") && - (startsWithCase_String(meta_GmRequest(d->request), "text/") || - !cmp_String(&d->sourceMime, mimeType_Gempub))) { - setCachedResponse_History(d->mod.history, lockResponse_GmRequest(d->request)); - unlockResponse_GmRequest(d->request); - } - } - iReleasePtr(&d->request); - updateVisible_DocumentView_(&d->view); - d->view.drawBufs->flags |= updateSideBuf_DrawBufsFlag; - postCommandf_Root(w->root, - "document.changed doc:%p status:%d url:%s", - d, - d->sourceStatus, - cstr_String(d->mod.url)); - /* Check for a pending goto. */ - if (!isEmpty_String(&d->pendingGotoHeading)) { - scrollToHeading_DocumentView_(&d->view, cstr_String(&d->pendingGotoHeading)); - clear_String(&d->pendingGotoHeading); - } - cacheDocumentGlyphs_DocumentWidget_(d); - return iFalse; + if (isFinished_SmoothScroll(&view->scrollY)) { + iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iFalse); + updateHover_DocumentView_(view, mouseCoord_Window(get_Window(), 0)); } - else if (equal_Command(cmd, "document.translate") && d == document_App()) { - if (!d->translation) { - d->translation = new_Translation(d); - if (isUsingPanelLayout_Mobile()) { - const iRect safe = safeRect_Root(w->root); - d->translation->dlg->rect.pos = windowToLocal_Widget(w, zero_I2()); - d->translation->dlg->rect.size = safe.size; - } - } - return iTrue; +} + +static void scrollBegan_DocumentWidget_(iAnyObject *any, int offset, uint32_t duration) { + iDocumentWidget *d = any; + /* Get rid of link numbers when scrolling. */ + if (offset && d->flags & showLinkNumbers_DocumentWidgetFlag) { + setLinkNumberMode_DocumentWidget_(d, iFalse); + invalidateVisibleLinks_DocumentView_(&d->view); } - else if (startsWith_CStr(cmd, "translation.") && d->translation) { - const iBool wasHandled = handleCommand_Translation(d->translation, cmd); - if (isFinished_Translation(d->translation)) { - delete_Translation(d->translation); - d->translation = NULL; + /* Show and hide toolbar on scroll. */ + if (deviceType_App() == phone_AppDeviceType) { + const float normPos = normScrollPos_DocumentView_(&d->view); + if (prefs_App()->hideToolbarOnScroll && iAbs(offset) > 5 && normPos >= 0) { + showToolbar_Root(as_Widget(d)->root, offset < 0); } - return wasHandled; } - else if (equal_Command(cmd, "document.upload") && d == document_App()) { - if (findChild_Widget(root_Widget(w), "upload")) { - return iTrue; /* already open */ - } - const iBool isGemini = equalCase_Rangecc(urlScheme_String(d->mod.url), "gemini"); - if (isGemini || equalCase_Rangecc(urlScheme_String(d->mod.url), "titan")) { - iUploadWidget *upload = new_UploadWidget(); - setUrl_UploadWidget(upload, d->mod.url); - setResponseViewer_UploadWidget(upload, d); - addChild_Widget(get_Root()->widget, iClob(upload)); - setupSheetTransition_Mobile(as_Widget(upload), iTrue); - postRefresh_App(); - } - return iTrue; + updateVisible_DocumentView_(&d->view); + refresh_Widget(as_Widget(d)); + if (duration > 0) { + iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iTrue); + addTicker_App(refreshWhileScrolling_DocumentWidget_, d); } - else if (equal_Command(cmd, "media.updated") || equal_Command(cmd, "media.finished")) { - return handleMediaCommand_DocumentWidget_(d, cmd); +} + +static void togglePreFold_DocumentWidget_(iDocumentWidget *d, uint16_t preId) { + d->view.hoverPre = NULL; + d->view.hoverAltPre = NULL; + d->selectMark = iNullRange; + foldPre_GmDocument(d->view.doc, preId); + redoLayout_GmDocument(d->view.doc); + clampScroll_DocumentView_(&d->view); + updateHover_DocumentView_(&d->view, mouseCoord_Window(get_Window(), 0)); + invalidate_DocumentWidget_(d); + refresh_Widget(as_Widget(d)); +} + +static iString *makeQueryUrl_DocumentWidget_(const iDocumentWidget *d, + const iString *userEnteredText) { + iString *url = copy_String(d->mod.url); + /* Remove the existing query string. */ + const size_t qPos = indexOfCStr_String(url, "?"); + if (qPos != iInvalidPos) { + remove_Block(&url->chars, qPos, iInvalidSize); } - else if (equal_Command(cmd, "media.player.started")) { - /* When one media player starts, pause the others that may be playing. */ - const iPlayer *startedPlr = pointerLabel_Command(cmd, "player"); - const iMedia * media = media_GmDocument(d->view.doc); - const size_t num = numAudio_Media(media); - for (size_t id = 1; id <= num; id++) { - iPlayer *plr = audioPlayer_Media(media, (iMediaId){ audio_MediaType, id }); - if (plr != startedPlr) { - setPaused_Player(plr, iTrue); - } + appendCStr_String(url, "?"); + iString *cleaned = copy_String(userEnteredText); + if (deviceType_App() != desktop_AppDeviceType) { + trimEnd_String(cleaned); /* autocorrect may insert an extra space */ + if (isEmpty_String(cleaned)) { + set_String(cleaned, userEnteredText); /* user wanted just spaces? */ } } - else if (equal_Command(cmd, "media.player.update")) { - updateMedia_DocumentWidget_(d); - return iFalse; + append_String(url, collect_String(urlEncode_String(cleaned))); + delete_String(cleaned); + return url; +} + +static void inputQueryValidator_(iInputWidget *input, void *context) { + iDocumentWidget *d = context; + iString *url = makeQueryUrl_DocumentWidget_(d, text_InputWidget(input)); + iWidget *dlg = parent_Widget(input); + iLabelWidget *counter = findChild_Widget(dlg, "valueinput.counter"); + iAssert(counter); + int avail = 1024 - (int) size_String(url); + setFlags_Widget(findChild_Widget(dlg, "default"), disabled_WidgetFlag, avail < 0); + setEnterKeyEnabled_InputWidget(input, avail >= 0); + int len = length_String(text_InputWidget(input)); + if (len > 1024) { + iString *trunc = copy_String(text_InputWidget(input)); + truncate_String(trunc, 1024); + setText_InputWidget(input, trunc); + delete_String(trunc); } - else if (equal_Command(cmd, "document.stop") && document_App() == d) { - if (cancelRequest_DocumentWidget_(d, iTrue /* navigate back */)) { - return iTrue; - } + setTextCStr_LabelWidget(counter, format_CStr("%d", avail)); /* Gemini URL maxlen */ + setTextColor_LabelWidget(counter, + avail < 0 ? uiTextCaution_ColorId : + avail < 128 ? uiTextStrong_ColorId + : uiTextDim_ColorId); + delete_String(url); + arrange_Widget(findChild_Widget(dlg, "dialogbuttons")); +} + +static const char *humanReadableStatusCode_(enum iGmStatusCode code) { + if (code <= 0) { + return ""; } - else if (equalWidget_Command(cmd, w, "document.media.save")) { - const iGmLinkId linkId = argLabel_Command(cmd, "link"); - const iMediaRequest *media = findMediaRequest_DocumentWidget_(d, linkId); - if (media) { - saveToDownloads_(url_GmRequest(media->req), meta_GmRequest(media->req), - body_GmRequest(media->req), iTrue); - } + return format_CStr("%d ", code); +} + +static void checkResponse_DocumentWidget_(iDocumentWidget *d) { + if (!d->request) { + return; } - else if (equal_Command(cmd, "document.save") && document_App() == d) { - if (d->request) { - makeSimpleMessage_Widget(uiTextCaution_ColorEscape "${heading.save.incomplete}", - "${dlg.save.incomplete}"); - } - else if (!isEmpty_Block(&d->sourceContent)) { - const iBool doOpen = argLabel_Command(cmd, "open"); - const iString *savePath = saveToDownloads_(d->mod.url, &d->sourceMime, - &d->sourceContent, !doOpen); - if (!isEmpty_String(savePath) && doOpen) { - postCommandf_Root( - w->root, "!open url:%s", cstrCollect_String(makeFileUrl_String(savePath))); - } - } - return iTrue; + enum iGmStatusCode statusCode = status_GmRequest(d->request); + if (statusCode == none_GmStatusCode) { + return; } - else if (equal_Command(cmd, "document.reload") && document_Command(cmd) == d) { - d->initNormScrollY = normScrollPos_DocumentView_(&d->view); - if (equalCase_Rangecc(urlScheme_String(d->mod.url), "titan")) { - /* Reopen so the Upload dialog gets shown. */ - postCommandf_App("open url:%s", cstr_String(d->mod.url)); - return iTrue; + iGmResponse *resp = lockResponse_GmRequest(d->request); + if (d->state == fetching_RequestState) { + d->state = receivedPartialResponse_RequestState; + d->flags &= ~fromCache_DocumentWidgetFlag; + updateTrust_DocumentWidget_(d, resp); + if (isSuccess_GmStatusCode(statusCode)) { + clear_Banner(d->banner); + updateTheme_DocumentWidget_(d); } - fetch_DocumentWidget_(d); - return iTrue; - } - else if (equal_Command(cmd, "document.linkkeys") && document_App() == d) { - if (argLabel_Command(cmd, "release")) { - setLinkNumberMode_DocumentWidget_(d, iFalse); + if (~d->certFlags & trusted_GmCertFlag && + isSuccess_GmStatusCode(statusCode) && + equalCase_Rangecc(urlScheme_String(d->mod.url), "gemini")) { + statusCode = tlsServerCertificateNotVerified_GmStatusCode; } - else if (argLabel_Command(cmd, "more")) { - if (d->flags & showLinkNumbers_DocumentWidgetFlag && - d->ordinalMode == homeRow_DocumentLinkOrdinalMode) { - const size_t numKeys = iElemCount(homeRowKeys_); - const iGmRun *last = lastVisibleLink_DocumentView_(&d->view); - if (!last) { - d->ordinalBase = 0; + init_Anim(&d->view.sideOpacity, 0); + init_Anim(&d->view.altTextOpacity, 0); + format_String(&d->sourceHeader, + "%s%s", + humanReadableStatusCode_(statusCode), + isEmpty_String(&resp->meta) && !isSuccess_GmStatusCode(statusCode) + ? get_GmError(statusCode)->title + : cstr_String(&resp->meta)); + d->sourceStatus = statusCode; + switch (category_GmStatusCode(statusCode)) { + case categoryInput_GmStatusCode: { + /* Let the navigation history know that we have been to this URL even though + it is only displayed as an input dialog. */ + visitUrl_Visited(visited_App(), d->mod.url, transient_VisitedUrlFlag); + iUrl parts; + init_Url(&parts, d->mod.url); + iWidget *dlg = makeValueInput_Widget( + as_Widget(d), + NULL, + format_CStr(uiHeading_ColorEscape "%s", cstr_Rangecc(parts.host)), + isEmpty_String(&resp->meta) + ? format_CStr(cstr_Lang("dlg.input.prompt"), cstr_Rangecc(parts.path)) + : cstr_String(&resp->meta), + uiTextCaution_ColorEscape "${dlg.input.send}", + format_CStr("!document.input.submit doc:%p", d)); + iWidget *buttons = findChild_Widget(dlg, "dialogbuttons"); + iLabelWidget *lineBreak = NULL; + if (statusCode != sensitiveInput_GmStatusCode) { + /* The line break and URL length counters are positioned differently on mobile. + There is no line breaks in sensitive input. */ + if (deviceType_App() == desktop_AppDeviceType) { + iString *keyStr = collectNew_String(); + toString_Sym(SDLK_RETURN, + lineBreakKeyMod_ReturnKeyBehavior(prefs_App()->returnKey), + keyStr); + lineBreak = new_LabelWidget( + format_CStr("${dlg.input.linebreak}" uiTextAction_ColorEscape " %s", + cstr_String(keyStr)), + NULL); + insertChildAfter_Widget(buttons, iClob(lineBreak), 0); + } + else { +#if !defined (iPlatformAppleMobile) + lineBreak = new_LabelWidget("${dlg.input.linebreak}", "text.insert arg:10"); +#endif + } + if (lineBreak) { + setFlags_Widget(as_Widget(lineBreak), frameless_WidgetFlag, iTrue); + setTextColor_LabelWidget(lineBreak, uiTextDim_ColorId); + } + } + iWidget *counter = (iWidget *) new_LabelWidget("", NULL); + setId_Widget(counter, "valueinput.counter"); + setFlags_Widget(counter, frameless_WidgetFlag | resizeToParentHeight_WidgetFlag, iTrue); + if (deviceType_App() == desktop_AppDeviceType) { + addChildPos_Widget(buttons, iClob(counter), front_WidgetAddPos); } else { - d->ordinalBase += numKeys; - if (visibleLinkOrdinal_DocumentView_(&d->view, last->linkId) < d->ordinalBase) { - d->ordinalBase = 0; + insertChildAfter_Widget(buttons, iClob(counter), 1); + } + if (lineBreak && deviceType_App() != desktop_AppDeviceType) { + addChildPos_Widget(buttons, iClob(lineBreak), front_WidgetAddPos); + } + /* Menu for additional actions, past entries. */ { + iMenuItem items[] = { { "${menu.input.precedingline}", + SDLK_v, + KMOD_PRIMARY | KMOD_SHIFT, + format_CStr("!valueinput.set ptr:%p text:%s", + buttons, + cstr_String(&d->linePrecedingLink)) } }; + iLabelWidget *menu = makeMenuButton_LabelWidget(midEllipsis_Icon, items, 1); + if (deviceType_App() == desktop_AppDeviceType) { + addChildPos_Widget(buttons, iClob(menu), front_WidgetAddPos); } + else { + insertChildAfterFlags_Widget(buttons, iClob(menu), 0, + frameless_WidgetFlag | noBackground_WidgetFlag); + setFont_LabelWidget(menu, font_LabelWidget((iLabelWidget *) lastChild_Widget(buttons))); + setTextColor_LabelWidget(menu, uiTextAction_ColorId); + } + } + setValidator_InputWidget(findChild_Widget(dlg, "input"), inputQueryValidator_, d); + setSensitiveContent_InputWidget(findChild_Widget(dlg, "input"), + statusCode == sensitiveInput_GmStatusCode); + if (document_App() != d) { + postCommandf_App("tabs.switch page:%p", d); } + else { + updateTheme_DocumentWidget_(d); + } + break; } - else if (~d->flags & showLinkNumbers_DocumentWidgetFlag) { - d->ordinalMode = homeRow_DocumentLinkOrdinalMode; - d->ordinalBase = 0; - setLinkNumberMode_DocumentWidget_(d, iTrue); - } - } - else { - d->ordinalMode = arg_Command(cmd); - d->ordinalBase = 0; - setLinkNumberMode_DocumentWidget_(d, iTrue); - iChangeFlags(d->flags, setHoverViaKeys_DocumentWidgetFlag, - argLabel_Command(cmd, "hover") != 0); - iChangeFlags(d->flags, newTabViaHomeKeys_DocumentWidgetFlag, - argLabel_Command(cmd, "newtab") != 0); + case categorySuccess_GmStatusCode: + if (d->flags & urlChanged_DocumentWidgetFlag) { + /* Keep scroll position when reloading the same page. */ + resetScroll_DocumentView_(&d->view); + } + d->view.scrollY.pullActionTriggered = 0; + pauseAllPlayers_Media(media_GmDocument(d->view.doc), iTrue); + iReleasePtr(&d->view.doc); /* new content incoming */ + delete_Gempub(d->sourceGempub); + d->sourceGempub = NULL; + destroy_Widget(d->footerButtons); + d->footerButtons = NULL; + d->view.doc = new_GmDocument(); + resetWideRuns_DocumentView_(&d->view); + updateDocument_DocumentWidget_(d, resp, NULL, iTrue); + break; + case categoryRedirect_GmStatusCode: + if (isEmpty_String(&resp->meta)) { + showErrorPage_DocumentWidget_(d, invalidRedirect_GmStatusCode, NULL); + } + else { + /* Only accept redirects that use gemini scheme. */ + const iString *dstUrl = absoluteUrl_String(d->mod.url, &resp->meta); + const iRangecc srcScheme = urlScheme_String(d->mod.url); + const iRangecc dstScheme = urlScheme_String(dstUrl); + if (d->redirectCount >= 5) { + showErrorPage_DocumentWidget_(d, tooManyRedirects_GmStatusCode, dstUrl); + } + /* Redirects with the same scheme are automatic, and switching automatically + between "gemini" and "titan" is allowed. */ + else if (equalRangeCase_Rangecc(dstScheme, srcScheme) || + (equalCase_Rangecc(srcScheme, "titan") && + equalCase_Rangecc(dstScheme, "gemini")) || + (equalCase_Rangecc(srcScheme, "gemini") && + equalCase_Rangecc(dstScheme, "titan"))) { + visitUrl_Visited(visited_App(), d->mod.url, transient_VisitedUrlFlag); + postCommandf_Root(as_Widget(d)->root, + "open doc:%p redirect:%d url:%s", d, d->redirectCount + 1, cstr_String(dstUrl)); + } + else { + /* Scheme changes must be manually approved. */ + showErrorPage_DocumentWidget_(d, schemeChangeRedirect_GmStatusCode, dstUrl); + } + unlockResponse_GmRequest(d->request); + iReleasePtr(&d->request); + } + break; + default: + if (isDefined_GmError(statusCode)) { + showErrorPage_DocumentWidget_(d, statusCode, &resp->meta); + } + else if (category_GmStatusCode(statusCode) == + categoryTemporaryFailure_GmStatusCode) { + showErrorPage_DocumentWidget_( + d, temporaryFailure_GmStatusCode, &resp->meta); + } + else if (category_GmStatusCode(statusCode) == + categoryPermanentFailure_GmStatusCode) { + showErrorPage_DocumentWidget_( + d, permanentFailure_GmStatusCode, &resp->meta); + } + else { + showErrorPage_DocumentWidget_(d, unknownStatusCode_GmStatusCode, &resp->meta); + } + break; } - invalidateVisibleLinks_DocumentView_(&d->view); - refresh_Widget(d); - return iTrue; } - else if (equal_Command(cmd, "navigate.back") && document_App() == d) { - if (d->request) { - postCommandf_Root(w->root, - "document.request.cancelled doc:%p url:%s", d, cstr_String(d->mod.url)); - iReleasePtr(&d->request); - updateFetchProgress_DocumentWidget_(d); + else if (d->state == receivedPartialResponse_RequestState) { + d->flags &= ~fromCache_DocumentWidgetFlag; + switch (category_GmStatusCode(statusCode)) { + case categorySuccess_GmStatusCode: + /* More content available. */ + updateDocument_DocumentWidget_(d, resp, NULL, iFalse); + break; + default: + break; } - goBack_History(d->mod.history); - return iTrue; - } - else if (equal_Command(cmd, "navigate.forward") && document_App() == d) { - goForward_History(d->mod.history); - return iTrue; } - else if (equal_Command(cmd, "navigate.parent") && document_App() == d) { - iUrl parts; - init_Url(&parts, d->mod.url); - /* Remove the last path segment. */ - if (size_Range(&parts.path) > 1) { - if (parts.path.end[-1] == '/') { - parts.path.end--; - } - while (parts.path.end > parts.path.start) { - if (parts.path.end[-1] == '/') break; - parts.path.end--; - } - postCommandf_Root(w->root, - "open url:%s", - cstr_Rangecc((iRangecc){ constBegin_String(d->mod.url), parts.path.end })); + unlockResponse_GmRequest(d->request); +} + +static void removeMediaRequest_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId) { + iForEach(ObjectList, i, d->media) { + iMediaRequest *req = (iMediaRequest *) i.object; + if (req->linkId == linkId) { + remove_ObjectListIterator(&i); + break; } - return iTrue; } - else if (equal_Command(cmd, "navigate.root") && document_App() == d) { - postCommandf_Root(w->root, "open url:%s/", cstr_Rangecc(urlRoot_String(d->mod.url))); - return iTrue; - } - else if (equalWidget_Command(cmd, w, "scroll.moved")) { - init_Anim(&d->view.scrollY.pos, arg_Command(cmd)); - updateVisible_DocumentView_(&d->view); +} + +static iBool requestMedia_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId, iBool enableFilters) { + if (!findMediaRequest_DocumentWidget_(d, linkId)) { + const iString *mediaUrl = absoluteUrl_String(d->mod.url, linkUrl_GmDocument(d->view.doc, linkId)); + pushBack_ObjectList(d->media, iClob(new_MediaRequest(d, linkId, mediaUrl, enableFilters))); + invalidate_DocumentWidget_(d); return iTrue; } - else if (equal_Command(cmd, "scroll.page") && document_App() == d) { - const int dir = arg_Command(cmd); - if (dir > 0 && !argLabel_Command(cmd, "repeat") && - prefs_App()->loadImageInsteadOfScrolling && - fetchNextUnfetchedImage_DocumentWidget_(d)) { - return iTrue; + return iFalse; +} + +static iBool isDownloadRequest_DocumentWidget(const iDocumentWidget *d, const iMediaRequest *req) { + return findMediaForLink_Media(constMedia_GmDocument(d->view.doc), req->linkId, download_MediaType).type != 0; +} + +static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) { + iMediaRequest *req = pointerLabel_Command(cmd, "request"); + iBool isOurRequest = iFalse; + /* This request may already be deleted so treat the pointer with caution. */ + iConstForEach(ObjectList, m, d->media) { + if (m.object == req) { + isOurRequest = iTrue; + break; } - const float amount = argLabel_Command(cmd, "full") != 0 ? 1.0f : 0.5f; - smoothScroll_DocumentView_(&d->view, - dir * amount * - height_Rect(documentBounds_DocumentView_(&d->view)), - smoothDuration_DocumentWidget_(keyboard_ScrollType)); - return iTrue; - } - else if (equal_Command(cmd, "scroll.top") && document_App() == d) { - init_Anim(&d->view.scrollY.pos, 0); - invalidate_VisBuf(d->view.visBuf); - clampScroll_DocumentView_(&d->view); - updateVisible_DocumentView_(&d->view); - refresh_Widget(w); - return iTrue; } - else if (equal_Command(cmd, "scroll.bottom") && document_App() == d) { - updateScrollMax_DocumentView_(&d->view); /* scrollY.max might not be fully updated */ - init_Anim(&d->view.scrollY.pos, d->view.scrollY.max); - invalidate_VisBuf(d->view.visBuf); - clampScroll_DocumentView_(&d->view); - updateVisible_DocumentView_(&d->view); - refresh_Widget(w); - return iTrue; + if (!isOurRequest) { + return iFalse; } - else if (equal_Command(cmd, "scroll.step") && document_App() == d) { - const int dir = arg_Command(cmd); - if (dir > 0 && !argLabel_Command(cmd, "repeat") && - prefs_App()->loadImageInsteadOfScrolling && - fetchNextUnfetchedImage_DocumentWidget_(d)) { - return iTrue; + if (equal_Command(cmd, "media.updated")) { + /* Pass new data to media players. */ + const enum iGmStatusCode code = status_GmRequest(req->req); + if (isSuccess_GmStatusCode(code)) { + iGmResponse *resp = lockResponse_GmRequest(req->req); + if (isDownloadRequest_DocumentWidget(d, req) || + startsWith_String(&resp->meta, "audio/")) { + /* TODO: Use a helper? This is same as below except for the partialData flag. */ + if (setData_Media(media_GmDocument(d->view.doc), + req->linkId, + &resp->meta, + &resp->body, + partialData_MediaFlag | allowHide_MediaFlag)) { + redoLayout_GmDocument(d->view.doc); + } + updateVisible_DocumentView_(&d->view); + invalidate_DocumentWidget_(d); + refresh_Widget(as_Widget(d)); + } + unlockResponse_GmRequest(req->req); } - smoothScroll_DocumentView_(&d->view, - 3 * lineHeight_Text(paragraph_FontId) * dir, - smoothDuration_DocumentWidget_(keyboard_ScrollType)); + /* Update the link's progress. */ + invalidateLink_DocumentView_(&d->view, req->linkId); + refresh_Widget(d); return iTrue; } - else if (equal_Command(cmd, "document.goto") && document_App() == d) { - const char *heading = suffixPtr_Command(cmd, "heading"); - if (heading) { - if (isRequestOngoing_DocumentWidget(d)) { - /* Scroll position set when request finishes. */ - setCStr_String(&d->pendingGotoHeading, heading); - return iTrue; + else if (equal_Command(cmd, "media.finished")) { + const enum iGmStatusCode code = status_GmRequest(req->req); + /* Give the media to the document for presentation. */ + if (isSuccess_GmStatusCode(code)) { + if (isDownloadRequest_DocumentWidget(d, req) || + startsWith_String(meta_GmRequest(req->req), "image/") || + startsWith_String(meta_GmRequest(req->req), "audio/")) { + setData_Media(media_GmDocument(d->view.doc), + req->linkId, + meta_GmRequest(req->req), + body_GmRequest(req->req), + allowHide_MediaFlag); + redoLayout_GmDocument(d->view.doc); + iZap(d->view.visibleRuns); /* pointers invalidated */ + updateVisible_DocumentView_(&d->view); + invalidate_DocumentWidget_(d); + refresh_Widget(as_Widget(d)); } - scrollToHeading_DocumentView_(&d->view, heading); - return iTrue; } - const char *loc = pointerLabel_Command(cmd, "loc"); - const iGmRun *run = findRunAtLoc_GmDocument(d->view.doc, loc); - if (run) { - scrollTo_DocumentView_(&d->view, run->visBounds.pos.y, iFalse); + else { + const iGmError *err = get_GmError(code); + makeSimpleMessage_Widget(format_CStr(uiTextCaution_ColorEscape "%s", err->title), err->info); + removeMediaRequest_DocumentWidget_(d, req->linkId); } return iTrue; } - else if ((equal_Command(cmd, "find.next") || equal_Command(cmd, "find.prev")) && - document_App() == d) { - const int dir = equal_Command(cmd, "find.next") ? +1 : -1; - iRangecc (*finder)(const iGmDocument *, const iString *, const char *) = - dir > 0 ? findText_GmDocument : findTextBefore_GmDocument; - iInputWidget *find = findWidget_App("find.input"); - if (isEmpty_String(text_InputWidget(find))) { - d->foundMark = iNullRange; - } - else { - const iBool wrap = d->foundMark.start != NULL; - d->foundMark = finder(d->view.doc, text_InputWidget(find), dir > 0 ? d->foundMark.end - : d->foundMark.start); - if (!d->foundMark.start && wrap) { - /* Wrap around. */ - d->foundMark = finder(d->view.doc, text_InputWidget(find), NULL); - } - if (d->foundMark.start) { - const iGmRun *found; - if ((found = findRunAtLoc_GmDocument(d->view.doc, d->foundMark.start)) != NULL) { - scrollTo_DocumentView_(&d->view, mid_Rect(found->bounds).y, iTrue); + return iFalse; +} + +static iBool fetchNextUnfetchedImage_DocumentWidget_(iDocumentWidget *d) { + iConstForEach(PtrArray, i, &d->view.visibleLinks) { + const iGmRun *run = i.ptr; + if (run->linkId && run->mediaType == none_MediaType && + ~run->flags & decoration_GmRunFlag) { + const int linkFlags = linkFlags_GmDocument(d->view.doc, run->linkId); + if (isMediaLink_GmDocument(d->view.doc, run->linkId) && + linkFlags & imageFileExtension_GmLinkFlag && + ~linkFlags & content_GmLinkFlag && ~linkFlags & permanent_GmLinkFlag ) { + if (requestMedia_DocumentWidget_(d, run->linkId, iTrue)) { + return iTrue; } } } - if (flags_Widget(w) & touchDrag_WidgetFlag) { - postCommand_Root(w->root, "document.select arg:0"); /* we can't handle both at the same time */ + } + return iFalse; +} + +static const iString *saveToDownloads_(const iString *url, const iString *mime, const iBlock *content, + iBool showDialog) { + const iString *savePath = downloadPathForUrl_App(url, mime); + /* Write the file. */ { + iFile *f = new_File(savePath); + if (open_File(f, writeOnly_FileMode)) { + write_File(f, content); + close_File(f); + const size_t size = size_Block(content); + const iBool isMega = size >= 1000000; +#if defined (iPlatformAppleMobile) + exportDownloadedFile_iOS(savePath); +#else + if (showDialog) { + const iMenuItem items[2] = { + { "${dlg.save.opendownload}", 0, 0, + format_CStr("!open url:%s", cstrCollect_String(makeFileUrl_String(savePath))) }, + { "${dlg.message.ok}", 0, 0, "message.ok" }, + }; + makeMessage_Widget(uiHeading_ColorEscape "${heading.save}", + format_CStr("%s\n${dlg.save.size} %.3f %s", + cstr_String(path_File(f)), + isMega ? size / 1.0e6f : (size / 1.0e3f), + isMega ? "${mb}" : "${kb}"), + items, + iElemCount(items)); + } +#endif + return savePath; + } + else { + makeSimpleMessage_Widget(uiTextCaution_ColorEscape "${heading.save.error}", + strerror(errno)); } - invalidateWideRunsWithNonzeroOffset_DocumentView_(&d->view); /* markers don't support offsets */ - resetWideRuns_DocumentView_(&d->view); - refresh_Widget(w); - return iTrue; + iRelease(f); } - else if (equal_Command(cmd, "find.clearmark")) { - if (d->foundMark.start) { - d->foundMark = iNullRange; - refresh_Widget(w); - } - return iTrue; + return collectNew_String(); +} + +static void addAllLinks_(void *context, const iGmRun *run) { + iPtrArray *links = context; + if (~run->flags & decoration_GmRunFlag && run->linkId) { + pushBack_PtrArray(links, run); } - else if (equal_Command(cmd, "bookmark.links") && document_App() == d) { - iPtrArray *links = collectNew_PtrArray(); - render_GmDocument(d->view.doc, (iRangei){ 0, size_GmDocument(d->view.doc).y }, addAllLinks_, links); - /* Find links that aren't already bookmarked. */ - iForEach(PtrArray, i, links) { - const iGmRun *run = i.ptr; - uint32_t bmid; - if ((bmid = findUrl_Bookmarks(bookmarks_App(), - linkUrl_GmDocument(d->view.doc, run->linkId))) != 0) { - const iBookmark *bm = get_Bookmarks(bookmarks_App(), bmid); - /* We can import local copies of remote bookmarks. */ - if (~bm->flags & remote_BookmarkFlag) { - remove_PtrArrayIterator(&i); - } +} + +static iBool handlePinch_DocumentWidget_(iDocumentWidget *d, const char *cmd) { + if (equal_Command(cmd, "pinch.began")) { + d->pinchZoomInitial = d->pinchZoomPosted = prefs_App()->zoomPercent; + d->flags |= pinchZoom_DocumentWidgetFlag; + refresh_Widget(d); + } + else if (equal_Command(cmd, "pinch.moved")) { + const float rel = argf_Command(cmd); + int zoom = iRound(d->pinchZoomInitial * rel / 5.0f) * 5; + zoom = iClamp(zoom, 50, 200); + if (d->pinchZoomPosted != zoom) { +#if defined (iPlatformAppleMobile) + if (zoom == 100) { + playHapticEffect_iOS(tap_HapticEffect); } +#endif + d->pinchZoomPosted = zoom; + postCommandf_App("zoom.set arg:%d", zoom); } - if (!isEmpty_PtrArray(links)) { - if (argLabel_Command(cmd, "confirm")) { - const size_t count = size_PtrArray(links); - makeQuestion_Widget( - uiHeading_ColorEscape "${heading.import.bookmarks}", - formatCStrs_Lang("dlg.import.found.n", count), - (iMenuItem[]){ { "${cancel}" }, - { format_CStr(cstrCount_Lang("dlg.import.add.n", (int) count), - uiTextAction_ColorEscape, - count), - 0, - 0, - "bookmark.links" } }, - 2); + } + else if (equal_Command(cmd, "pinch.ended")) { + d->flags &= ~pinchZoom_DocumentWidgetFlag; + refresh_Widget(d); + } + return iTrue; +} + +static void swap_DocumentWidget_(iDocumentWidget *d, iGmDocument *doc, + iDocumentWidget *swapBuffersWith) { + if (doc) { + iAssert(isInstance_Object(doc, &Class_GmDocument)); + replaceDocument_DocumentWidget_(d, doc); + iSwap(iBanner *, d->banner, swapBuffersWith->banner); + setOwner_Banner(d->banner, d); + setOwner_Banner(swapBuffersWith->banner, swapBuffersWith); + swap_DocumentView_(&d->view, &swapBuffersWith->view); +// invalidate_DocumentWidget_(swapBuffersWith); + } +} + +static iWidget *swipeParent_DocumentWidget_(iDocumentWidget *d) { + return findChild_Widget(as_Widget(d)->root->widget, "doctabs"); +} + +static void setUrl_DocumentWidget_(iDocumentWidget *d, const iString *url) { + url = canonicalUrl_String(url); + if (!equal_String(d->mod.url, url)) { + d->flags |= urlChanged_DocumentWidgetFlag; + set_String(d->mod.url, url); + } +} + +static void setupSwipeOverlay_DocumentWidget_(iDocumentWidget *d, iWidget *overlay) { + iWidget *w = as_Widget(d); + iWidget *swipeParent = swipeParent_DocumentWidget_(d); + /* The target takes the old document and jumps on top. */ + overlay->rect.pos = windowToInner_Widget(swipeParent, innerToWindow_Widget(w, zero_I2())); + /* Note: `innerToWindow_Widget` does not apply visual offset. */ + overlay->rect.size = w->rect.size; + setFlags_Widget(overlay, fixedPosition_WidgetFlag | fixedSize_WidgetFlag, iTrue); +// swap_DocumentWidget_(target, d->doc, d); + setFlags_Widget(as_Widget(d), refChildrenOffset_WidgetFlag, iTrue); + as_Widget(d)->offsetRef = swipeParent; + /* `overlay` animates off the screen to the right. */ + const int fromPos = value_Anim(&w->visualOffset); + const int toPos = width_Widget(overlay); + setVisualOffset_Widget(overlay, fromPos, 0, 0); + /* Bigger screen, faster swipes. */ + if (deviceType_App() == desktop_AppDeviceType) { + setVisualOffset_Widget(overlay, toPos, 250, easeOut_AnimFlag | softer_AnimFlag); + } + else { + const float devFactor = (deviceType_App() == phone_AppDeviceType ? 1.0f : 2.0f); + float swipe = iClamp(d->swipeSpeed, devFactor * 400, devFactor * 1000) * gap_UI; + uint32_t span = ((toPos - fromPos) / swipe) * 1000; + // printf("from:%d to:%d swipe:%f span:%u\n", fromPos, toPos, d->swipeSpeed, span); + setVisualOffset_Widget(overlay, toPos, span, deviceType_App() == tablet_AppDeviceType ? + easeOut_AnimFlag : 0); + } + setVisualOffset_Widget(w, 0, 0, 0); +} + +static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { + /* TODO: Cleanup + If DocumentWidget is refactored to split the document presentation from state + and request management (a new DocumentView class), plain views could be used for this + animation without having to mess with the complete state of the DocumentWidget. That + seems like a less error-prone approach -- the current implementation will likely break + down (again) if anything is changed in the document internals. + */ + iWidget *w = as_Widget(d); + /* The swipe animation is implemented in a rather complex way. It utilizes both cached + GmDocument content and temporary underlay/overlay DocumentWidgets. Depending on the + swipe direction, the DocumentWidget `d` may wait until the finger is released to actually + perform the navigation action. */ + if (equal_Command(cmd, "edgeswipe.moved")) { + //printf("[%p] responds to edgeswipe.moved\n", d); + as_Widget(d)->offsetRef = NULL; + const int side = argLabel_Command(cmd, "side"); + const int offset = arg_Command(cmd); + if (side == 1) { /* left edge */ + if (atOldest_History(d->mod.history)) { + return iTrue; } - else { - iConstForEach(PtrArray, j, links) { - const iGmRun *run = j.ptr; - add_Bookmarks(bookmarks_App(), - linkUrl_GmDocument(d->view.doc, run->linkId), - collect_String(newRange_String(run->text)), - NULL, - 0x1f588 /* pin */); + iWidget *swipeParent = swipeParent_DocumentWidget_(d); + if (findChild_Widget(swipeParent, "swipeout")) { + return iTrue; /* too fast, previous animation hasn't finished */ + } + /* The temporary "swipein" will display the previous page until the finger is lifted. */ + iDocumentWidget *swipeIn = findChild_Widget(swipeParent, "swipein"); + if (!swipeIn) { + swipeIn = new_DocumentWidget(); + swipeIn->flags |= animationPlaceholder_DocumentWidgetFlag; + setId_Widget(as_Widget(swipeIn), "swipein"); + setFlags_Widget(as_Widget(swipeIn), + disabled_WidgetFlag | refChildrenOffset_WidgetFlag | + fixedPosition_WidgetFlag | fixedSize_WidgetFlag, iTrue); + setFlags_Widget(findChild_Widget(as_Widget(swipeIn), "scroll"), hidden_WidgetFlag, iTrue); + swipeIn->widget.rect.pos = windowToInner_Widget(swipeParent, localToWindow_Widget(w, w->rect.pos)); + swipeIn->widget.rect.size = d->widget.rect.size; + swipeIn->widget.offsetRef = parent_Widget(w); + /* Use a cached document for the layer underneath. */ { + lock_History(d->mod.history); + iRecentUrl *recent = precedingLocked_History(d->mod.history); + if (recent && recent->cachedResponse) { + setUrl_DocumentWidget_(swipeIn, &recent->url); + updateFromCachedResponse_DocumentWidget_(swipeIn, + recent->normScrollY, + recent->cachedResponse, + recent->cachedDoc); + parseUser_DocumentWidget_(swipeIn); + updateBanner_DocumentWidget_(swipeIn); + } + else { + setUrlAndSource_DocumentWidget(swipeIn, &recent->url, + collectNewCStr_String("text/gemini"), + collect_Block(new_Block(0))); + } + unlock_History(d->mod.history); } - postCommand_App("bookmarks.changed"); + addChildPos_Widget(swipeParent, iClob(swipeIn), front_WidgetAddPos); } } - else { - makeSimpleMessage_Widget(uiHeading_ColorEscape "${heading.import.bookmarks}", - "${dlg.import.notnew}"); - } - return iTrue; - } - else if (equalWidget_Command(cmd, w, "menu.closed")) { - updateHover_DocumentView_(&d->view, mouseCoord_Window(get_Window(), 0)); - } - else if (equal_Command(cmd, "document.autoreload")) { - if (d->mod.reloadInterval) { - if (!isValid_Time(&d->sourceTime) || elapsedSeconds_Time(&d->sourceTime) >= - seconds_ReloadInterval_(d->mod.reloadInterval)) { - postCommand_Widget(w, "document.reload"); + if (side == 2) { /* right edge */ + if (offset < -get_Window()->pixelRatio * 10) { + int animSpan = 10; + if (!atNewest_History(d->mod.history) && ~flags_Widget(w) & dragged_WidgetFlag) { + iWidget *swipeParent = swipeParent_DocumentWidget_(d); + if (findChild_Widget(swipeParent, "swipeout")) { + return iTrue; /* too fast, previous animation hasn't finished */ + } + /* Setup the drag. `d` will be moving with the finger. */ + animSpan = 0; + postCommand_Widget(d, "navigate.forward"); + setFlags_Widget(w, dragged_WidgetFlag, iTrue); + /* Set up the swipe dummy. */ + iDocumentWidget *target = new_DocumentWidget(); + target->flags |= animationPlaceholder_DocumentWidgetFlag; + setId_Widget(as_Widget(target), "swipeout"); + /* "swipeout" takes `d`'s document and goes underneath. */ + target->widget.rect.pos = windowToInner_Widget(swipeParent, localToWindow_Widget(w, w->rect.pos)); + target->widget.rect.size = d->widget.rect.size; + setFlags_Widget(as_Widget(target), fixedPosition_WidgetFlag | fixedSize_WidgetFlag, iTrue); + swap_DocumentWidget_(target, d->view.doc, d); + addChildPos_Widget(swipeParent, iClob(target), front_WidgetAddPos); + setFlags_Widget(as_Widget(target), refChildrenOffset_WidgetFlag, iTrue); + as_Widget(target)->offsetRef = parent_Widget(w); + /* Mark it for deletion after animation finishes. */ + destroy_Widget(as_Widget(target)); + /* The `d` document will now navigate forward and be replaced with a cached + copy. However, if a cached response isn't available, we'll need to show a + blank page. */ + setUrlAndSource_DocumentWidget(d, + collectNewCStr_String("about:blank"), + collectNewCStr_String("text/gemini"), + collect_Block(new_Block(0))); + } + if (flags_Widget(w) & dragged_WidgetFlag) { + setVisualOffset_Widget(w, width_Widget(w) + + width_Widget(d) * offset / size_Root(w->root).x, + animSpan, 0); + } + else { + setVisualOffset_Widget(w, offset / 4, animSpan, 0); + } } + return iTrue; } } - else if (equal_Command(cmd, "document.autoreload.menu") && document_App() == d) { - iArray *items = collectNew_Array(sizeof(iMenuItem)); - for (int i = 0; i < max_ReloadInterval; ++i) { - pushBack_Array(items, &(iMenuItem){ - format_CStr("%s%s", ((int) d->mod.reloadInterval == i ? "&" : "*"), - label_ReloadInterval_(i)), - 0, - 0, - format_CStr("document.autoreload.set arg:%d", i) }); - } - pushBack_Array(items, &(iMenuItem){ "${cancel}", 0, 0, NULL }); - makeQuestion_Widget(uiTextAction_ColorEscape "${heading.autoreload}", - "${dlg.autoreload}", - constData_Array(items), size_Array(items)); - return iTrue; - } - else if (equal_Command(cmd, "document.autoreload.set") && document_App() == d) { - d->mod.reloadInterval = arg_Command(cmd); - } - else if (equalWidget_Command(cmd, w, "document.dismiss")) { - const iString *site = collectNewRange_String(urlRoot_String(d->mod.url)); - const int dismissed = value_SiteSpec(site, dismissWarnings_SiteSpecKey); - const int arg = argLabel_Command(cmd, "warning"); - setValue_SiteSpec(site, dismissWarnings_SiteSpecKey, dismissed | arg); - if (arg == ansiEscapes_GmDocumentWarning) { - remove_Banner(d->banner, ansiEscapes_GmStatusCode); - refresh_Widget(w); - } - return iTrue; - } - else if (startsWith_CStr(cmd, "pinch.") && document_Command(cmd) == d) { - return handlePinch_DocumentWidget_(d, cmd); - } - else if ((startsWith_CStr(cmd, "edgeswipe.") || startsWith_CStr(cmd, "swipe.")) && - document_App() == d) { - return handleSwipe_DocumentWidget_(d, cmd); - } - else if (equal_Command(cmd, "document.setmediatype") && document_App() == d) { - if (!isRequestOngoing_DocumentWidget(d)) { - setUrlAndSource_DocumentWidget(d, d->mod.url, string_Command(cmd, "mime"), - &d->sourceContent); - } - return iTrue; - } - else if (equal_Command(cmd, "fontpack.install") && document_App() == d) { - if (argLabel_Command(cmd, "ttf")) { - iAssert(!cmp_String(&d->sourceMime, "font/ttf")); - installFontFile_Fonts(collect_String(suffix_Command(cmd, "name")), &d->sourceContent); - postCommand_App("open url:about:fonts"); - } - else { - const iString *id = idFromUrl_FontPack(d->mod.url); - install_Fonts(id, &d->sourceContent); - postCommandf_App("open gotoheading:%s url:about:fonts", cstr_String(id)); + if (equal_Command(cmd, "edgeswipe.ended") && argLabel_Command(cmd, "side") == 2) { + if (argLabel_Command(cmd, "abort") && flags_Widget(w) & dragged_WidgetFlag) { + setFlags_Widget(w, dragged_WidgetFlag, iFalse); + postCommand_Widget(d, "navigate.back"); + /* We must now undo the swap that was done when the drag started. */ + /* TODO: Currently not animated! What exactly is the appropriate thing to do here? */ + iWidget *swipeParent = swipeParent_DocumentWidget_(d); + iDocumentWidget *swipeOut = findChild_Widget(swipeParent, "swipeout"); + swap_DocumentWidget_(d, swipeOut->view.doc, swipeOut); +// const int visOff = visualOffsetByReference_Widget(w); + w->offsetRef = NULL; +// setVisualOffset_Widget(w, visOff, 0, 0); +// setVisualOffset_Widget(w, 0, 150, 0); + setVisualOffset_Widget(w, 0, 0, 0); + /* Make it an overlay instead. */ +// removeChild_Widget(swipeParent, swipeOut); +// addChildPos_Widget(swipeParent, iClob(swipeOut), back_WidgetAddPos); +// setupSwipeOverlay_DocumentWidget_(d, as_Widget(swipeOut)); + return iTrue; } + iAssert(~d->flags & animationPlaceholder_DocumentWidgetFlag); + setFlags_Widget(w, dragged_WidgetFlag, iFalse); + setVisualOffset_Widget(w, 0, 250, easeOut_AnimFlag | softer_AnimFlag); return iTrue; } - return iFalse; -} - -static iRect runRect_DocumentView_(const iDocumentView *d, const iGmRun *run) { - const iRect docBounds = documentBounds_DocumentView_(d); - return moved_Rect(run->bounds, addY_I2(topLeft_Rect(docBounds), viewPos_DocumentView_(d))); -} - -static void setGrabbedPlayer_DocumentWidget_(iDocumentWidget *d, const iGmRun *run) { - if (run && run->mediaType == audio_MediaType) { - iPlayer *plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run)); - setFlags_Player(plr, volumeGrabbed_PlayerFlag, iTrue); - d->grabbedStartVolume = volume_Player(plr); - d->grabbedPlayer = run; - refresh_Widget(d); - } - else if (d->grabbedPlayer) { - setFlags_Player( - audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(d->grabbedPlayer)), - volumeGrabbed_PlayerFlag, - iFalse); - d->grabbedPlayer = NULL; - refresh_Widget(d); - } - else { - iAssert(iFalse); - } -} - -static iBool processMediaEvents_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) { - if (ev->type != SDL_MOUSEBUTTONDOWN && ev->type != SDL_MOUSEBUTTONUP && - ev->type != SDL_MOUSEMOTION) { - return iFalse; - } - if (ev->type == SDL_MOUSEBUTTONDOWN || ev->type == SDL_MOUSEBUTTONUP) { - if (ev->button.button != SDL_BUTTON_LEFT) { - return iFalse; + if (equal_Command(cmd, "edgeswipe.ended") && argLabel_Command(cmd, "side") == 1) { + iWidget *swipeParent = swipeParent_DocumentWidget_(d); + iDocumentWidget *swipeIn = findChild_Widget(swipeParent, "swipein"); + d->swipeSpeed = argLabel_Command(cmd, "speed") / gap_UI; + /* "swipe.back" will soon follow. The `d` document will do the actual back navigation, + switching immediately to a cached page. However, if one is not available, we'll need + to show a blank page for a while. */ + if (swipeIn) { + if (!argLabel_Command(cmd, "abort")) { + iWidget *swipeParent = swipeParent_DocumentWidget_(d); + /* What was being shown in the `d` document is now being swapped to + the outgoing page animation. */ + iDocumentWidget *target = new_DocumentWidget(); + target->flags |= animationPlaceholder_DocumentWidgetFlag; + addChildPos_Widget(swipeParent, iClob(target), back_WidgetAddPos); + setId_Widget(as_Widget(target), "swipeout"); + setFlags_Widget(as_Widget(target), disabled_WidgetFlag, iTrue); + swap_DocumentWidget_(target, d->view.doc, d); + setUrlAndSource_DocumentWidget(d, + swipeIn->mod.url, + collectNewCStr_String("text/gemini"), + collect_Block(new_Block(0))); + as_Widget(swipeIn)->offsetRef = NULL; + } + destroy_Widget(as_Widget(swipeIn)); } } - if (d->grabbedPlayer) { - /* Updated in the drag. */ - return iFalse; - } - const iInt2 mouse = init_I2(ev->button.x, ev->button.y); - iConstForEach(PtrArray, i, &d->view.visibleMedia) { - const iGmRun *run = i.ptr; - if (run->mediaType != audio_MediaType) { - continue; - } - /* TODO: move this to mediaui.c */ - const iRect rect = runRect_DocumentView_(&d->view, run); - iPlayer * plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run)); - if (contains_Rect(rect, mouse)) { - iPlayerUI ui; - init_PlayerUI(&ui, plr, rect); - if (ev->type == SDL_MOUSEBUTTONDOWN && flags_Player(plr) & adjustingVolume_PlayerFlag && - contains_Rect(adjusted_Rect(ui.volumeAdjustRect, - zero_I2(), - init_I2(-height_Rect(ui.volumeAdjustRect), 0)), - mouse)) { - setGrabbedPlayer_DocumentWidget_(d, run); - processEvent_Click(&d->click, ev); - /* The rest is done in the DocumentWidget click responder. */ - refresh_Widget(d); - return iTrue; - } - else if (ev->type == SDL_MOUSEBUTTONDOWN || ev->type == SDL_MOUSEMOTION) { - refresh_Widget(d); - return iTrue; - } - if (contains_Rect(ui.playPauseRect, mouse)) { - setPaused_Player(plr, !isPaused_Player(plr)); - animateMedia_DocumentWidget_(d); - return iTrue; - } - else if (contains_Rect(ui.rewindRect, mouse)) { - if (isStarted_Player(plr) && time_Player(plr) > 0.5f) { - stop_Player(plr); - start_Player(plr); - setPaused_Player(plr, iTrue); - } - refresh_Widget(d); - return iTrue; - } - else if (contains_Rect(ui.volumeRect, mouse)) { - setFlags_Player(plr, - adjustingVolume_PlayerFlag, - !(flags_Player(plr) & adjustingVolume_PlayerFlag)); - animateMedia_DocumentWidget_(d); - refresh_Widget(d); - return iTrue; - } - else if (contains_Rect(ui.menuRect, mouse)) { - /* TODO: Add menu items for: - - output device - - Save to Downloads - */ - if (d->playerMenu) { - destroy_Widget(d->playerMenu); - d->playerMenu = NULL; - return iTrue; - } - d->playerMenu = makeMenu_Widget( - as_Widget(d), - (iMenuItem[]){ - { cstrCollect_String(metadataLabel_Player(plr)) }, - }, - 1); - openMenu_Widget(d->playerMenu, bottomLeft_Rect(ui.menuRect)); - return iTrue; + if (equal_Command(cmd, "swipe.back")) { + iWidget *swipeParent = swipeParent_DocumentWidget_(d); + iDocumentWidget *target = findChild_Widget(swipeParent, "swipeout"); + if (atOldest_History(d->mod.history)) { + setVisualOffset_Widget(w, 0, 100, 0); + if (target) { + destroy_Widget(as_Widget(target)); /* didn't need it after all */ } + return iTrue; } + setupSwipeOverlay_DocumentWidget_(d, as_Widget(target)); + destroy_Widget(as_Widget(target)); /* will be actually deleted after animation finishes */ + postCommand_Widget(d, "navigate.back"); + return iTrue; } return iFalse; } -static size_t linkOrdinalFromKey_DocumentWidget_(const iDocumentWidget *d, int key) { - size_t ord = iInvalidPos; - if (d->ordinalMode == numbersAndAlphabet_DocumentLinkOrdinalMode) { - if (key >= '1' && key <= '9') { - return key - '1'; - } - if (key < 'a' || key > 'z') { - return iInvalidPos; - } - ord = key - 'a' + 9; -#if defined (iPlatformApple) - /* Skip keys that would conflict with default system shortcuts: hide, minimize, quit, close. */ - if (key == 'h' || key == 'm' || key == 'q' || key == 'w') { - return iInvalidPos; - } - if (key > 'h') ord--; - if (key > 'm') ord--; - if (key > 'q') ord--; - if (key > 'w') ord--; -#endif - } - else { - iForIndices(i, homeRowKeys_) { - if (homeRowKeys_[i] == key) { - return i; +static iBool cancelRequest_DocumentWidget_(iDocumentWidget *d, iBool postBack) { + if (d->request) { + iWidget *w = as_Widget(d); + postCommandf_Root(w->root, + "document.request.cancelled doc:%p url:%s", d, cstr_String(d->mod.url)); + iReleasePtr(&d->request); + if (d->state != ready_RequestState) { + d->state = ready_RequestState; + if (postBack) { + postCommand_Root(w->root, "navigate.back"); } } + updateFetchProgress_DocumentWidget_(d); + return iTrue; } - return ord; + return iFalse; } -static iChar linkOrdinalChar_DocumentWidget_(const iDocumentWidget *d, size_t ord) { - if (d->ordinalMode == numbersAndAlphabet_DocumentLinkOrdinalMode) { - if (ord < 9) { - return '1' + ord; - } -#if defined (iPlatformApple) - if (ord < 9 + 22) { - int key = 'a' + ord - 9; - if (key >= 'h') key++; - if (key >= 'm') key++; - if (key >= 'q') key++; - if (key >= 'w') key++; - return 'A' + key - 'a'; +static const int smoothDuration_DocumentWidget_(enum iScrollType type) { + return 600 /* milliseconds */ * scrollSpeedFactor_Prefs(prefs_App(), type); +} + +static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) { + iWidget *w = as_Widget(d); + if (equal_Command(cmd, "document.openurls.changed")) { + if (d->flags & animationPlaceholder_DocumentWidgetFlag) { + return iFalse; } -#else - if (ord < 9 + 26) { - return 'A' + ord - 9; + /* When any tab changes its document URL, update the open link indicators. */ + if (updateOpenURLs_GmDocument(d->view.doc)) { + invalidate_DocumentWidget_(d); + refresh_Widget(d); } -#endif + return iFalse; } - else { - if (ord < iElemCount(homeRowKeys_)) { - return 'A' + homeRowKeys_[ord] - 'a'; - } + if (equal_Command(cmd, "visited.changed")) { + updateVisitedLinks_GmDocument(d->view.doc); + invalidateVisibleLinks_DocumentView_(&d->view); + return iFalse; } - return 0; -} - -static void beginMarkingSelection_DocumentWidget_(iDocumentWidget *d, iInt2 pos) { - setFocus_Widget(NULL); /* TODO: Focus this document? */ - invalidateWideRunsWithNonzeroOffset_DocumentView_(&d->view); - resetWideRuns_DocumentView_(&d->view); /* Selections don't support horizontal scrolling. */ - iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iTrue); - d->initialSelectMark = d->selectMark = sourceLoc_DocumentView_(&d->view, pos); - refresh_Widget(as_Widget(d)); -} - -static void interactingWithLink_DocumentWidget_(iDocumentWidget *d, iGmLinkId id) { - iRangecc loc = linkUrlRange_GmDocument(d->view.doc, id); - if (!loc.start) { - clear_String(&d->linePrecedingLink); - return; + if (equal_Command(cmd, "document.render")) /* `Periodic` makes direct dispatch to here */ { +// printf("%u: document.render\n", SDL_GetTicks()); + if (SDL_GetTicks() - d->view.drawBufs->lastRenderTime > 150) { + remove_Periodic(periodic_App(), d); + /* Scrolling has stopped, begin filling up the buffer. */ + if (d->view.visBuf->buffers[0].texture) { + addTicker_App(prerender_DocumentWidget_, d); + } + } + return iTrue; } - const char *start = range_String(source_GmDocument(d->view.doc)).start; - /* Find the preceding line. This is offered as a prefill option for a possible input query. */ - while (loc.start > start && *loc.start != '\n') { - loc.start--; + else if (equal_Command(cmd, "window.resized") || equal_Command(cmd, "font.changed") || + equal_Command(cmd, "keyroot.changed")) { + if (equal_Command(cmd, "font.changed")) { + invalidateCachedLayout_History(d->mod.history); + } + /* Alt/Option key may be involved in window size changes. */ + setLinkNumberMode_DocumentWidget_(d, iFalse); + d->phoneToolbar = findWidget_App("toolbar"); + const iBool keepCenter = equal_Command(cmd, "font.changed"); + updateDocumentWidthRetainingScrollPosition_DocumentView_(&d->view, keepCenter); + d->view.drawBufs->flags |= updateSideBuf_DrawBufsFlag; + updateVisible_DocumentView_(&d->view); + invalidate_DocumentWidget_(d); + dealloc_VisBuf(d->view.visBuf); + updateWindowTitle_DocumentWidget_(d); + showOrHidePinningIndicator_DocumentWidget_(d); + refresh_Widget(w); } - loc.end = loc.start; /* End of the preceding line. */ - if (loc.start > start) { - loc.start--; + else if (equal_Command(cmd, "window.focus.lost")) { + if (d->flags & showLinkNumbers_DocumentWidgetFlag) { + setLinkNumberMode_DocumentWidget_(d, iFalse); + invalidateVisibleLinks_DocumentView_(&d->view); + refresh_Widget(w); + } + return iFalse; } - while (loc.start > start && *loc.start != '\n') { - loc.start--; + else if (equal_Command(cmd, "window.mouse.exited")) { + return iFalse; } - if (*loc.start == '\n') { - loc.start++; /* Start of the preceding line. */ + else if (equal_Command(cmd, "theme.changed")) { + invalidateTheme_History(d->mod.history); /* forget cached color palettes */ + if (document_App() == d) { + updateTheme_DocumentWidget_(d); + updateVisible_DocumentView_(&d->view); + updateTrust_DocumentWidget_(d, NULL); + d->view.drawBufs->flags |= updateSideBuf_DrawBufsFlag; + invalidate_DocumentWidget_(d); + refresh_Widget(w); + } } - setRange_String(&d->linePrecedingLink, loc); -} - -iLocalDef int wheelSwipeSide_DocumentWidget_(const iDocumentWidget *d) { - return (d->flags & rightWheelSwipe_DocumentWidgetFlag ? 2 - : d->flags & leftWheelSwipe_DocumentWidgetFlag ? 1 - : 0); -} - -static void finishWheelSwipe_DocumentWidget_(iDocumentWidget *d) { - if (d->flags & eitherWheelSwipe_DocumentWidgetFlag && - d->wheelSwipeState == direct_WheelSwipeState) { - const int side = wheelSwipeSide_DocumentWidget_(d); - int abort = ((side == 1 && d->swipeSpeed < 0) || (side == 2 && d->swipeSpeed > 0)); - if (iAbs(d->wheelSwipeDistance) < width_Widget(d) / 4 && iAbs(d->swipeSpeed) < 4 * gap_UI) { - abort = 1; + else if (equal_Command(cmd, "document.layout.changed") && document_Root(get_Root()) == d) { + if (argLabel_Command(cmd, "redo")) { + redoLayout_GmDocument(d->view.doc); } - postCommand_Widget(d, "edgeswipe.ended side:%d abort:%d", side, abort); - d->flags &= ~eitherWheelSwipe_DocumentWidgetFlag; + updateSize_DocumentWidget(d); } -} - -static iBool handleWheelSwipe_DocumentWidget_(iDocumentWidget *d, const SDL_MouseWheelEvent *ev) { - iWidget *w = as_Widget(d); - if (deviceType_App() != desktop_AppDeviceType) { + else if (equal_Command(cmd, "pinsplit.set")) { + postCommand_App("document.update.pin"); /* prefs value not set yet */ return iFalse; } - if (~flags_Widget(w) & horizontalOffset_WidgetFlag) { + else if (equal_Command(cmd, "document.update.pin")) { + showOrHidePinningIndicator_DocumentWidget_(d); return iFalse; } - iAssert(~d->flags & animationPlaceholder_DocumentWidgetFlag); -// printf("STATE:%d wheel x:%d inert:%d end:%d\n", d->wheelSwipeState, -// ev->x, isInertia_MouseWheelEvent(ev), -// isScrollFinished_MouseWheelEvent(ev)); -// fflush(stdout); - switch (d->wheelSwipeState) { - case none_WheelSwipeState: - /* A new swipe starts. */ - if (!isInertia_MouseWheelEvent(ev) && !isScrollFinished_MouseWheelEvent(ev)) { - int side = ev->x > 0 ? 1 : 2; - d->wheelSwipeDistance = ev->x * 2; - d->flags &= ~eitherWheelSwipe_DocumentWidgetFlag; - d->flags |= (side == 1 ? leftWheelSwipe_DocumentWidgetFlag - : rightWheelSwipe_DocumentWidgetFlag); - // printf("swipe starts at %d, side %d\n", d->wheelSwipeDistance, side); - d->wheelSwipeState = direct_WheelSwipeState; - d->swipeSpeed = 0; - postCommand_Widget(d, "edgeswipe.moved arg:%d side:%d", d->wheelSwipeDistance, side); - return iTrue; - } - break; - case direct_WheelSwipeState: - if (isInertia_MouseWheelEvent(ev) || isScrollFinished_MouseWheelEvent(ev)) { - finishWheelSwipe_DocumentWidget_(d); - d->wheelSwipeState = none_WheelSwipeState; - } - else { - int step = ev->x * 2; - d->wheelSwipeDistance += step; - /* Remember the maximum speed. */ - if (d->swipeSpeed < 0 && step < 0) { - d->swipeSpeed = iMin(d->swipeSpeed, step); - } - else if (d->swipeSpeed > 0 && step > 0) { - d->swipeSpeed = iMax(d->swipeSpeed, step); - } - else { - d->swipeSpeed = step; - } - switch (wheelSwipeSide_DocumentWidget_(d)) { - case 1: - d->wheelSwipeDistance = iMax(0, d->wheelSwipeDistance); - d->wheelSwipeDistance = iMin(width_Widget(d), d->wheelSwipeDistance); - break; - case 2: - d->wheelSwipeDistance = iMin(0, d->wheelSwipeDistance); - d->wheelSwipeDistance = iMax(-width_Widget(d), d->wheelSwipeDistance); - break; - } - /* TODO: calculate speed, rememeber direction */ - //printf("swipe moved to %d, side %d\n", d->wheelSwipeDistance, side); - postCommand_Widget(d, "edgeswipe.moved arg:%d side:%d", d->wheelSwipeDistance, - wheelSwipeSide_DocumentWidget_(d)); - } - return iTrue; + else if (equal_Command(cmd, "tabs.changed")) { + setLinkNumberMode_DocumentWidget_(d, iFalse); + if (cmp_String(id_Widget(w), suffixPtr_Command(cmd, "id")) == 0) { + /* Set palette for our document. */ + updateTheme_DocumentWidget_(d); + updateTrust_DocumentWidget_(d, NULL); + updateSize_DocumentWidget(d); + showOrHidePinningIndicator_DocumentWidget_(d); + updateFetchProgress_DocumentWidget_(d); + updateHover_Window(window_Widget(w)); + } + init_Anim(&d->view.sideOpacity, 0); + init_Anim(&d->view.altTextOpacity, 0); + updateSideOpacity_DocumentView_(&d->view, iFalse); + updateWindowTitle_DocumentWidget_(d); + allocVisBuffer_DocumentView_(&d->view); + animateMedia_DocumentWidget_(d); + remove_Periodic(periodic_App(), d); + removeTicker_App(prerender_DocumentWidget_, d); + return iFalse; } - return iFalse; -} - -static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) { - iWidget *w = as_Widget(d); - iDocumentView *view = &d->view; - if (isMetricsChange_UserEvent(ev)) { - updateSize_DocumentWidget(d); + else if (equal_Command(cmd, "tab.created")) { + /* Space for tab buttons has changed. */ + updateWindowTitle_DocumentWidget_(d); + return iFalse; } - else if (processEvent_SmoothScroll(&d->view.scrollY, ev)) { + else if (equal_Command(cmd, "document.select") && d == document_App()) { + /* Touch selection mode. */ + if (!arg_Command(cmd)) { + d->selectMark = iNullRange; + setFlags_Widget(w, touchDrag_WidgetFlag, iFalse); + setFadeEnabled_ScrollWidget(d->scroll, iTrue); + } + else { + setFlags_Widget(w, touchDrag_WidgetFlag, iTrue); + d->flags |= movingSelectMarkEnd_DocumentWidgetFlag | + selectWords_DocumentWidgetFlag; /* finger-based selection is imprecise */ + d->flags &= ~selectLines_DocumentWidgetFlag; + setFadeEnabled_ScrollWidget(d->scroll, iFalse); + d->selectMark = sourceLoc_DocumentView_(&d->view, d->contextPos); + extendRange_Rangecc(&d->selectMark, range_String(source_GmDocument(d->view.doc)), + word_RangeExtension | bothStartAndEnd_RangeExtension); + d->initialSelectMark = d->selectMark; + } return iTrue; } - else if (ev->type == SDL_USEREVENT && ev->user.code == command_UserEventCode) { - if (isCommand_Widget(w, ev, "pullaction")) { - postCommand_Widget(w, "navigate.reload"); - return iTrue; + else if (equal_Command(cmd, "document.info") && d == document_App()) { + const char *unchecked = red_ColorEscape "\u2610"; + const char *checked = green_ColorEscape "\u2611"; + const iBool haveFingerprint = (d->certFlags & haveFingerprint_GmCertFlag) != 0; + const int requiredForTrust = (available_GmCertFlag | haveFingerprint_GmCertFlag | + timeVerified_GmCertFlag); + const iBool canTrust = ~d->certFlags & trusted_GmCertFlag && + ((d->certFlags & requiredForTrust) == requiredForTrust); + const iRecentUrl *recent = constMostRecentUrl_History(d->mod.history); + const iString *meta = &d->sourceMime; + if (recent && recent->cachedResponse) { + meta = &recent->cachedResponse->meta; } - if (!handleCommand_DocumentWidget_(d, command_UserEvent(ev))) { - /* Base class commands. */ - return processEvent_Widget(w, ev); + iString *msg = collectNew_String(); + if (isEmpty_String(&d->sourceHeader)) { + appendFormat_String(msg, + "%s\n%s\n", + cstr_String(meta), + formatCStrs_Lang("num.bytes.n", size_Block(&d->sourceContent))); + } + else { + appendFormat_String(msg, "%s\n", cstr_String(&d->sourceHeader)); + if (size_Block(&d->sourceContent)) { + appendFormat_String( + msg, "%s\n", formatCStrs_Lang("num.bytes.n", size_Block(&d->sourceContent))); + } + } + /* TODO: On mobile, omit the CA status. */ + appendFormat_String( + msg, + "\n%s${pageinfo.cert.status}\n" + "%s%s %s\n" + "%s%s %s%s\n" + "%s%s %s (%04d-%02d-%02d %02d:%02d:%02d)\n" + "%s%s %s", + uiHeading_ColorEscape, + d->certFlags & authorityVerified_GmCertFlag ? checked + : uiTextAction_ColorEscape "\u2610", + uiText_ColorEscape, + d->certFlags & authorityVerified_GmCertFlag ? "${pageinfo.cert.ca.verified}" + : "${pageinfo.cert.ca.unverified}", + d->certFlags & domainVerified_GmCertFlag ? checked : unchecked, + uiText_ColorEscape, + d->certFlags & domainVerified_GmCertFlag ? "${pageinfo.domain.match}" + : "${pageinfo.domain.mismatch}", + ~d->certFlags & domainVerified_GmCertFlag + ? format_CStr(" (%s)", cstr_String(d->certSubject)) + : "", + d->certFlags & timeVerified_GmCertFlag ? checked : unchecked, + uiText_ColorEscape, + d->certFlags & timeVerified_GmCertFlag ? "${pageinfo.cert.notexpired}" + : "${pageinfo.cert.expired}", + d->certExpiry.year, + d->certExpiry.month, + d->certExpiry.day, + d->certExpiry.hour, + d->certExpiry.minute, + d->certExpiry.second, + d->certFlags & trusted_GmCertFlag ? checked : unchecked, + uiText_ColorEscape, + d->certFlags & trusted_GmCertFlag ? "${pageinfo.cert.trusted}" + : "${pageinfo.cert.untrusted}"); + setFocus_Widget(NULL); + iArray *items = new_Array(sizeof(iMenuItem)); + if (canTrust) { + pushBack_Array(items, + &(iMenuItem){ uiTextCaution_ColorEscape "${dlg.cert.trust}", + SDLK_u, + KMOD_PRIMARY | KMOD_SHIFT, + "server.trustcert" }); + } + if (haveFingerprint) { + pushBack_Array(items, &(iMenuItem){ "${dlg.cert.fingerprint}", 0, 0, "server.copycert" }); + } + if (!isEmpty_Array(items)) { + pushBack_Array(items, &(iMenuItem){ "---", 0, 0, 0 }); + } + pushBack_Array(items, &(iMenuItem){ "${close}", 0, 0, "message.ok" }); + iWidget *dlg = makeQuestion_Widget(uiHeading_ColorEscape "${heading.pageinfo}", + cstr_String(msg), + data_Array(items), + size_Array(items)); + delete_Array(items); + /* Enforce a minimum size. */ +// iWidget *sizer = new_Widget(); +// setFixedSize_Widget(sizer, init_I2(gap_UI * 65, 1)); +// addChildFlags_Widget(dlg, iClob(sizer), frameless_WidgetFlag); +// setFlags_Widget(dlg, centerHorizontal_WidgetFlag, iFalse); + if (deviceType_App() == desktop_AppDeviceType) { + const iWidget *lockButton = findWidget_Root("navbar.lock"); + setPos_Widget(dlg, windowToLocal_Widget(dlg, bottomLeft_Rect(bounds_Widget(lockButton)))); } + arrange_Widget(dlg); + addAction_Widget(dlg, SDLK_ESCAPE, 0, "message.ok"); + addAction_Widget(dlg, SDLK_SPACE, 0, "message.ok"); return iTrue; } - if (ev->type == SDL_KEYDOWN) { - const int key = ev->key.keysym.sym; - if ((d->flags & showLinkNumbers_DocumentWidgetFlag) && - ((key >= '1' && key <= '9') || (key >= 'a' && key <= 'z'))) { - const size_t ord = linkOrdinalFromKey_DocumentWidget_(d, key) + d->ordinalBase; - iConstForEach(PtrArray, i, &d->view.visibleLinks) { - if (ord == iInvalidPos) break; - const iGmRun *run = i.ptr; - if (run->flags & decoration_GmRunFlag && - visibleLinkOrdinal_DocumentView_(view, run->linkId) == ord) { - if (d->flags & setHoverViaKeys_DocumentWidgetFlag) { - view->hoverLink = run; - } - else { - postCommandf_Root( - w->root, - "open newtab:%d url:%s", - (isPinned_DocumentWidget_(d) ? otherRoot_OpenTabFlag : 0) ^ - (d->ordinalMode == numbersAndAlphabet_DocumentLinkOrdinalMode - ? openTabMode_Sym(modState_Keys()) - : (d->flags & newTabViaHomeKeys_DocumentWidgetFlag ? 1 : 0)), - cstr_String(absoluteUrl_String( - d->mod.url, linkUrl_GmDocument(view->doc, run->linkId)))); - interactingWithLink_DocumentWidget_(d, run->linkId); - } - setLinkNumberMode_DocumentWidget_(d, iFalse); - invalidateVisibleLinks_DocumentView_(view); - refresh_Widget(d); - return iTrue; - } - } + else if (equal_Command(cmd, "server.unexpire") && document_App() == d) { + const iRangecc host = urlHost_String(d->mod.url); + const uint16_t port = urlPort_String(d->mod.url); + if (!isEmpty_Block(d->certFingerprint) && !isEmpty_Range(&host)) { + iTime expiry; + initCurrent_Time(&expiry); + iTime oneHour; /* One hour is long enough for a single visit (?). */ + initSeconds_Time(&oneHour, 3600); + add_Time(&expiry, &oneHour); + iDate expDate; + init_Date(&expDate, &expiry); + setTrusted_GmCerts(certs_App(), host, port, d->certFingerprint, &expDate); + postCommand_Widget(w, "navigate.reload"); } - switch (key) { - case SDLK_ESCAPE: - if (d->flags & showLinkNumbers_DocumentWidgetFlag && document_App() == d) { - setLinkNumberMode_DocumentWidget_(d, iFalse); - invalidateVisibleLinks_DocumentView_(view); - refresh_Widget(d); - return iTrue; - } - break; -#if !defined (NDEBUG) - case SDLK_KP_1: - case '`': { - iBlock *seed = new_Block(64); - for (size_t i = 0; i < 64; ++i) { - setByte_Block(seed, i, iRandom(0, 256)); - } - setThemeSeed_GmDocument(view->doc, seed); - delete_Block(seed); - invalidate_DocumentWidget_(d); - refresh_Widget(w); - break; - } -#endif -#if 0 - case '0': { - extern int enableHalfPixelGlyphs_Text; - enableHalfPixelGlyphs_Text = !enableHalfPixelGlyphs_Text; - refresh_Widget(w); - printf("halfpixel: %d\n", enableHalfPixelGlyphs_Text); - fflush(stdout); - break; - } -#endif -#if 0 - case '0': { - extern int enableKerning_Text; - enableKerning_Text = !enableKerning_Text; - invalidate_DocumentWidget_(d); - refresh_Widget(w); - printf("kerning: %d\n", enableKerning_Text); - fflush(stdout); - break; - } -#endif + return iTrue; + } + else if (equal_Command(cmd, "server.trustcert") && document_App() == d) { + const iRangecc host = urlHost_String(d->mod.url); + const uint16_t port = urlPort_String(d->mod.url); + if (!isEmpty_Block(d->certFingerprint) && !isEmpty_Range(&host)) { + setTrusted_GmCerts(certs_App(), host, port, d->certFingerprint, &d->certExpiry); + postCommand_Widget(w, "navigate.reload"); } + return iTrue; } -#if defined (iPlatformAppleDesktop) - else if (ev->type == SDL_MOUSEWHEEL && - ev->wheel.y == 0 && - d->wheelSwipeState == direct_WheelSwipeState && - handleWheelSwipe_DocumentWidget_(d, &ev->wheel)) { + else if (equal_Command(cmd, "server.copycert") && document_App() == d) { + SDL_SetClipboardText(cstrCollect_String(hexEncode_Block(d->certFingerprint))); return iTrue; } -#endif - else if (ev->type == SDL_MOUSEWHEEL && isHover_Widget(w)) { - const iInt2 mouseCoord = coord_MouseWheelEvent(&ev->wheel); - if (isPerPixel_MouseWheelEvent(&ev->wheel)) { - const iInt2 wheel = init_I2(ev->wheel.x, ev->wheel.y); - stop_Anim(&d->view.scrollY.pos); - immediateScroll_DocumentView_(view, -wheel.y); - if (!scrollWideBlock_DocumentView_(view, mouseCoord, -wheel.x, 0) && - wheel.x) { - handleWheelSwipe_DocumentWidget_(d, &ev->wheel); + else if (equal_Command(cmd, "copy") && document_App() == d && !focus_Widget()) { + iString *copied; + if (d->selectMark.start) { + iRangecc mark = d->selectMark; + if (mark.start > mark.end) { + iSwap(const char *, mark.start, mark.end); } + copied = newRange_String(mark); } else { - /* Traditional mouse wheel. */ - const int amount = ev->wheel.y; - if (keyMods_Sym(modState_Keys()) == KMOD_PRIMARY) { - postCommandf_App("zoom.delta arg:%d", amount > 0 ? 10 : -10); - return iTrue; - } - smoothScroll_DocumentView_(view, - -3 * amount * lineHeight_Text(paragraph_FontId), - smoothDuration_DocumentWidget_(mouse_ScrollType)); - scrollWideBlock_DocumentView_( - view, mouseCoord, -3 * ev->wheel.x * lineHeight_Text(paragraph_FontId), 167); + /* Full document. */ + copied = copy_String(source_GmDocument(d->view.doc)); + } + SDL_SetClipboardText(cstr_String(copied)); + delete_String(copied); + if (flags_Widget(w) & touchDrag_WidgetFlag) { + postCommand_Widget(w, "document.select arg:0"); } - iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iTrue); return iTrue; } - else if (ev->type == SDL_MOUSEMOTION) { - if (ev->motion.which != SDL_TOUCH_MOUSEID) { - iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iFalse); - } - const iInt2 mpos = init_I2(ev->motion.x, ev->motion.y); - if (isVisible_Widget(d->menu)) { - setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW); - } -#if 0 - else if (contains_Rect(siteBannerRect_DocumentWidget_(d), mpos)) { - setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_HAND); + else if (equal_Command(cmd, "document.copylink") && document_App() == d) { + if (d->contextLink) { + SDL_SetClipboardText(cstr_String(canonicalUrl_String(absoluteUrl_String( + d->mod.url, linkUrl_GmDocument(d->view.doc, d->contextLink->linkId))))); } -#endif else { - if (value_Anim(&view->altTextOpacity) < 0.833f) { - setValue_Anim(&view->altTextOpacity, 0, 0); /* keep it hidden while moving */ - } - updateHover_DocumentView_(view, mpos); + SDL_SetClipboardText(cstr_String(canonicalUrl_String(d->mod.url))); } - } - if (ev->type == SDL_USEREVENT && ev->user.code == widgetTapBegins_UserEventCode) { - iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iFalse); return iTrue; } - if (ev->type == SDL_MOUSEBUTTONDOWN) { - if (ev->button.button == SDL_BUTTON_X1) { - postCommand_Root(w->root, "navigate.back"); - return iTrue; - } - if (ev->button.button == SDL_BUTTON_X2) { - postCommand_Root(w->root, "navigate.forward"); - return iTrue; + else if (equalWidget_Command(cmd, w, "document.downloadlink")) { + if (d->contextLink) { + const iGmLinkId linkId = d->contextLink->linkId; + setUrl_Media(media_GmDocument(d->view.doc), + linkId, + download_MediaType, + linkUrl_GmDocument(d->view.doc, linkId)); + requestMedia_DocumentWidget_(d, linkId, iFalse /* no filters */); + redoLayout_GmDocument(d->view.doc); /* inline downloader becomes visible */ + updateVisible_DocumentView_(&d->view); + invalidate_DocumentWidget_(d); + refresh_Widget(w); } - if (ev->button.button == SDL_BUTTON_MIDDLE && view->hoverLink) { - interactingWithLink_DocumentWidget_(d, view->hoverLink->linkId); - postCommandf_Root(w->root, "open newtab:%d url:%s", - (isPinned_DocumentWidget_(d) ? otherRoot_OpenTabFlag : 0) | - (modState_Keys() & KMOD_SHIFT ? new_OpenTabFlag : newBackground_OpenTabFlag), - cstr_String(linkUrl_GmDocument(view->doc, view->hoverLink->linkId))); - return iTrue; + return iTrue; + } + else if (equal_Command(cmd, "document.input.submit") && document_Command(cmd) == d) { + postCommandf_Root(w->root, + /* use the `redirect:1` argument to cause the input query URL to be + replaced in History; we don't want to navigate onto it */ + "open redirect:1 url:%s", + cstrCollect_String(makeQueryUrl_DocumentWidget_ + (d, collect_String(suffix_Command(cmd, "value"))))); + return iTrue; + } + else if (equal_Command(cmd, "valueinput.cancelled") && + equal_Rangecc(range_Command(cmd, "id"), "document.input.submit") && document_App() == d) { + postCommand_Root(get_Root(), "navigate.back"); + return iTrue; + } + else if (equalWidget_Command(cmd, w, "document.request.updated") && + id_GmRequest(d->request) == argU32Label_Command(cmd, "reqid")) { +// set_Block(&d->sourceContent, &lockResponse_GmRequest(d->request)->body); +// unlockResponse_GmRequest(d->request); + if (document_App() == d) { + updateFetchProgress_DocumentWidget_(d); } - if (ev->button.button == SDL_BUTTON_RIGHT && - contains_Widget(w, init_I2(ev->button.x, ev->button.y))) { - if (!isVisible_Widget(d->menu)) { - d->contextLink = view->hoverLink; - d->contextPos = init_I2(ev->button.x, ev->button.y); - if (d->menu) { - destroy_Widget(d->menu); - d->menu = NULL; - } - setFocus_Widget(NULL); - iArray items; - init_Array(&items, sizeof(iMenuItem)); - if (d->contextLink) { - /* Context menu for a link. */ - interactingWithLink_DocumentWidget_(d, d->contextLink->linkId); /* perhaps will be triggered */ - const iString *linkUrl = linkUrl_GmDocument(view->doc, d->contextLink->linkId); -// const int linkFlags = linkFlags_GmDocument(d->doc, d->contextLink->linkId); - const iRangecc scheme = urlScheme_String(linkUrl); - const iBool isGemini = equalCase_Rangecc(scheme, "gemini"); - iBool isNative = iFalse; - if (deviceType_App() != desktop_AppDeviceType) { - /* Show the link as the first, non-interactive item. */ - pushBack_Array(&items, &(iMenuItem){ - format_CStr("```%s", cstr_String(linkUrl)), - 0, 0, NULL }); - } - if (willUseProxy_App(scheme) || isGemini || - equalCase_Rangecc(scheme, "file") || - equalCase_Rangecc(scheme, "finger") || - equalCase_Rangecc(scheme, "gopher")) { - isNative = iTrue; - /* Regular links that we can open. */ - pushBackN_Array( - &items, - (iMenuItem[]){ { openTab_Icon " ${link.newtab}", - 0, - 0, - format_CStr("!open newtab:1 origin:%s url:%s", - cstr_String(id_Widget(w)), - cstr_String(linkUrl)) }, - { openTabBg_Icon " ${link.newtab.background}", - 0, - 0, - format_CStr("!open newtab:2 origin:%s url:%s", - cstr_String(id_Widget(w)), - cstr_String(linkUrl)) }, - { "${link.side}", - 0, - 0, - format_CStr("!open newtab:4 origin:%s url:%s", - cstr_String(id_Widget(w)), - cstr_String(linkUrl)) }, - { "${link.side.newtab}", - 0, - 0, - format_CStr("!open newtab:5 origin:%s url:%s", - cstr_String(id_Widget(w)), - cstr_String(linkUrl)) } }, - 4); - if (deviceType_App() == phone_AppDeviceType) { - removeN_Array(&items, size_Array(&items) - 2, iInvalidSize); - } - } - else if (!willUseProxy_App(scheme)) { - pushBack_Array( - &items, - &(iMenuItem){ openExt_Icon " ${link.browser}", - 0, - 0, - format_CStr("!open default:1 url:%s", cstr_String(linkUrl)) }); - } - if (willUseProxy_App(scheme)) { - pushBackN_Array( - &items, - (iMenuItem[]){ - { "---" }, - { isGemini ? "${link.noproxy}" : openExt_Icon " ${link.browser}", - 0, - 0, - format_CStr("!open origin:%s noproxy:1 url:%s", - cstr_String(id_Widget(w)), - cstr_String(linkUrl)) } }, - 2); - } - iString *linkLabel = collectNewRange_String( - linkLabel_GmDocument(view->doc, d->contextLink->linkId)); - urlEncodeSpaces_String(linkLabel); - pushBackN_Array(&items, - (iMenuItem[]){ { "---" }, - { "${link.copy}", 0, 0, "document.copylink" }, - { bookmark_Icon " ${link.bookmark}", - 0, - 0, - format_CStr("!bookmark.add title:%s url:%s", - cstr_String(linkLabel), - cstr_String(linkUrl)) }, - }, - 3); - if (isNative && d->contextLink->mediaType != download_MediaType) { - pushBackN_Array(&items, (iMenuItem[]){ - { "---" }, - { download_Icon " ${link.download}", 0, 0, "document.downloadlink" }, - }, 2); - } - iMediaRequest *mediaReq; - if ((mediaReq = findMediaRequest_DocumentWidget_(d, d->contextLink->linkId)) != NULL && - d->contextLink->mediaType != download_MediaType) { - if (isFinished_GmRequest(mediaReq->req)) { - pushBack_Array(&items, - &(iMenuItem){ download_Icon " " saveToDownloads_Label, - 0, - 0, - format_CStr("document.media.save link:%u", - d->contextLink->linkId) }); - } - } - if (equalCase_Rangecc(scheme, "file")) { - /* Local files may be deleted. */ - pushBack_Array( - &items, - &(iMenuItem){ delete_Icon " " uiTextCaution_ColorEscape - "${link.file.delete}", - 0, - 0, - format_CStr("!file.delete confirm:1 path:%s", - cstrCollect_String( - localFilePathFromUrl_String(linkUrl))) }); - } - } - else if (deviceType_App() == desktop_AppDeviceType) { - if (!isEmpty_Range(&d->selectMark)) { - pushBackN_Array(&items, - (iMenuItem[]){ { "${menu.copy}", 0, 0, "copy" }, - { "---", 0, 0, NULL } }, - 2); - } - pushBackN_Array( - &items, - (iMenuItem[]){ - { backArrow_Icon " ${menu.back}", navigateBack_KeyShortcut, "navigate.back" }, - { forwardArrow_Icon " ${menu.forward}", navigateForward_KeyShortcut, "navigate.forward" }, - { upArrow_Icon " ${menu.parent}", navigateParent_KeyShortcut, "navigate.parent" }, - { upArrowBar_Icon " ${menu.root}", navigateRoot_KeyShortcut, "navigate.root" }, - { "---" }, - { reload_Icon " ${menu.reload}", reload_KeyShortcut, "navigate.reload" }, - { timer_Icon " ${menu.autoreload}", 0, 0, "document.autoreload.menu" }, - { "---" }, - { bookmark_Icon " ${menu.page.bookmark}", SDLK_d, KMOD_PRIMARY, "bookmark.add" }, - { star_Icon " ${menu.page.subscribe}", subscribeToPage_KeyModifier, "feeds.subscribe" }, - { "---" }, - { book_Icon " ${menu.page.import}", 0, 0, "bookmark.links confirm:1" }, - { globe_Icon " ${menu.page.translate}", 0, 0, "document.translate" }, - { upload_Icon " ${menu.page.upload}", 0, 0, "document.upload" }, - { "---" }, - { "${menu.page.copyurl}", 0, 0, "document.copylink" } }, - 16); - if (isEmpty_Range(&d->selectMark)) { - pushBackN_Array( - &items, - (iMenuItem[]){ - { "${menu.page.copysource}", 'c', KMOD_PRIMARY, "copy" }, - { download_Icon " " saveToDownloads_Label, SDLK_s, KMOD_PRIMARY, "document.save" } }, - 2); - } - } - else { - /* Mobile text selection menu. */ -#if 0 - pushBackN_Array( - &items, - (iMenuItem[]){ - { "${menu.select}", 0, 0, "document.select arg:1" }, - { "${menu.select.word}", 0, 0, "document.select arg:2" }, - { "${menu.select.par}", 0, 0, "document.select arg:3" }, - }, - 3); -#endif - postCommand_Root(w->root, "document.select arg:1"); - return iTrue; - } - d->menu = makeMenu_Widget(w, data_Array(&items), size_Array(&items)); - deinit_Array(&items); - setMenuItemDisabled_Widget( - d->menu, - "document.upload", - !equalCase_Rangecc(urlScheme_String(d->mod.url), "gemini") && - !equalCase_Rangecc(urlScheme_String(d->mod.url), "titan")); + checkResponse_DocumentWidget_(d); + set_Atomic(&d->isRequestUpdated, iFalse); /* ready to be notified again */ + return iFalse; + } + else if (equalWidget_Command(cmd, w, "document.request.finished") && + id_GmRequest(d->request) == argU32Label_Command(cmd, "reqid")) { + d->flags &= ~fromCache_DocumentWidgetFlag; + set_Block(&d->sourceContent, body_GmRequest(d->request)); + if (!isSuccess_GmStatusCode(status_GmRequest(d->request))) { + /* TODO: Why is this here? Can it be removed? */ + format_String(&d->sourceHeader, + "%s%s", + humanReadableStatusCode_(status_GmRequest(d->request)), + cstr_String(meta_GmRequest(d->request))); + } + updateFetchProgress_DocumentWidget_(d); + checkResponse_DocumentWidget_(d); + if (category_GmStatusCode(status_GmRequest(d->request)) == categorySuccess_GmStatusCode) { + init_Anim(&d->view.scrollY.pos, d->initNormScrollY * pageHeight_DocumentView_(&d->view)); + /* TODO: unless user already scrolled! */ + } + addBannerWarnings_DocumentWidget_(d); + iChangeFlags(d->flags, + urlChanged_DocumentWidgetFlag | drawDownloadCounter_DocumentWidgetFlag, + iFalse); + d->state = ready_RequestState; + postProcessRequestContent_DocumentWidget_(d, iFalse); + /* The response may be cached. */ + if (d->request) { + iAssert(~d->flags & animationPlaceholder_DocumentWidgetFlag); + iAssert(~d->flags & fromCache_DocumentWidgetFlag); + if (!equal_Rangecc(urlScheme_String(d->mod.url), "about") && + (startsWithCase_String(meta_GmRequest(d->request), "text/") || + !cmp_String(&d->sourceMime, mimeType_Gempub))) { + setCachedResponse_History(d->mod.history, lockResponse_GmRequest(d->request)); + unlockResponse_GmRequest(d->request); } - processContextMenuEvent_Widget(d->menu, ev, {}); } + iReleasePtr(&d->request); + updateVisible_DocumentView_(&d->view); + d->view.drawBufs->flags |= updateSideBuf_DrawBufsFlag; + postCommandf_Root(w->root, + "document.changed doc:%p status:%d url:%s", + d, + d->sourceStatus, + cstr_String(d->mod.url)); + /* Check for a pending goto. */ + if (!isEmpty_String(&d->pendingGotoHeading)) { + scrollToHeading_DocumentView_(&d->view, cstr_String(&d->pendingGotoHeading)); + clear_String(&d->pendingGotoHeading); + } + cacheDocumentGlyphs_DocumentWidget_(d); + return iFalse; } - if (processMediaEvents_DocumentWidget_(d, ev)) { + else if (equal_Command(cmd, "document.translate") && d == document_App()) { + if (!d->translation) { + d->translation = new_Translation(d); + if (isUsingPanelLayout_Mobile()) { + const iRect safe = safeRect_Root(w->root); + d->translation->dlg->rect.pos = windowToLocal_Widget(w, zero_I2()); + d->translation->dlg->rect.size = safe.size; + } + } return iTrue; } - if (processEvent_Banner(d->banner, ev)) { + else if (startsWith_CStr(cmd, "translation.") && d->translation) { + const iBool wasHandled = handleCommand_Translation(d->translation, cmd); + if (isFinished_Translation(d->translation)) { + delete_Translation(d->translation); + d->translation = NULL; + } + return wasHandled; + } + else if (equal_Command(cmd, "document.upload") && d == document_App()) { + if (findChild_Widget(root_Widget(w), "upload")) { + return iTrue; /* already open */ + } + const iBool isGemini = equalCase_Rangecc(urlScheme_String(d->mod.url), "gemini"); + if (isGemini || equalCase_Rangecc(urlScheme_String(d->mod.url), "titan")) { + iUploadWidget *upload = new_UploadWidget(); + setUrl_UploadWidget(upload, d->mod.url); + setResponseViewer_UploadWidget(upload, d); + addChild_Widget(get_Root()->widget, iClob(upload)); + setupSheetTransition_Mobile(as_Widget(upload), iTrue); + postRefresh_App(); + } return iTrue; } - /* The left mouse button. */ - switch (processEvent_Click(&d->click, ev)) { - case started_ClickResult: - if (d->grabbedPlayer) { - return iTrue; - } - /* Enable hover state now that scrolling has surely finished. */ - if (d->flags & noHoverWhileScrolling_DocumentWidgetFlag) { - d->flags &= ~noHoverWhileScrolling_DocumentWidgetFlag; - updateHover_DocumentView_(view, mouseCoord_Window(get_Window(), ev->button.which)); - } - if (~flags_Widget(w) & touchDrag_WidgetFlag) { - iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iFalse); - iChangeFlags(d->flags, selectWords_DocumentWidgetFlag, d->click.count == 2); - iChangeFlags(d->flags, selectLines_DocumentWidgetFlag, d->click.count >= 3); - /* Double/triple clicks marks the selection immediately. */ - if (d->click.count >= 2) { - beginMarkingSelection_DocumentWidget_(d, d->click.startPos); - extendRange_Rangecc( - &d->selectMark, - range_String(source_GmDocument(view->doc)), - bothStartAndEnd_RangeExtension | - (d->click.count == 2 ? word_RangeExtension : line_RangeExtension)); - d->initialSelectMark = d->selectMark; - refresh_Widget(w); - } - else { - d->initialSelectMark = iNullRange; - } + else if (equal_Command(cmd, "media.updated") || equal_Command(cmd, "media.finished")) { + return handleMediaCommand_DocumentWidget_(d, cmd); + } + else if (equal_Command(cmd, "media.player.started")) { + /* When one media player starts, pause the others that may be playing. */ + const iPlayer *startedPlr = pointerLabel_Command(cmd, "player"); + const iMedia * media = media_GmDocument(d->view.doc); + const size_t num = numAudio_Media(media); + for (size_t id = 1; id <= num; id++) { + iPlayer *plr = audioPlayer_Media(media, (iMediaId){ audio_MediaType, id }); + if (plr != startedPlr) { + setPaused_Player(plr, iTrue); } + } + } + else if (equal_Command(cmd, "media.player.update")) { + updateMedia_DocumentWidget_(d); + return iFalse; + } + else if (equal_Command(cmd, "document.stop") && document_App() == d) { + if (cancelRequest_DocumentWidget_(d, iTrue /* navigate back */)) { return iTrue; - case drag_ClickResult: { - if (d->grabbedPlayer) { - iPlayer *plr = - audioPlayer_Media(media_GmDocument(view->doc), mediaId_GmRun(d->grabbedPlayer)); - iPlayerUI ui; - init_PlayerUI(&ui, plr, runRect_DocumentView_(view, d->grabbedPlayer)); - float off = (float) delta_Click(&d->click).x / (float) width_Rect(ui.volumeSlider); - setVolume_Player(plr, d->grabbedStartVolume + off); - refresh_Widget(w); - return iTrue; - } - /* Fold/unfold a preformatted block. */ - if (~d->flags & selecting_DocumentWidgetFlag && view->hoverPre && - preIsFolded_GmDocument(view->doc, preId_GmRun(view->hoverPre))) { - return iTrue; - } - /* Begin selecting a range of text. */ - if (~d->flags & selecting_DocumentWidgetFlag) { - beginMarkingSelection_DocumentWidget_(d, d->click.startPos); - } - iRangecc loc = sourceLoc_DocumentView_(view, pos_Click(&d->click)); - if (d->selectMark.start == NULL) { - d->selectMark = loc; + } + } + else if (equalWidget_Command(cmd, w, "document.media.save")) { + const iGmLinkId linkId = argLabel_Command(cmd, "link"); + const iMediaRequest *media = findMediaRequest_DocumentWidget_(d, linkId); + if (media) { + saveToDownloads_(url_GmRequest(media->req), meta_GmRequest(media->req), + body_GmRequest(media->req), iTrue); + } + } + else if (equal_Command(cmd, "document.save") && document_App() == d) { + if (d->request) { + makeSimpleMessage_Widget(uiTextCaution_ColorEscape "${heading.save.incomplete}", + "${dlg.save.incomplete}"); + } + else if (!isEmpty_Block(&d->sourceContent)) { + const iBool doOpen = argLabel_Command(cmd, "open"); + const iString *savePath = saveToDownloads_(d->mod.url, &d->sourceMime, + &d->sourceContent, !doOpen); + if (!isEmpty_String(savePath) && doOpen) { + postCommandf_Root( + w->root, "!open url:%s", cstrCollect_String(makeFileUrl_String(savePath))); } - else if (loc.end) { - if (flags_Widget(w) & touchDrag_WidgetFlag) { - /* Choose which end to move. */ - if (!(d->flags & (movingSelectMarkStart_DocumentWidgetFlag | - movingSelectMarkEnd_DocumentWidgetFlag))) { - const iRangecc mark = selectMark_DocumentWidget_(d); - const char * midMark = mark.start + size_Range(&mark) / 2; - const iRangecc loc = sourceLoc_DocumentView_(view, pos_Click(&d->click)); - const iBool isCloserToStart = d->selectMark.start > d->selectMark.end ? - (loc.start > midMark) : (loc.start < midMark); - iChangeFlags(d->flags, movingSelectMarkStart_DocumentWidgetFlag, isCloserToStart); - iChangeFlags(d->flags, movingSelectMarkEnd_DocumentWidgetFlag, !isCloserToStart); - } - /* Move the start or the end depending on which is nearer. */ - if (d->flags & movingSelectMarkStart_DocumentWidgetFlag) { - d->selectMark.start = loc.start; - } - else { - d->selectMark.end = (d->selectMark.end > d->selectMark.start ? loc.end : loc.start); - } + } + return iTrue; + } + else if (equal_Command(cmd, "document.reload") && document_Command(cmd) == d) { + d->initNormScrollY = normScrollPos_DocumentView_(&d->view); + if (equalCase_Rangecc(urlScheme_String(d->mod.url), "titan")) { + /* Reopen so the Upload dialog gets shown. */ + postCommandf_App("open url:%s", cstr_String(d->mod.url)); + return iTrue; + } + fetch_DocumentWidget_(d); + return iTrue; + } + else if (equal_Command(cmd, "document.linkkeys") && document_App() == d) { + if (argLabel_Command(cmd, "release")) { + setLinkNumberMode_DocumentWidget_(d, iFalse); + } + else if (argLabel_Command(cmd, "more")) { + if (d->flags & showLinkNumbers_DocumentWidgetFlag && + d->ordinalMode == homeRow_DocumentLinkOrdinalMode) { + const size_t numKeys = iElemCount(homeRowKeys_); + const iGmRun *last = lastVisibleLink_DocumentView_(&d->view); + if (!last) { + d->ordinalBase = 0; } else { - d->selectMark.end = loc.end;// (d->selectMark.end > d->selectMark.start ? loc.end : loc.start); - if (loc.start < d->initialSelectMark.start) { - d->selectMark.end = loc.start; - } - if (isEmpty_Range(&d->selectMark)) { - d->selectMark = d->initialSelectMark; + d->ordinalBase += numKeys; + if (visibleLinkOrdinal_DocumentView_(&d->view, last->linkId) < d->ordinalBase) { + d->ordinalBase = 0; } } } - iAssert((!d->selectMark.start && !d->selectMark.end) || - ( d->selectMark.start && d->selectMark.end)); - /* Extend to full words/paragraphs. */ - if (d->flags & (selectWords_DocumentWidgetFlag | selectLines_DocumentWidgetFlag)) { - extendRange_Rangecc( - &d->selectMark, - range_String(source_GmDocument(view->doc)), - (d->flags & movingSelectMarkStart_DocumentWidgetFlag ? moveStart_RangeExtension - : moveEnd_RangeExtension) | - (d->flags & selectWords_DocumentWidgetFlag ? word_RangeExtension - : line_RangeExtension)); - if (d->flags & movingSelectMarkStart_DocumentWidgetFlag) { - d->initialSelectMark.start = - d->initialSelectMark.end = d->selectMark.start; - } + else if (~d->flags & showLinkNumbers_DocumentWidgetFlag) { + d->ordinalMode = homeRow_DocumentLinkOrdinalMode; + d->ordinalBase = 0; + setLinkNumberMode_DocumentWidget_(d, iTrue); + } + } + else { + d->ordinalMode = arg_Command(cmd); + d->ordinalBase = 0; + setLinkNumberMode_DocumentWidget_(d, iTrue); + iChangeFlags(d->flags, setHoverViaKeys_DocumentWidgetFlag, + argLabel_Command(cmd, "hover") != 0); + iChangeFlags(d->flags, newTabViaHomeKeys_DocumentWidgetFlag, + argLabel_Command(cmd, "newtab") != 0); + } + invalidateVisibleLinks_DocumentView_(&d->view); + refresh_Widget(d); + return iTrue; + } + else if (equal_Command(cmd, "navigate.back") && document_App() == d) { + if (d->request) { + postCommandf_Root(w->root, + "document.request.cancelled doc:%p url:%s", d, cstr_String(d->mod.url)); + iReleasePtr(&d->request); + updateFetchProgress_DocumentWidget_(d); + } + goBack_History(d->mod.history); + return iTrue; + } + else if (equal_Command(cmd, "navigate.forward") && document_App() == d) { + goForward_History(d->mod.history); + return iTrue; + } + else if (equal_Command(cmd, "navigate.parent") && document_App() == d) { + iUrl parts; + init_Url(&parts, d->mod.url); + /* Remove the last path segment. */ + if (size_Range(&parts.path) > 1) { + if (parts.path.end[-1] == '/') { + parts.path.end--; } - if (d->initialSelectMark.start) { - if (d->selectMark.end > d->selectMark.start) { - d->selectMark.start = d->initialSelectMark.start; - } - else if (d->selectMark.end < d->selectMark.start) { - d->selectMark.start = d->initialSelectMark.end; - } + while (parts.path.end > parts.path.start) { + if (parts.path.end[-1] == '/') break; + parts.path.end--; } -// printf("mark %zu ... %zu\n", d->selectMark.start - cstr_String(source_GmDocument(d->doc)), -// d->selectMark.end - cstr_String(source_GmDocument(d->doc))); -// fflush(stdout); - refresh_Widget(w); + postCommandf_Root(w->root, + "open url:%s", + cstr_Rangecc((iRangecc){ constBegin_String(d->mod.url), parts.path.end })); + } + return iTrue; + } + else if (equal_Command(cmd, "navigate.root") && document_App() == d) { + postCommandf_Root(w->root, "open url:%s/", cstr_Rangecc(urlRoot_String(d->mod.url))); + return iTrue; + } + else if (equalWidget_Command(cmd, w, "scroll.moved")) { + init_Anim(&d->view.scrollY.pos, arg_Command(cmd)); + updateVisible_DocumentView_(&d->view); + return iTrue; + } + else if (equal_Command(cmd, "scroll.page") && document_App() == d) { + const int dir = arg_Command(cmd); + if (dir > 0 && !argLabel_Command(cmd, "repeat") && + prefs_App()->loadImageInsteadOfScrolling && + fetchNextUnfetchedImage_DocumentWidget_(d)) { return iTrue; } - case finished_ClickResult: - if (d->grabbedPlayer) { - setGrabbedPlayer_DocumentWidget_(d, NULL); + const float amount = argLabel_Command(cmd, "full") != 0 ? 1.0f : 0.5f; + smoothScroll_DocumentView_(&d->view, + dir * amount * + height_Rect(documentBounds_DocumentView_(&d->view)), + smoothDuration_DocumentWidget_(keyboard_ScrollType)); + return iTrue; + } + else if (equal_Command(cmd, "scroll.top") && document_App() == d) { + init_Anim(&d->view.scrollY.pos, 0); + invalidate_VisBuf(d->view.visBuf); + clampScroll_DocumentView_(&d->view); + updateVisible_DocumentView_(&d->view); + refresh_Widget(w); + return iTrue; + } + else if (equal_Command(cmd, "scroll.bottom") && document_App() == d) { + updateScrollMax_DocumentView_(&d->view); /* scrollY.max might not be fully updated */ + init_Anim(&d->view.scrollY.pos, d->view.scrollY.max); + invalidate_VisBuf(d->view.visBuf); + clampScroll_DocumentView_(&d->view); + updateVisible_DocumentView_(&d->view); + refresh_Widget(w); + return iTrue; + } + else if (equal_Command(cmd, "scroll.step") && document_App() == d) { + const int dir = arg_Command(cmd); + if (dir > 0 && !argLabel_Command(cmd, "repeat") && + prefs_App()->loadImageInsteadOfScrolling && + fetchNextUnfetchedImage_DocumentWidget_(d)) { + return iTrue; + } + smoothScroll_DocumentView_(&d->view, + 3 * lineHeight_Text(paragraph_FontId) * dir, + smoothDuration_DocumentWidget_(keyboard_ScrollType)); + return iTrue; + } + else if (equal_Command(cmd, "document.goto") && document_App() == d) { + const char *heading = suffixPtr_Command(cmd, "heading"); + if (heading) { + if (isRequestOngoing_DocumentWidget(d)) { + /* Scroll position set when request finishes. */ + setCStr_String(&d->pendingGotoHeading, heading); return iTrue; } - if (isVisible_Widget(d->menu)) { - closeMenu_Widget(d->menu); + scrollToHeading_DocumentView_(&d->view, heading); + return iTrue; + } + const char *loc = pointerLabel_Command(cmd, "loc"); + const iGmRun *run = findRunAtLoc_GmDocument(d->view.doc, loc); + if (run) { + scrollTo_DocumentView_(&d->view, run->visBounds.pos.y, iFalse); + } + return iTrue; + } + else if ((equal_Command(cmd, "find.next") || equal_Command(cmd, "find.prev")) && + document_App() == d) { + const int dir = equal_Command(cmd, "find.next") ? +1 : -1; + iRangecc (*finder)(const iGmDocument *, const iString *, const char *) = + dir > 0 ? findText_GmDocument : findTextBefore_GmDocument; + iInputWidget *find = findWidget_App("find.input"); + if (isEmpty_String(text_InputWidget(find))) { + d->foundMark = iNullRange; + } + else { + const iBool wrap = d->foundMark.start != NULL; + d->foundMark = finder(d->view.doc, text_InputWidget(find), dir > 0 ? d->foundMark.end + : d->foundMark.start); + if (!d->foundMark.start && wrap) { + /* Wrap around. */ + d->foundMark = finder(d->view.doc, text_InputWidget(find), NULL); } - d->flags &= ~(movingSelectMarkStart_DocumentWidgetFlag | - movingSelectMarkEnd_DocumentWidgetFlag); - if (!isMoved_Click(&d->click)) { - setFocus_Widget(NULL); - /* Tap in tap selection mode. */ - if (flags_Widget(w) & touchDrag_WidgetFlag) { - const iRangecc tapLoc = sourceLoc_DocumentView_(view, pos_Click(&d->click)); - /* Tapping on the selection will show a menu. */ - const iRangecc mark = selectMark_DocumentWidget_(d); - if (tapLoc.start >= mark.start && tapLoc.end <= mark.end) { - if (d->copyMenu) { - closeMenu_Widget(d->copyMenu); - destroy_Widget(d->copyMenu); - d->copyMenu = NULL; - } - d->copyMenu = makeMenu_Widget(w, (iMenuItem[]){ - { clipCopy_Icon " ${menu.copy}", 0, 0, "copy" }, - { "---" }, - { close_Icon " ${menu.select.clear}", 0, 0, "document.select arg:0" }, - }, 3); - setFlags_Widget(d->copyMenu, noFadeBackground_WidgetFlag, iTrue); - openMenu_Widget(d->copyMenu, pos_Click(&d->click)); - return iTrue; - } - else { - /* Tapping elsewhere exits selection mode. */ - postCommand_Widget(d, "document.select arg:0"); - return iTrue; - } - } - if (view->hoverPre) { - togglePreFold_DocumentWidget_(d, preId_GmRun(view->hoverPre)); - return iTrue; + if (d->foundMark.start) { + const iGmRun *found; + if ((found = findRunAtLoc_GmDocument(d->view.doc, d->foundMark.start)) != NULL) { + scrollTo_DocumentView_(&d->view, mid_Rect(found->bounds).y, iTrue); } - if (view->hoverLink) { - /* TODO: Move this to a method. */ - const iGmLinkId linkId = view->hoverLink->linkId; - const iMediaId linkMedia = mediaId_GmRun(view->hoverLink); - const int linkFlags = linkFlags_GmDocument(view->doc, linkId); - iAssert(linkId); - /* Media links are opened inline by default. */ - if (isMediaLink_GmDocument(view->doc, linkId)) { - if (linkFlags & content_GmLinkFlag && linkFlags & permanent_GmLinkFlag) { - /* We have the content and it cannot be dismissed, so nothing - further to do. */ - return iTrue; - } - if (!requestMedia_DocumentWidget_(d, linkId, iTrue)) { - if (linkFlags & content_GmLinkFlag) { - /* Dismiss shown content on click. */ - setData_Media(media_GmDocument(view->doc), - linkId, - NULL, - NULL, - allowHide_MediaFlag); - /* Cancel a partially received request. */ { - iMediaRequest *req = findMediaRequest_DocumentWidget_(d, linkId); - if (!isFinished_GmRequest(req->req)) { - cancel_GmRequest(req->req); - removeMediaRequest_DocumentWidget_(d, linkId); - /* Note: Some of the audio IDs have changed now, layout must - be redone. */ - } - } - redoLayout_GmDocument(view->doc); - view->hoverLink = NULL; - clampScroll_DocumentView_(view); - updateVisible_DocumentView_(view); - invalidate_DocumentWidget_(d); - refresh_Widget(w); - return iTrue; - } - else { - /* Show the existing content again if we have it. */ - iMediaRequest *req = findMediaRequest_DocumentWidget_(d, linkId); - if (req) { - setData_Media(media_GmDocument(view->doc), - linkId, - meta_GmRequest(req->req), - body_GmRequest(req->req), - allowHide_MediaFlag); - redoLayout_GmDocument(view->doc); - updateVisible_DocumentView_(view); - invalidate_DocumentWidget_(d); - refresh_Widget(w); - return iTrue; - } - } - } - refresh_Widget(w); - } - else if (linkMedia.type == download_MediaType || - findMediaRequest_DocumentWidget_(d, linkId)) { - /* TODO: What should be done when clicking on an inline download? - Maybe dismiss if finished? */ - return iTrue; - } - else if (linkFlags & supportedScheme_GmLinkFlag) { - int tabMode = openTabMode_Sym(modState_Keys()); - if (isPinned_DocumentWidget_(d)) { - tabMode ^= otherRoot_OpenTabFlag; - } - interactingWithLink_DocumentWidget_(d, linkId); - postCommandf_Root(w->root, "open newtab:%d url:%s", - tabMode, - cstr_String(absoluteUrl_String( - d->mod.url, linkUrl_GmDocument(view->doc, linkId)))); - } - else { - const iString *url = absoluteUrl_String( - d->mod.url, linkUrl_GmDocument(view->doc, linkId)); - makeQuestion_Widget( - uiTextCaution_ColorEscape "${heading.openlink}", - format_CStr( - cstr_Lang("dlg.openlink.confirm"), - uiTextAction_ColorEscape, - cstr_String(url)), - (iMenuItem[]){ - { "${cancel}" }, - { uiTextCaution_ColorEscape "${dlg.openlink}", - 0, 0, format_CStr("!open default:1 url:%s", cstr_String(url)) } }, - 2); - } + } + } + if (flags_Widget(w) & touchDrag_WidgetFlag) { + postCommand_Root(w->root, "document.select arg:0"); /* we can't handle both at the same time */ + } + invalidateWideRunsWithNonzeroOffset_DocumentView_(&d->view); /* markers don't support offsets */ + resetWideRuns_DocumentView_(&d->view); + refresh_Widget(w); + return iTrue; + } + else if (equal_Command(cmd, "find.clearmark")) { + if (d->foundMark.start) { + d->foundMark = iNullRange; + refresh_Widget(w); + } + return iTrue; + } + else if (equal_Command(cmd, "bookmark.links") && document_App() == d) { + iPtrArray *links = collectNew_PtrArray(); + render_GmDocument(d->view.doc, (iRangei){ 0, size_GmDocument(d->view.doc).y }, addAllLinks_, links); + /* Find links that aren't already bookmarked. */ + iForEach(PtrArray, i, links) { + const iGmRun *run = i.ptr; + uint32_t bmid; + if ((bmid = findUrl_Bookmarks(bookmarks_App(), + linkUrl_GmDocument(d->view.doc, run->linkId))) != 0) { + const iBookmark *bm = get_Bookmarks(bookmarks_App(), bmid); + /* We can import local copies of remote bookmarks. */ + if (~bm->flags & remote_BookmarkFlag) { + remove_PtrArrayIterator(&i); } - if (d->selectMark.start && !(d->flags & (selectLines_DocumentWidgetFlag | - selectWords_DocumentWidgetFlag))) { - d->selectMark = iNullRange; - refresh_Widget(w); + } + } + if (!isEmpty_PtrArray(links)) { + if (argLabel_Command(cmd, "confirm")) { + const size_t count = size_PtrArray(links); + makeQuestion_Widget( + uiHeading_ColorEscape "${heading.import.bookmarks}", + formatCStrs_Lang("dlg.import.found.n", count), + (iMenuItem[]){ { "${cancel}" }, + { format_CStr(cstrCount_Lang("dlg.import.add.n", (int) count), + uiTextAction_ColorEscape, + count), + 0, + 0, + "bookmark.links" } }, + 2); + } + else { + iConstForEach(PtrArray, j, links) { + const iGmRun *run = j.ptr; + add_Bookmarks(bookmarks_App(), + linkUrl_GmDocument(d->view.doc, run->linkId), + collect_String(newRange_String(run->text)), + NULL, + 0x1f588 /* pin */); } + postCommand_App("bookmarks.changed"); } - return iTrue; - case aborted_ClickResult: - if (d->grabbedPlayer) { - setGrabbedPlayer_DocumentWidget_(d, NULL); - return iTrue; + } + else { + makeSimpleMessage_Widget(uiHeading_ColorEscape "${heading.import.bookmarks}", + "${dlg.import.notnew}"); + } + return iTrue; + } + else if (equalWidget_Command(cmd, w, "menu.closed")) { + updateHover_DocumentView_(&d->view, mouseCoord_Window(get_Window(), 0)); + } + else if (equal_Command(cmd, "document.autoreload")) { + if (d->mod.reloadInterval) { + if (!isValid_Time(&d->sourceTime) || elapsedSeconds_Time(&d->sourceTime) >= + seconds_ReloadInterval_(d->mod.reloadInterval)) { + postCommand_Widget(w, "document.reload"); } - return iTrue; - default: - break; + } } - return processEvent_Widget(w, ev); -} - -/*----------------------------------------------------------------------------------------------*/ - -iDeclareType(DrawContext) - -struct Impl_DrawContext { - const iDocumentView *view; - iRect widgetBounds; - iRect docBounds; - iRangei vis; - iInt2 viewPos; /* document area origin */ - iPaint paint; - iBool inSelectMark; - iBool inFoundMark; - iBool showLinkNumbers; - iRect firstMarkRect; - iRect lastMarkRect; - iGmRunRange runsDrawn; -}; - -static void fillRange_DrawContext_(iDrawContext *d, const iGmRun *run, enum iColorId color, - iRangecc mark, iBool *isInside) { - if (mark.start > mark.end) { - /* Selection may be done in either direction. */ - iSwap(const char *, mark.start, mark.end); + else if (equal_Command(cmd, "document.autoreload.menu") && document_App() == d) { + iArray *items = collectNew_Array(sizeof(iMenuItem)); + for (int i = 0; i < max_ReloadInterval; ++i) { + pushBack_Array(items, &(iMenuItem){ + format_CStr("%s%s", ((int) d->mod.reloadInterval == i ? "&" : "*"), + label_ReloadInterval_(i)), + 0, + 0, + format_CStr("document.autoreload.set arg:%d", i) }); + } + pushBack_Array(items, &(iMenuItem){ "${cancel}", 0, 0, NULL }); + makeQuestion_Widget(uiTextAction_ColorEscape "${heading.autoreload}", + "${dlg.autoreload}", + constData_Array(items), size_Array(items)); + return iTrue; } - if (*isInside || (contains_Range(&run->text, mark.start) || - contains_Range(&mark, run->text.start))) { - int x = 0; - if (!*isInside) { - x = measureRange_Text(run->font, - (iRangecc){ run->text.start, iMax(run->text.start, mark.start) }) - .advance.x; + else if (equal_Command(cmd, "document.autoreload.set") && document_App() == d) { + d->mod.reloadInterval = arg_Command(cmd); + } + else if (equalWidget_Command(cmd, w, "document.dismiss")) { + const iString *site = collectNewRange_String(urlRoot_String(d->mod.url)); + const int dismissed = value_SiteSpec(site, dismissWarnings_SiteSpecKey); + const int arg = argLabel_Command(cmd, "warning"); + setValue_SiteSpec(site, dismissWarnings_SiteSpecKey, dismissed | arg); + if (arg == ansiEscapes_GmDocumentWarning) { + remove_Banner(d->banner, ansiEscapes_GmStatusCode); + refresh_Widget(w); } - int w = width_Rect(run->visBounds) - x; - if (contains_Range(&run->text, mark.end) || mark.end < run->text.start) { - iRangecc mk = !*isInside ? mark - : (iRangecc){ run->text.start, iMax(run->text.start, mark.end) }; - mk.start = iMax(mk.start, run->text.start); - w = measureRange_Text(run->font, mk).advance.x; - *isInside = iFalse; + return iTrue; + } + else if (startsWith_CStr(cmd, "pinch.") && document_Command(cmd) == d) { + return handlePinch_DocumentWidget_(d, cmd); + } + else if ((startsWith_CStr(cmd, "edgeswipe.") || startsWith_CStr(cmd, "swipe.")) && + document_App() == d) { + return handleSwipe_DocumentWidget_(d, cmd); + } + else if (equal_Command(cmd, "document.setmediatype") && document_App() == d) { + if (!isRequestOngoing_DocumentWidget(d)) { + setUrlAndSource_DocumentWidget(d, d->mod.url, string_Command(cmd, "mime"), + &d->sourceContent); + } + return iTrue; + } + else if (equal_Command(cmd, "fontpack.install") && document_App() == d) { + if (argLabel_Command(cmd, "ttf")) { + iAssert(!cmp_String(&d->sourceMime, "font/ttf")); + installFontFile_Fonts(collect_String(suffix_Command(cmd, "name")), &d->sourceContent); + postCommand_App("open url:about:fonts"); } else { - *isInside = iTrue; /* at least until the next run */ + const iString *id = idFromUrl_FontPack(d->mod.url); + install_Fonts(id, &d->sourceContent); + postCommandf_App("open gotoheading:%s url:about:fonts", cstr_String(id)); } - if (w > width_Rect(run->visBounds) - x) { - w = width_Rect(run->visBounds) - x; + return iTrue; + } + return iFalse; +} + +static void setGrabbedPlayer_DocumentWidget_(iDocumentWidget *d, const iGmRun *run) { + if (run && run->mediaType == audio_MediaType) { + iPlayer *plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run)); + setFlags_Player(plr, volumeGrabbed_PlayerFlag, iTrue); + d->grabbedStartVolume = volume_Player(plr); + d->grabbedPlayer = run; + refresh_Widget(d); + } + else if (d->grabbedPlayer) { + setFlags_Player( + audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(d->grabbedPlayer)), + volumeGrabbed_PlayerFlag, + iFalse); + d->grabbedPlayer = NULL; + refresh_Widget(d); + } + else { + iAssert(iFalse); + } +} + +static iBool processMediaEvents_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) { + if (ev->type != SDL_MOUSEBUTTONDOWN && ev->type != SDL_MOUSEBUTTONUP && + ev->type != SDL_MOUSEMOTION) { + return iFalse; + } + if (ev->type == SDL_MOUSEBUTTONDOWN || ev->type == SDL_MOUSEBUTTONUP) { + if (ev->button.button != SDL_BUTTON_LEFT) { + return iFalse; } - if (~run->flags & decoration_GmRunFlag) { - const iInt2 visPos = - add_I2(run->bounds.pos, addY_I2(d->viewPos, viewPos_DocumentView_(d->view))); - const iRect rangeRect = { addX_I2(visPos, x), init_I2(w, height_Rect(run->bounds)) }; - if (rangeRect.size.x) { - fillRect_Paint(&d->paint, rangeRect, color); - /* Keep track of the first and last marked rects. */ - if (d->firstMarkRect.size.x == 0) { - d->firstMarkRect = rangeRect; + } + if (d->grabbedPlayer) { + /* Updated in the drag. */ + return iFalse; + } + const iInt2 mouse = init_I2(ev->button.x, ev->button.y); + iConstForEach(PtrArray, i, &d->view.visibleMedia) { + const iGmRun *run = i.ptr; + if (run->mediaType != audio_MediaType) { + continue; + } + /* TODO: move this to mediaui.c */ + const iRect rect = runRect_DocumentView_(&d->view, run); + iPlayer * plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run)); + if (contains_Rect(rect, mouse)) { + iPlayerUI ui; + init_PlayerUI(&ui, plr, rect); + if (ev->type == SDL_MOUSEBUTTONDOWN && flags_Player(plr) & adjustingVolume_PlayerFlag && + contains_Rect(adjusted_Rect(ui.volumeAdjustRect, + zero_I2(), + init_I2(-height_Rect(ui.volumeAdjustRect), 0)), + mouse)) { + setGrabbedPlayer_DocumentWidget_(d, run); + processEvent_Click(&d->click, ev); + /* The rest is done in the DocumentWidget click responder. */ + refresh_Widget(d); + return iTrue; + } + else if (ev->type == SDL_MOUSEBUTTONDOWN || ev->type == SDL_MOUSEMOTION) { + refresh_Widget(d); + return iTrue; + } + if (contains_Rect(ui.playPauseRect, mouse)) { + setPaused_Player(plr, !isPaused_Player(plr)); + animateMedia_DocumentWidget_(d); + return iTrue; + } + else if (contains_Rect(ui.rewindRect, mouse)) { + if (isStarted_Player(plr) && time_Player(plr) > 0.5f) { + stop_Player(plr); + start_Player(plr); + setPaused_Player(plr, iTrue); } - d->lastMarkRect = rangeRect; + refresh_Widget(d); + return iTrue; + } + else if (contains_Rect(ui.volumeRect, mouse)) { + setFlags_Player(plr, + adjustingVolume_PlayerFlag, + !(flags_Player(plr) & adjustingVolume_PlayerFlag)); + animateMedia_DocumentWidget_(d); + refresh_Widget(d); + return iTrue; + } + else if (contains_Rect(ui.menuRect, mouse)) { + /* TODO: Add menu items for: + - output device + - Save to Downloads + */ + if (d->playerMenu) { + destroy_Widget(d->playerMenu); + d->playerMenu = NULL; + return iTrue; + } + d->playerMenu = makeMenu_Widget( + as_Widget(d), + (iMenuItem[]){ + { cstrCollect_String(metadataLabel_Player(plr)) }, + }, + 1); + openMenu_Widget(d->playerMenu, bottomLeft_Rect(ui.menuRect)); + return iTrue; } } } - /* Link URLs are not part of the visible document, so they are ignored above. Handle - these ranges as a special case. */ - if (run->linkId && run->flags & decoration_GmRunFlag) { - const iRangecc url = linkUrlRange_GmDocument(d->view->doc, run->linkId); - if (contains_Range(&url, mark.start) && - (contains_Range(&url, mark.end) || url.end == mark.end)) { - fillRect_Paint( - &d->paint, - moved_Rect(run->visBounds, addY_I2(d->viewPos, viewPos_DocumentView_(d->view))), - color); - } - } + return iFalse; } -static void drawMark_DrawContext_(void *context, const iGmRun *run) { - iDrawContext *d = context; - if (!isMedia_GmRun(run)) { - fillRange_DrawContext_(d, run, uiMatching_ColorId, d->view->owner->foundMark, &d->inFoundMark); - fillRange_DrawContext_(d, run, uiMarked_ColorId, d->view->owner->selectMark, &d->inSelectMark); - } +static void beginMarkingSelection_DocumentWidget_(iDocumentWidget *d, iInt2 pos) { + setFocus_Widget(NULL); /* TODO: Focus this document? */ + invalidateWideRunsWithNonzeroOffset_DocumentView_(&d->view); + resetWideRuns_DocumentView_(&d->view); /* Selections don't support horizontal scrolling. */ + iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iTrue); + d->initialSelectMark = d->selectMark = sourceLoc_DocumentView_(&d->view, pos); + refresh_Widget(as_Widget(d)); } -static void drawRun_DrawContext_(void *context, const iGmRun *run) { - iDrawContext *d = context; - const iInt2 origin = d->viewPos; - /* Keep track of the drawn visible runs. */ { - if (!d->runsDrawn.start || run < d->runsDrawn.start) { - d->runsDrawn.start = run; - } - if (!d->runsDrawn.end || run > d->runsDrawn.end) { - d->runsDrawn.end = run; - } - } - if (run->mediaType == image_MediaType) { - SDL_Texture *tex = imageTexture_Media(media_GmDocument(d->view->doc), mediaId_GmRun(run)); - const iRect dst = moved_Rect(run->visBounds, origin); - if (tex) { - fillRect_Paint(&d->paint, dst, tmBackground_ColorId); /* in case the image has alpha */ - SDL_RenderCopy(d->paint.dst->render, tex, NULL, - &(SDL_Rect){ dst.pos.x, dst.pos.y, dst.size.x, dst.size.y }); - } - else { - drawRect_Paint(&d->paint, dst, tmQuoteIcon_ColorId); - drawCentered_Text(uiLabel_FontId, - dst, - iFalse, - tmQuote_ColorId, - explosion_Icon " Error Loading Image"); - } +static void interactingWithLink_DocumentWidget_(iDocumentWidget *d, iGmLinkId id) { + iRangecc loc = linkUrlRange_GmDocument(d->view.doc, id); + if (!loc.start) { + clear_String(&d->linePrecedingLink); return; } - else if (isMedia_GmRun(run)) { - /* Media UIs are drawn afterwards as a dynamic overlay. */ - return; + const char *start = range_String(source_GmDocument(d->view.doc)).start; + /* Find the preceding line. This is offered as a prefill option for a possible input query. */ + while (loc.start > start && *loc.start != '\n') { + loc.start--; } - enum iColorId fg = run->color; - const iGmDocument *doc = d->view->doc; - const int linkFlags = linkFlags_GmDocument(doc, run->linkId); - /* Hover state of a link. */ - iBool isHover = - (run->linkId && d->view->hoverLink && run->linkId == d->view->hoverLink->linkId && - ~run->flags & decoration_GmRunFlag); - /* Visible (scrolled) position of the run. */ - const iInt2 visPos = addX_I2(add_I2(run->visBounds.pos, origin), - /* Preformatted runs can be scrolled. */ - runOffset_DocumentView_(d->view, run)); - const iRect visRect = { visPos, run->visBounds.size }; - /* Fill the background. */ { -#if 0 - iBool isInlineImageCaption = run->linkId && linkFlags & content_GmLinkFlag && - ~linkFlags & permanent_GmLinkFlag; - if (run->flags & decoration_GmRunFlag && ~run->flags & startOfLine_GmRunFlag) { - /* This is the metadata. */ - isInlineImageCaption = iFalse; - } -#endif - /* While this is consistent, it's a bit excessive to indicate that an inlined image - is open: the image itself is the indication. */ - const iBool isInlineImageCaption = iFalse; - if (run->linkId && (linkFlags & isOpen_GmLinkFlag || isInlineImageCaption)) { - /* Open links get a highlighted background. */ - int bg = tmBackgroundOpenLink_ColorId; - const int frame = tmFrameOpenLink_ColorId; - const int pad = gap_Text; - iRect wideRect = { init_I2(origin.x - pad, visPos.y), - init_I2(d->docBounds.size.x + 2 * pad, - height_Rect(run->visBounds)) }; - adjustEdges_Rect(&wideRect, - run->flags & startOfLine_GmRunFlag ? -pad * 3 / 4 : 0, 0, - run->flags & endOfLine_GmRunFlag ? pad * 3 / 4 : 0, 0); - /* The first line is composed of two runs that may be drawn in either order, so - only draw half of the background. */ - if (run->flags & decoration_GmRunFlag) { - wideRect.size.x = right_Rect(visRect) - left_Rect(wideRect); - } - else if (run->flags & startOfLine_GmRunFlag) { - wideRect.size.x = right_Rect(wideRect) - left_Rect(visRect); - wideRect.pos.x = left_Rect(visRect); - } - fillRect_Paint(&d->paint, wideRect, bg); - } - else { - /* Normal background for other runs. There are cases when runs get drawn multiple times, - e.g., at the buffer boundary, and there are slightly overlapping characters in - monospace blocks. Clearing the background here ensures a cleaner visual appearance - since only one glyph is visible at any given point. */ - fillRect_Paint(&d->paint, visRect, tmBackground_ColorId); - } + loc.end = loc.start; /* End of the preceding line. */ + if (loc.start > start) { + loc.start--; } - if (run->linkId) { - if (run->flags & decoration_GmRunFlag && run->flags & startOfLine_GmRunFlag) { - /* Link icon. */ - if (linkFlags & content_GmLinkFlag) { - fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart); - } - } - else if (~run->flags & decoration_GmRunFlag) { - fg = linkColor_GmDocument(doc, run->linkId, isHover ? textHover_GmLinkPart : text_GmLinkPart); - if (linkFlags & content_GmLinkFlag) { - fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart); /* link is inactive */ - } + while (loc.start > start && *loc.start != '\n') { + loc.start--; + } + if (*loc.start == '\n') { + loc.start++; /* Start of the preceding line. */ + } + setRange_String(&d->linePrecedingLink, loc); +} + +iLocalDef int wheelSwipeSide_DocumentWidget_(const iDocumentWidget *d) { + return (d->flags & rightWheelSwipe_DocumentWidgetFlag ? 2 + : d->flags & leftWheelSwipe_DocumentWidgetFlag ? 1 + : 0); +} + +static void finishWheelSwipe_DocumentWidget_(iDocumentWidget *d) { + if (d->flags & eitherWheelSwipe_DocumentWidgetFlag && + d->wheelSwipeState == direct_WheelSwipeState) { + const int side = wheelSwipeSide_DocumentWidget_(d); + int abort = ((side == 1 && d->swipeSpeed < 0) || (side == 2 && d->swipeSpeed > 0)); + if (iAbs(d->wheelSwipeDistance) < width_Widget(d) / 4 && iAbs(d->swipeSpeed) < 4 * gap_UI) { + abort = 1; } + postCommand_Widget(d, "edgeswipe.ended side:%d abort:%d", side, abort); + d->flags &= ~eitherWheelSwipe_DocumentWidgetFlag; } - if (run->flags & altText_GmRunFlag) { - const iInt2 margin = preRunMargin_GmDocument(doc, preId_GmRun(run)); - fillRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmBackgroundAltText_ColorId); - drawRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmFrameAltText_ColorId); - drawWrapRange_Text(run->font, - add_I2(visPos, margin), - run->visBounds.size.x - 2 * margin.x, - run->color, - run->text); +} + +static iBool handleWheelSwipe_DocumentWidget_(iDocumentWidget *d, const SDL_MouseWheelEvent *ev) { + iWidget *w = as_Widget(d); + if (deviceType_App() != desktop_AppDeviceType) { + return iFalse; } - else { - if (d->showLinkNumbers && run->linkId && run->flags & decoration_GmRunFlag) { - const size_t ord = visibleLinkOrdinal_DocumentView_(d->view, run->linkId); - if (ord >= d->view->owner->ordinalBase) { - const iChar ordChar = - linkOrdinalChar_DocumentWidget_(d->view->owner, ord - d->view->owner->ordinalBase); - if (ordChar) { - const char *circle = "\u25ef"; /* Large Circle */ - const int circleFont = FONT_ID(default_FontId, regular_FontStyle, contentRegular_FontSize); - iRect nbArea = { init_I2(d->viewPos.x - gap_UI / 3, visPos.y), - init_I2(3.95f * gap_Text, 1.0f * lineHeight_Text(circleFont)) }; - drawRange_Text( - circleFont, topLeft_Rect(nbArea), tmQuote_ColorId, range_CStr(circle)); - iRect circleArea = visualBounds_Text(circleFont, range_CStr(circle)); - addv_I2(&circleArea.pos, topLeft_Rect(nbArea)); - drawCentered_Text(FONT_ID(default_FontId, regular_FontStyle, contentSmall_FontSize), - circleArea, - iTrue, - tmQuote_ColorId, - "%lc", - (int) ordChar); - goto runDrawn; + if (~flags_Widget(w) & horizontalOffset_WidgetFlag) { + return iFalse; + } + iAssert(~d->flags & animationPlaceholder_DocumentWidgetFlag); +// printf("STATE:%d wheel x:%d inert:%d end:%d\n", d->wheelSwipeState, +// ev->x, isInertia_MouseWheelEvent(ev), +// isScrollFinished_MouseWheelEvent(ev)); +// fflush(stdout); + switch (d->wheelSwipeState) { + case none_WheelSwipeState: + /* A new swipe starts. */ + if (!isInertia_MouseWheelEvent(ev) && !isScrollFinished_MouseWheelEvent(ev)) { + int side = ev->x > 0 ? 1 : 2; + d->wheelSwipeDistance = ev->x * 2; + d->flags &= ~eitherWheelSwipe_DocumentWidgetFlag; + d->flags |= (side == 1 ? leftWheelSwipe_DocumentWidgetFlag + : rightWheelSwipe_DocumentWidgetFlag); + // printf("swipe starts at %d, side %d\n", d->wheelSwipeDistance, side); + d->wheelSwipeState = direct_WheelSwipeState; + d->swipeSpeed = 0; + postCommand_Widget(d, "edgeswipe.moved arg:%d side:%d", d->wheelSwipeDistance, side); + return iTrue; + } + break; + case direct_WheelSwipeState: + if (isInertia_MouseWheelEvent(ev) || isScrollFinished_MouseWheelEvent(ev)) { + finishWheelSwipe_DocumentWidget_(d); + d->wheelSwipeState = none_WheelSwipeState; + } + else { + int step = ev->x * 2; + d->wheelSwipeDistance += step; + /* Remember the maximum speed. */ + if (d->swipeSpeed < 0 && step < 0) { + d->swipeSpeed = iMin(d->swipeSpeed, step); + } + else if (d->swipeSpeed > 0 && step > 0) { + d->swipeSpeed = iMax(d->swipeSpeed, step); + } + else { + d->swipeSpeed = step; + } + switch (wheelSwipeSide_DocumentWidget_(d)) { + case 1: + d->wheelSwipeDistance = iMax(0, d->wheelSwipeDistance); + d->wheelSwipeDistance = iMin(width_Widget(d), d->wheelSwipeDistance); + break; + case 2: + d->wheelSwipeDistance = iMin(0, d->wheelSwipeDistance); + d->wheelSwipeDistance = iMax(-width_Widget(d), d->wheelSwipeDistance); + break; } + /* TODO: calculate speed, rememeber direction */ + //printf("swipe moved to %d, side %d\n", d->wheelSwipeDistance, side); + postCommand_Widget(d, "edgeswipe.moved arg:%d side:%d", d->wheelSwipeDistance, + wheelSwipeSide_DocumentWidget_(d)); } + return iTrue; + } + return iFalse; +} + +static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) { + iWidget *w = as_Widget(d); + iDocumentView *view = &d->view; + if (isMetricsChange_UserEvent(ev)) { + updateSize_DocumentWidget(d); + } + else if (processEvent_SmoothScroll(&d->view.scrollY, ev)) { + return iTrue; + } + else if (ev->type == SDL_USEREVENT && ev->user.code == command_UserEventCode) { + if (isCommand_Widget(w, ev, "pullaction")) { + postCommand_Widget(w, "navigate.reload"); + return iTrue; } - if (run->flags & quoteBorder_GmRunFlag) { - drawVLine_Paint(&d->paint, - addX_I2(visPos, - !run->isRTL - ? -gap_Text * 5 / 2 - : (width_Rect(run->visBounds) + gap_Text * 5 / 2)), - height_Rect(run->visBounds), - tmQuoteIcon_ColorId); - } - /* Base attributes. */ { - int f, c; - runBaseAttributes_GmDocument(doc, run, &f, &c); - setBaseAttributes_Text(f, c); + if (!handleCommand_DocumentWidget_(d, command_UserEvent(ev))) { + /* Base class commands. */ + return processEvent_Widget(w, ev); } - drawBoundRange_Text(run->font, - visPos, - (run->isRTL ? -1 : 1) * width_Rect(run->visBounds), - fg, - run->text); - setBaseAttributes_Text(-1, -1); - runDrawn:; + return iTrue; } - /* Presentation of links. */ - if (run->linkId && ~run->flags & decoration_GmRunFlag) { - const int metaFont = paragraph_FontId; - /* TODO: Show status of an ongoing media request. */ - const int flags = linkFlags; - const iRect linkRect = moved_Rect(run->visBounds, origin); - iMediaRequest *mr = NULL; - /* Show metadata about inline content. */ - if (flags & content_GmLinkFlag && run->flags & endOfLine_GmRunFlag) { - fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart); - iString text; - init_String(&text); - const iMediaId linkMedia = findMediaForLink_Media(constMedia_GmDocument(doc), - run->linkId, none_MediaType); - iAssert(linkMedia.type != none_MediaType); - iGmMediaInfo info; - info_Media(constMedia_GmDocument(doc), linkMedia, &info); - switch (linkMedia.type) { - case image_MediaType: { - /* There's a separate decorative GmRun for the metadata. */ - break; + if (ev->type == SDL_KEYDOWN) { + const int key = ev->key.keysym.sym; + if ((d->flags & showLinkNumbers_DocumentWidgetFlag) && + ((key >= '1' && key <= '9') || (key >= 'a' && key <= 'z'))) { + const size_t ord = linkOrdinalFromKey_DocumentWidget_(d, key) + d->ordinalBase; + iConstForEach(PtrArray, i, &d->view.visibleLinks) { + if (ord == iInvalidPos) break; + const iGmRun *run = i.ptr; + if (run->flags & decoration_GmRunFlag && + visibleLinkOrdinal_DocumentView_(view, run->linkId) == ord) { + if (d->flags & setHoverViaKeys_DocumentWidgetFlag) { + view->hoverLink = run; + } + else { + postCommandf_Root( + w->root, + "open newtab:%d url:%s", + (isPinned_DocumentWidget_(d) ? otherRoot_OpenTabFlag : 0) ^ + (d->ordinalMode == numbersAndAlphabet_DocumentLinkOrdinalMode + ? openTabMode_Sym(modState_Keys()) + : (d->flags & newTabViaHomeKeys_DocumentWidgetFlag ? 1 : 0)), + cstr_String(absoluteUrl_String( + d->mod.url, linkUrl_GmDocument(view->doc, run->linkId)))); + interactingWithLink_DocumentWidget_(d, run->linkId); + } + setLinkNumberMode_DocumentWidget_(d, iFalse); + invalidateVisibleLinks_DocumentView_(view); + refresh_Widget(d); + return iTrue; } - case audio_MediaType: - format_String(&text, "%s", info.type); - break; - case download_MediaType: - format_String(&text, "%s", info.type); - break; - default: - break; } - if (linkMedia.type != download_MediaType && /* can't cancel downloads currently */ - linkMedia.type != image_MediaType && - findMediaRequest_DocumentWidget_(d->view->owner, run->linkId)) { - appendFormat_String( - &text, " %s" close_Icon, isHover ? escape_Color(tmLinkText_ColorId) : ""); + } + switch (key) { + case SDLK_ESCAPE: + if (d->flags & showLinkNumbers_DocumentWidgetFlag && document_App() == d) { + setLinkNumberMode_DocumentWidget_(d, iFalse); + invalidateVisibleLinks_DocumentView_(view); + refresh_Widget(d); + return iTrue; + } + break; +#if !defined (NDEBUG) + case SDLK_KP_1: + case '`': { + iBlock *seed = new_Block(64); + for (size_t i = 0; i < 64; ++i) { + setByte_Block(seed, i, iRandom(0, 256)); + } + setThemeSeed_GmDocument(view->doc, seed); + delete_Block(seed); + invalidate_DocumentWidget_(d); + refresh_Widget(w); + break; } - const iInt2 size = measureRange_Text(metaFont, range_String(&text)).bounds.size; - if (size.x) { - fillRect_Paint( - &d->paint, - (iRect){ add_I2(origin, addX_I2(topRight_Rect(run->bounds), -size.x - gap_UI)), - addX_I2(size, 2 * gap_UI) }, - tmBackground_ColorId); - drawAlign_Text(metaFont, - add_I2(topRight_Rect(run->bounds), origin), - fg, - right_Alignment, - "%s", cstr_String(&text)); +#endif +#if 0 + case '0': { + extern int enableHalfPixelGlyphs_Text; + enableHalfPixelGlyphs_Text = !enableHalfPixelGlyphs_Text; + refresh_Widget(w); + printf("halfpixel: %d\n", enableHalfPixelGlyphs_Text); + fflush(stdout); + break; } - deinit_String(&text); - } - else if (run->flags & endOfLine_GmRunFlag && - (mr = findMediaRequest_DocumentWidget_(d->view->owner, run->linkId)) != NULL) { - if (!isFinished_GmRequest(mr->req)) { - draw_Text(metaFont, - topRight_Rect(linkRect), - tmInlineContentMetadata_ColorId, - translateCStr_Lang(" \u2014 ${doc.fetching}\u2026 (%.1f ${mb})"), - (float) bodySize_GmRequest(mr->req) / 1.0e6f); +#endif +#if 0 + case '0': { + extern int enableKerning_Text; + enableKerning_Text = !enableKerning_Text; + invalidate_DocumentWidget_(d); + refresh_Widget(w); + printf("kerning: %d\n", enableKerning_Text); + fflush(stdout); + break; } +#endif } } - if (0) { - drawRect_Paint(&d->paint, (iRect){ visPos, run->bounds.size }, green_ColorId); - drawRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, red_ColorId); - } -} - -static int drawSideRect_(iPaint *p, iRect rect) { - int bg = tmBannerBackground_ColorId; - int fg = tmBannerIcon_ColorId; - if (equal_Color(get_Color(bg), get_Color(tmBackground_ColorId))) { - bg = tmBannerIcon_ColorId; - fg = tmBannerBackground_ColorId; - } - fillRect_Paint(p, rect, bg); - return fg; -} - -static int sideElementAvailWidth_DocumentView_(const iDocumentView *d) { - return left_Rect(documentBounds_DocumentView_(d)) - - left_Rect(bounds_Widget(constAs_Widget(d->owner))) - 2 * d->pageMargin * gap_UI; -} - -static iBool isSideHeadingVisible_DocumentView_(const iDocumentView *d) { - return sideElementAvailWidth_DocumentView_(d) >= lineHeight_Text(banner_FontId) * 4.5f; -} - -static void updateSideIconBuf_DocumentView_(const iDocumentView *d) { - if (!isExposed_Window(get_Window())) { - return; - } - iDrawBufs *dbuf = d->drawBufs; - dbuf->flags &= ~updateSideBuf_DrawBufsFlag; - if (dbuf->sideIconBuf) { - SDL_DestroyTexture(dbuf->sideIconBuf); - dbuf->sideIconBuf = NULL; +#if defined (iPlatformAppleDesktop) + else if (ev->type == SDL_MOUSEWHEEL && + ev->wheel.y == 0 && + d->wheelSwipeState == direct_WheelSwipeState && + handleWheelSwipe_DocumentWidget_(d, &ev->wheel)) { + return iTrue; } -// const iGmRun *banner = siteBanner_GmDocument(d->doc); - if (isEmpty_Banner(d->owner->banner)) { - return; +#endif + else if (ev->type == SDL_MOUSEWHEEL && isHover_Widget(w)) { + const iInt2 mouseCoord = coord_MouseWheelEvent(&ev->wheel); + if (isPerPixel_MouseWheelEvent(&ev->wheel)) { + const iInt2 wheel = init_I2(ev->wheel.x, ev->wheel.y); + stop_Anim(&d->view.scrollY.pos); + immediateScroll_DocumentView_(view, -wheel.y); + if (!scrollWideBlock_DocumentView_(view, mouseCoord, -wheel.x, 0) && + wheel.x) { + handleWheelSwipe_DocumentWidget_(d, &ev->wheel); + } + } + else { + /* Traditional mouse wheel. */ + const int amount = ev->wheel.y; + if (keyMods_Sym(modState_Keys()) == KMOD_PRIMARY) { + postCommandf_App("zoom.delta arg:%d", amount > 0 ? 10 : -10); + return iTrue; + } + smoothScroll_DocumentView_(view, + -3 * amount * lineHeight_Text(paragraph_FontId), + smoothDuration_DocumentWidget_(mouse_ScrollType)); + scrollWideBlock_DocumentView_( + view, mouseCoord, -3 * ev->wheel.x * lineHeight_Text(paragraph_FontId), 167); + } + iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iTrue); + return iTrue; } - const int margin = gap_UI * d->pageMargin; - const int minBannerSize = lineHeight_Text(banner_FontId) * 2; - const iChar icon = siteIcon_GmDocument(d->doc); - const int avail = sideElementAvailWidth_DocumentView_(d) - margin; - iBool isHeadingVisible = isSideHeadingVisible_DocumentView_(d); - /* Determine the required size. */ - iInt2 bufSize = init1_I2(minBannerSize); - const int sideHeadingFont = FONT_ID(documentHeading_FontId, regular_FontStyle, contentBig_FontSize); - if (isHeadingVisible) { - const iInt2 headingSize = measureWrapRange_Text(sideHeadingFont, avail, - currentHeading_DocumentView_(d)).bounds.size; - if (headingSize.x > 0) { - bufSize.y += gap_Text + headingSize.y; - bufSize.x = iMax(bufSize.x, headingSize.x); + else if (ev->type == SDL_MOUSEMOTION) { + if (ev->motion.which != SDL_TOUCH_MOUSEID) { + iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iFalse); + } + const iInt2 mpos = init_I2(ev->motion.x, ev->motion.y); + if (isVisible_Widget(d->menu)) { + setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW); + } +#if 0 + else if (contains_Rect(siteBannerRect_DocumentWidget_(d), mpos)) { + setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_HAND); } +#endif else { - isHeadingVisible = iFalse; + if (value_Anim(&view->altTextOpacity) < 0.833f) { + setValue_Anim(&view->altTextOpacity, 0, 0); /* keep it hidden while moving */ + } + updateHover_DocumentView_(view, mpos); } } - SDL_Renderer *render = renderer_Window(get_Window()); - dbuf->sideIconBuf = SDL_CreateTexture(render, - SDL_PIXELFORMAT_RGBA4444, - SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET, - bufSize.x, bufSize.y); - iPaint p; - init_Paint(&p); - beginTarget_Paint(&p, dbuf->sideIconBuf); - const iColor back = get_Color(tmBannerSideTitle_ColorId); - SDL_SetRenderDrawColor(render, back.r, back.g, back.b, 0); /* better blending of the edge */ - SDL_RenderClear(render); - const iRect iconRect = { zero_I2(), init1_I2(minBannerSize) }; - int fg = drawSideRect_(&p, iconRect); - iString str; - initUnicodeN_String(&str, &icon, 1); - drawCentered_Text(banner_FontId, iconRect, iTrue, fg, "%s", cstr_String(&str)); - deinit_String(&str); - if (isHeadingVisible) { - iRangecc text = currentHeading_DocumentView_(d); - iInt2 pos = addY_I2(bottomLeft_Rect(iconRect), gap_Text); - const int font = sideHeadingFont; - drawWrapRange_Text(font, pos, avail, tmBannerSideTitle_ColorId, text); + if (ev->type == SDL_USEREVENT && ev->user.code == widgetTapBegins_UserEventCode) { + iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iFalse); + return iTrue; } - endTarget_Paint(&p); - SDL_SetTextureBlendMode(dbuf->sideIconBuf, SDL_BLENDMODE_BLEND); -} - -static void drawSideElements_DocumentView_(const iDocumentView *d) { - const iWidget *w = constAs_Widget(d->owner); - const iRect bounds = bounds_Widget(w); - const iRect docBounds = documentBounds_DocumentView_(d); - const int margin = gap_UI * d->pageMargin; - float opacity = value_Anim(&d->sideOpacity); - const int avail = left_Rect(docBounds) - left_Rect(bounds) - 2 * margin; - iDrawBufs * dbuf = d->drawBufs; - iPaint p; - init_Paint(&p); - setClip_Paint(&p, boundsWithoutVisualOffset_Widget(w)); - /* Side icon and current heading. */ - if (prefs_App()->sideIcon && opacity > 0 && dbuf->sideIconBuf) { - const iInt2 texSize = size_SDLTexture(dbuf->sideIconBuf); - if (avail > texSize.x) { - const int minBannerSize = lineHeight_Text(banner_FontId) * 2; - iInt2 pos = addY_I2(add_I2(topLeft_Rect(bounds), init_I2(margin, 0)), - height_Rect(bounds) / 2 - minBannerSize / 2 - - (texSize.y > minBannerSize - ? (gap_Text + lineHeight_Text(heading3_FontId)) / 2 - : 0)); - SDL_SetTextureAlphaMod(dbuf->sideIconBuf, 255 * opacity); - SDL_RenderCopy(renderer_Window(get_Window()), - dbuf->sideIconBuf, NULL, - &(SDL_Rect){ pos.x, pos.y, texSize.x, texSize.y }); + if (ev->type == SDL_MOUSEBUTTONDOWN) { + if (ev->button.button == SDL_BUTTON_X1) { + postCommand_Root(w->root, "navigate.back"); + return iTrue; + } + if (ev->button.button == SDL_BUTTON_X2) { + postCommand_Root(w->root, "navigate.forward"); + return iTrue; + } + if (ev->button.button == SDL_BUTTON_MIDDLE && view->hoverLink) { + interactingWithLink_DocumentWidget_(d, view->hoverLink->linkId); + postCommandf_Root(w->root, "open newtab:%d url:%s", + (isPinned_DocumentWidget_(d) ? otherRoot_OpenTabFlag : 0) | + (modState_Keys() & KMOD_SHIFT ? new_OpenTabFlag : newBackground_OpenTabFlag), + cstr_String(linkUrl_GmDocument(view->doc, view->hoverLink->linkId))); + return iTrue; + } + if (ev->button.button == SDL_BUTTON_RIGHT && + contains_Widget(w, init_I2(ev->button.x, ev->button.y))) { + if (!isVisible_Widget(d->menu)) { + d->contextLink = view->hoverLink; + d->contextPos = init_I2(ev->button.x, ev->button.y); + if (d->menu) { + destroy_Widget(d->menu); + d->menu = NULL; + } + setFocus_Widget(NULL); + iArray items; + init_Array(&items, sizeof(iMenuItem)); + if (d->contextLink) { + /* Context menu for a link. */ + interactingWithLink_DocumentWidget_(d, d->contextLink->linkId); /* perhaps will be triggered */ + const iString *linkUrl = linkUrl_GmDocument(view->doc, d->contextLink->linkId); +// const int linkFlags = linkFlags_GmDocument(d->doc, d->contextLink->linkId); + const iRangecc scheme = urlScheme_String(linkUrl); + const iBool isGemini = equalCase_Rangecc(scheme, "gemini"); + iBool isNative = iFalse; + if (deviceType_App() != desktop_AppDeviceType) { + /* Show the link as the first, non-interactive item. */ + pushBack_Array(&items, &(iMenuItem){ + format_CStr("```%s", cstr_String(linkUrl)), + 0, 0, NULL }); + } + if (willUseProxy_App(scheme) || isGemini || + equalCase_Rangecc(scheme, "file") || + equalCase_Rangecc(scheme, "finger") || + equalCase_Rangecc(scheme, "gopher")) { + isNative = iTrue; + /* Regular links that we can open. */ + pushBackN_Array( + &items, + (iMenuItem[]){ { openTab_Icon " ${link.newtab}", + 0, + 0, + format_CStr("!open newtab:1 origin:%s url:%s", + cstr_String(id_Widget(w)), + cstr_String(linkUrl)) }, + { openTabBg_Icon " ${link.newtab.background}", + 0, + 0, + format_CStr("!open newtab:2 origin:%s url:%s", + cstr_String(id_Widget(w)), + cstr_String(linkUrl)) }, + { "${link.side}", + 0, + 0, + format_CStr("!open newtab:4 origin:%s url:%s", + cstr_String(id_Widget(w)), + cstr_String(linkUrl)) }, + { "${link.side.newtab}", + 0, + 0, + format_CStr("!open newtab:5 origin:%s url:%s", + cstr_String(id_Widget(w)), + cstr_String(linkUrl)) } }, + 4); + if (deviceType_App() == phone_AppDeviceType) { + removeN_Array(&items, size_Array(&items) - 2, iInvalidSize); + } + } + else if (!willUseProxy_App(scheme)) { + pushBack_Array( + &items, + &(iMenuItem){ openExt_Icon " ${link.browser}", + 0, + 0, + format_CStr("!open default:1 url:%s", cstr_String(linkUrl)) }); + } + if (willUseProxy_App(scheme)) { + pushBackN_Array( + &items, + (iMenuItem[]){ + { "---" }, + { isGemini ? "${link.noproxy}" : openExt_Icon " ${link.browser}", + 0, + 0, + format_CStr("!open origin:%s noproxy:1 url:%s", + cstr_String(id_Widget(w)), + cstr_String(linkUrl)) } }, + 2); + } + iString *linkLabel = collectNewRange_String( + linkLabel_GmDocument(view->doc, d->contextLink->linkId)); + urlEncodeSpaces_String(linkLabel); + pushBackN_Array(&items, + (iMenuItem[]){ { "---" }, + { "${link.copy}", 0, 0, "document.copylink" }, + { bookmark_Icon " ${link.bookmark}", + 0, + 0, + format_CStr("!bookmark.add title:%s url:%s", + cstr_String(linkLabel), + cstr_String(linkUrl)) }, + }, + 3); + if (isNative && d->contextLink->mediaType != download_MediaType) { + pushBackN_Array(&items, (iMenuItem[]){ + { "---" }, + { download_Icon " ${link.download}", 0, 0, "document.downloadlink" }, + }, 2); + } + iMediaRequest *mediaReq; + if ((mediaReq = findMediaRequest_DocumentWidget_(d, d->contextLink->linkId)) != NULL && + d->contextLink->mediaType != download_MediaType) { + if (isFinished_GmRequest(mediaReq->req)) { + pushBack_Array(&items, + &(iMenuItem){ download_Icon " " saveToDownloads_Label, + 0, + 0, + format_CStr("document.media.save link:%u", + d->contextLink->linkId) }); + } + } + if (equalCase_Rangecc(scheme, "file")) { + /* Local files may be deleted. */ + pushBack_Array( + &items, + &(iMenuItem){ delete_Icon " " uiTextCaution_ColorEscape + "${link.file.delete}", + 0, + 0, + format_CStr("!file.delete confirm:1 path:%s", + cstrCollect_String( + localFilePathFromUrl_String(linkUrl))) }); + } + } + else if (deviceType_App() == desktop_AppDeviceType) { + if (!isEmpty_Range(&d->selectMark)) { + pushBackN_Array(&items, + (iMenuItem[]){ { "${menu.copy}", 0, 0, "copy" }, + { "---", 0, 0, NULL } }, + 2); + } + pushBackN_Array( + &items, + (iMenuItem[]){ + { backArrow_Icon " ${menu.back}", navigateBack_KeyShortcut, "navigate.back" }, + { forwardArrow_Icon " ${menu.forward}", navigateForward_KeyShortcut, "navigate.forward" }, + { upArrow_Icon " ${menu.parent}", navigateParent_KeyShortcut, "navigate.parent" }, + { upArrowBar_Icon " ${menu.root}", navigateRoot_KeyShortcut, "navigate.root" }, + { "---" }, + { reload_Icon " ${menu.reload}", reload_KeyShortcut, "navigate.reload" }, + { timer_Icon " ${menu.autoreload}", 0, 0, "document.autoreload.menu" }, + { "---" }, + { bookmark_Icon " ${menu.page.bookmark}", SDLK_d, KMOD_PRIMARY, "bookmark.add" }, + { star_Icon " ${menu.page.subscribe}", subscribeToPage_KeyModifier, "feeds.subscribe" }, + { "---" }, + { book_Icon " ${menu.page.import}", 0, 0, "bookmark.links confirm:1" }, + { globe_Icon " ${menu.page.translate}", 0, 0, "document.translate" }, + { upload_Icon " ${menu.page.upload}", 0, 0, "document.upload" }, + { "---" }, + { "${menu.page.copyurl}", 0, 0, "document.copylink" } }, + 16); + if (isEmpty_Range(&d->selectMark)) { + pushBackN_Array( + &items, + (iMenuItem[]){ + { "${menu.page.copysource}", 'c', KMOD_PRIMARY, "copy" }, + { download_Icon " " saveToDownloads_Label, SDLK_s, KMOD_PRIMARY, "document.save" } }, + 2); + } + } + else { + /* Mobile text selection menu. */ +#if 0 + pushBackN_Array( + &items, + (iMenuItem[]){ + { "${menu.select}", 0, 0, "document.select arg:1" }, + { "${menu.select.word}", 0, 0, "document.select arg:2" }, + { "${menu.select.par}", 0, 0, "document.select arg:3" }, + }, + 3); +#endif + postCommand_Root(w->root, "document.select arg:1"); + return iTrue; + } + d->menu = makeMenu_Widget(w, data_Array(&items), size_Array(&items)); + deinit_Array(&items); + setMenuItemDisabled_Widget( + d->menu, + "document.upload", + !equalCase_Rangecc(urlScheme_String(d->mod.url), "gemini") && + !equalCase_Rangecc(urlScheme_String(d->mod.url), "titan")); + } + processContextMenuEvent_Widget(d->menu, ev, {}); } } - /* Reception timestamp. */ - if (dbuf->timestampBuf && dbuf->timestampBuf->size.x <= avail) { - draw_TextBuf( - dbuf->timestampBuf, - add_I2( - bottomLeft_Rect(bounds), - init_I2(margin, - -margin + -dbuf->timestampBuf->size.y + - iMax(0, d->scrollY.max - pos_SmoothScroll(&d->scrollY)))), - tmQuoteIcon_ColorId); - } - unsetClip_Paint(&p); -} - -static void drawMedia_DocumentView_(const iDocumentView *d, iPaint *p) { - iConstForEach(PtrArray, i, &d->visibleMedia) { - const iGmRun * run = i.ptr; - if (run->mediaType == audio_MediaType) { - iPlayerUI ui; - init_PlayerUI(&ui, - audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run)), - runRect_DocumentView_(d, run)); - draw_PlayerUI(&ui, p); - } - else if (run->mediaType == download_MediaType) { - iDownloadUI ui; - init_DownloadUI(&ui, constMedia_GmDocument(d->doc), run->mediaId, - runRect_DocumentView_(d, run)); - draw_DownloadUI(&ui, p); - } + if (processMediaEvents_DocumentWidget_(d, ev)) { + return iTrue; } -} - -static void extend_GmRunRange_(iGmRunRange *runs) { - if (runs->start) { - runs->start--; - runs->end++; + if (processEvent_Banner(d->banner, ev)) { + return iTrue; } -} - -static iBool render_DocumentView_(const iDocumentView *d, iDrawContext *ctx, iBool prerenderExtra) { - iBool didDraw = iFalse; - const iRect bounds = bounds_Widget(constAs_Widget(d->owner)); - const iRect ctxWidgetBounds = - init_Rect(0, - 0, - width_Rect(bounds) - constAs_Widget(d->owner->scroll)->rect.size.x, - height_Rect(bounds)); - const iRangei full = { 0, size_GmDocument(d->doc).y }; - const iRangei vis = ctx->vis; - iVisBuf *visBuf = d->visBuf; /* will be updated now */ - d->drawBufs->lastRenderTime = SDL_GetTicks(); - /* Swap buffers around to have room available both before and after the visible region. */ - allocVisBuffer_DocumentView_(d); - reposition_VisBuf(visBuf, vis); - /* Redraw the invalid ranges. */ - if (~flags_Widget(constAs_Widget(d->owner)) & destroyPending_WidgetFlag) { - iPaint *p = &ctx->paint; - init_Paint(p); - iForIndices(i, visBuf->buffers) { - iVisBufTexture *buf = &visBuf->buffers[i]; - iVisBufMeta *meta = buf->user; - const iRangei bufRange = intersect_Rangei(bufferRange_VisBuf(visBuf, i), full); - const iRangei bufVisRange = intersect_Rangei(bufRange, vis); - ctx->widgetBounds = moved_Rect(ctxWidgetBounds, init_I2(0, -buf->origin)); - ctx->viewPos = init_I2(left_Rect(ctx->docBounds) - left_Rect(bounds), -buf->origin); -// printf(" buffer %zu: buf vis range %d...%d\n", i, bufVisRange.start, bufVisRange.end); - if (!prerenderExtra && !isEmpty_Range(&bufVisRange)) { - didDraw = iTrue; - if (isEmpty_Rangei(buf->validRange)) { - /* Fill the required currently visible range (vis). */ - const iRangei bufVisRange = intersect_Rangei(bufRange, vis); - if (!isEmpty_Range(&bufVisRange)) { - beginTarget_Paint(p, buf->texture); - fillRect_Paint(p, (iRect){ zero_I2(), visBuf->texSize }, tmBackground_ColorId); - iZap(ctx->runsDrawn); - render_GmDocument(d->doc, bufVisRange, drawRun_DrawContext_, ctx); - meta->runsDrawn = ctx->runsDrawn; - extend_GmRunRange_(&meta->runsDrawn); - buf->validRange = bufVisRange; - // printf(" buffer %zu valid %d...%d\n", i, bufRange.start, bufRange.end); + /* The left mouse button. */ + switch (processEvent_Click(&d->click, ev)) { + case started_ClickResult: + if (d->grabbedPlayer) { + return iTrue; + } + /* Enable hover state now that scrolling has surely finished. */ + if (d->flags & noHoverWhileScrolling_DocumentWidgetFlag) { + d->flags &= ~noHoverWhileScrolling_DocumentWidgetFlag; + updateHover_DocumentView_(view, mouseCoord_Window(get_Window(), ev->button.which)); + } + if (~flags_Widget(w) & touchDrag_WidgetFlag) { + iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iFalse); + iChangeFlags(d->flags, selectWords_DocumentWidgetFlag, d->click.count == 2); + iChangeFlags(d->flags, selectLines_DocumentWidgetFlag, d->click.count >= 3); + /* Double/triple clicks marks the selection immediately. */ + if (d->click.count >= 2) { + beginMarkingSelection_DocumentWidget_(d, d->click.startPos); + extendRange_Rangecc( + &d->selectMark, + range_String(source_GmDocument(view->doc)), + bothStartAndEnd_RangeExtension | + (d->click.count == 2 ? word_RangeExtension : line_RangeExtension)); + d->initialSelectMark = d->selectMark; + refresh_Widget(w); + } + else { + d->initialSelectMark = iNullRange; + } + } + return iTrue; + case drag_ClickResult: { + if (d->grabbedPlayer) { + iPlayer *plr = + audioPlayer_Media(media_GmDocument(view->doc), mediaId_GmRun(d->grabbedPlayer)); + iPlayerUI ui; + init_PlayerUI(&ui, plr, runRect_DocumentView_(view, d->grabbedPlayer)); + float off = (float) delta_Click(&d->click).x / (float) width_Rect(ui.volumeSlider); + setVolume_Player(plr, d->grabbedStartVolume + off); + refresh_Widget(w); + return iTrue; + } + /* Fold/unfold a preformatted block. */ + if (~d->flags & selecting_DocumentWidgetFlag && view->hoverPre && + preIsFolded_GmDocument(view->doc, preId_GmRun(view->hoverPre))) { + return iTrue; + } + /* Begin selecting a range of text. */ + if (~d->flags & selecting_DocumentWidgetFlag) { + beginMarkingSelection_DocumentWidget_(d, d->click.startPos); + } + iRangecc loc = sourceLoc_DocumentView_(view, pos_Click(&d->click)); + if (d->selectMark.start == NULL) { + d->selectMark = loc; + } + else if (loc.end) { + if (flags_Widget(w) & touchDrag_WidgetFlag) { + /* Choose which end to move. */ + if (!(d->flags & (movingSelectMarkStart_DocumentWidgetFlag | + movingSelectMarkEnd_DocumentWidgetFlag))) { + const iRangecc mark = selectMark_DocumentWidget_(d); + const char * midMark = mark.start + size_Range(&mark) / 2; + const iRangecc loc = sourceLoc_DocumentView_(view, pos_Click(&d->click)); + const iBool isCloserToStart = d->selectMark.start > d->selectMark.end ? + (loc.start > midMark) : (loc.start < midMark); + iChangeFlags(d->flags, movingSelectMarkStart_DocumentWidgetFlag, isCloserToStart); + iChangeFlags(d->flags, movingSelectMarkEnd_DocumentWidgetFlag, !isCloserToStart); + } + /* Move the start or the end depending on which is nearer. */ + if (d->flags & movingSelectMarkStart_DocumentWidgetFlag) { + d->selectMark.start = loc.start; + } + else { + d->selectMark.end = (d->selectMark.end > d->selectMark.start ? loc.end : loc.start); } } else { - /* Progressively fill the required runs. */ - if (meta->runsDrawn.start) { - beginTarget_Paint(p, buf->texture); - meta->runsDrawn.start = renderProgressive_GmDocument(d->doc, meta->runsDrawn.start, - -1, iInvalidSize, - bufVisRange, - drawRun_DrawContext_, - ctx); - buf->validRange.start = bufVisRange.start; + d->selectMark.end = loc.end;// (d->selectMark.end > d->selectMark.start ? loc.end : loc.start); + if (loc.start < d->initialSelectMark.start) { + d->selectMark.end = loc.start; + } + if (isEmpty_Range(&d->selectMark)) { + d->selectMark = d->initialSelectMark; + } + } + } + iAssert((!d->selectMark.start && !d->selectMark.end) || + ( d->selectMark.start && d->selectMark.end)); + /* Extend to full words/paragraphs. */ + if (d->flags & (selectWords_DocumentWidgetFlag | selectLines_DocumentWidgetFlag)) { + extendRange_Rangecc( + &d->selectMark, + range_String(source_GmDocument(view->doc)), + (d->flags & movingSelectMarkStart_DocumentWidgetFlag ? moveStart_RangeExtension + : moveEnd_RangeExtension) | + (d->flags & selectWords_DocumentWidgetFlag ? word_RangeExtension + : line_RangeExtension)); + if (d->flags & movingSelectMarkStart_DocumentWidgetFlag) { + d->initialSelectMark.start = + d->initialSelectMark.end = d->selectMark.start; + } + } + if (d->initialSelectMark.start) { + if (d->selectMark.end > d->selectMark.start) { + d->selectMark.start = d->initialSelectMark.start; + } + else if (d->selectMark.end < d->selectMark.start) { + d->selectMark.start = d->initialSelectMark.end; + } + } +// printf("mark %zu ... %zu\n", d->selectMark.start - cstr_String(source_GmDocument(d->doc)), +// d->selectMark.end - cstr_String(source_GmDocument(d->doc))); +// fflush(stdout); + refresh_Widget(w); + return iTrue; + } + case finished_ClickResult: + if (d->grabbedPlayer) { + setGrabbedPlayer_DocumentWidget_(d, NULL); + return iTrue; + } + if (isVisible_Widget(d->menu)) { + closeMenu_Widget(d->menu); + } + d->flags &= ~(movingSelectMarkStart_DocumentWidgetFlag | + movingSelectMarkEnd_DocumentWidgetFlag); + if (!isMoved_Click(&d->click)) { + setFocus_Widget(NULL); + /* Tap in tap selection mode. */ + if (flags_Widget(w) & touchDrag_WidgetFlag) { + const iRangecc tapLoc = sourceLoc_DocumentView_(view, pos_Click(&d->click)); + /* Tapping on the selection will show a menu. */ + const iRangecc mark = selectMark_DocumentWidget_(d); + if (tapLoc.start >= mark.start && tapLoc.end <= mark.end) { + if (d->copyMenu) { + closeMenu_Widget(d->copyMenu); + destroy_Widget(d->copyMenu); + d->copyMenu = NULL; + } + d->copyMenu = makeMenu_Widget(w, (iMenuItem[]){ + { clipCopy_Icon " ${menu.copy}", 0, 0, "copy" }, + { "---" }, + { close_Icon " ${menu.select.clear}", 0, 0, "document.select arg:0" }, + }, 3); + setFlags_Widget(d->copyMenu, noFadeBackground_WidgetFlag, iTrue); + openMenu_Widget(d->copyMenu, pos_Click(&d->click)); + return iTrue; } - if (meta->runsDrawn.end) { - beginTarget_Paint(p, buf->texture); - meta->runsDrawn.end = renderProgressive_GmDocument(d->doc, meta->runsDrawn.end, - +1, iInvalidSize, - bufVisRange, - drawRun_DrawContext_, - ctx); - buf->validRange.end = bufVisRange.end; + else { + /* Tapping elsewhere exits selection mode. */ + postCommand_Widget(d, "document.select arg:0"); + return iTrue; } } - } - /* Progressively draw the rest of the buffer if it isn't fully valid. */ - if (prerenderExtra && !equal_Rangei(bufRange, buf->validRange)) { - const iGmRun *next; -// printf("%zu: prerenderExtra (start:%p end:%p)\n", i, meta->runsDrawn.start, meta->runsDrawn.end); - if (meta->runsDrawn.start == NULL) { - /* Haven't drawn anything yet in this buffer, so let's try seeding it. */ - const int rh = lineHeight_Text(paragraph_FontId); - const int y = i >= iElemCount(visBuf->buffers) / 2 ? bufRange.start : (bufRange.end - rh); - beginTarget_Paint(p, buf->texture); - fillRect_Paint(p, (iRect){ zero_I2(), visBuf->texSize }, tmBackground_ColorId); - buf->validRange = (iRangei){ y, y + rh }; - iZap(ctx->runsDrawn); - render_GmDocument(d->doc, buf->validRange, drawRun_DrawContext_, ctx); - meta->runsDrawn = ctx->runsDrawn; - extend_GmRunRange_(&meta->runsDrawn); -// printf("%zu: seeded, next %p:%p\n", i, meta->runsDrawn.start, meta->runsDrawn.end); - didDraw = iTrue; + if (view->hoverPre) { + togglePreFold_DocumentWidget_(d, preId_GmRun(view->hoverPre)); + return iTrue; } - else { - if (meta->runsDrawn.start) { - const iRangei upper = intersect_Rangei(bufRange, (iRangei){ full.start, buf->validRange.start }); - if (upper.end > upper.start) { - beginTarget_Paint(p, buf->texture); - next = renderProgressive_GmDocument(d->doc, meta->runsDrawn.start, - -1, 1, upper, - drawRun_DrawContext_, - ctx); - if (next && meta->runsDrawn.start != next) { - meta->runsDrawn.start = next; - buf->validRange.start = bottom_Rect(next->visBounds); - didDraw = iTrue; - } - else { - buf->validRange.start = bufRange.start; - } + if (view->hoverLink) { + /* TODO: Move this to a method. */ + const iGmLinkId linkId = view->hoverLink->linkId; + const iMediaId linkMedia = mediaId_GmRun(view->hoverLink); + const int linkFlags = linkFlags_GmDocument(view->doc, linkId); + iAssert(linkId); + /* Media links are opened inline by default. */ + if (isMediaLink_GmDocument(view->doc, linkId)) { + if (linkFlags & content_GmLinkFlag && linkFlags & permanent_GmLinkFlag) { + /* We have the content and it cannot be dismissed, so nothing + further to do. */ + return iTrue; } - } - if (!didDraw && meta->runsDrawn.end) { - const iRangei lower = intersect_Rangei(bufRange, (iRangei){ buf->validRange.end, full.end }); - if (lower.end > lower.start) { - beginTarget_Paint(p, buf->texture); - next = renderProgressive_GmDocument(d->doc, meta->runsDrawn.end, - +1, 1, lower, - drawRun_DrawContext_, - ctx); - if (next && meta->runsDrawn.end != next) { - meta->runsDrawn.end = next; - buf->validRange.end = top_Rect(next->visBounds); - didDraw = iTrue; + if (!requestMedia_DocumentWidget_(d, linkId, iTrue)) { + if (linkFlags & content_GmLinkFlag) { + /* Dismiss shown content on click. */ + setData_Media(media_GmDocument(view->doc), + linkId, + NULL, + NULL, + allowHide_MediaFlag); + /* Cancel a partially received request. */ { + iMediaRequest *req = findMediaRequest_DocumentWidget_(d, linkId); + if (!isFinished_GmRequest(req->req)) { + cancel_GmRequest(req->req); + removeMediaRequest_DocumentWidget_(d, linkId); + /* Note: Some of the audio IDs have changed now, layout must + be redone. */ + } + } + redoLayout_GmDocument(view->doc); + view->hoverLink = NULL; + clampScroll_DocumentView_(view); + updateVisible_DocumentView_(view); + invalidate_DocumentWidget_(d); + refresh_Widget(w); + return iTrue; } else { - buf->validRange.end = bufRange.end; + /* Show the existing content again if we have it. */ + iMediaRequest *req = findMediaRequest_DocumentWidget_(d, linkId); + if (req) { + setData_Media(media_GmDocument(view->doc), + linkId, + meta_GmRequest(req->req), + body_GmRequest(req->req), + allowHide_MediaFlag); + redoLayout_GmDocument(view->doc); + updateVisible_DocumentView_(view); + invalidate_DocumentWidget_(d); + refresh_Widget(w); + return iTrue; + } } } + refresh_Widget(w); } - } - } - /* Draw any invalidated runs that fall within this buffer. */ - if (!prerenderExtra) { - const iRangei bufRange = { buf->origin, buf->origin + visBuf->texSize.y }; - /* Clear full-width backgrounds first in case there are any dynamic elements. */ { - iConstForEach(PtrSet, r, d->invalidRuns) { - const iGmRun *run = *r.value; - if (isOverlapping_Rangei(bufRange, ySpan_Rect(run->visBounds))) { - beginTarget_Paint(p, buf->texture); - fillRect_Paint(p, - init_Rect(0, - run->visBounds.pos.y - buf->origin, - visBuf->texSize.x, - run->visBounds.size.y), - tmBackground_ColorId); - } - } - } - setAnsiFlags_Text(ansiEscapes_GmDocument(d->doc)); - iConstForEach(PtrSet, r, d->invalidRuns) { - const iGmRun *run = *r.value; - if (isOverlapping_Rangei(bufRange, ySpan_Rect(run->visBounds))) { - beginTarget_Paint(p, buf->texture); - drawRun_DrawContext_(ctx, run); + else if (linkMedia.type == download_MediaType || + findMediaRequest_DocumentWidget_(d, linkId)) { + /* TODO: What should be done when clicking on an inline download? + Maybe dismiss if finished? */ + return iTrue; + } + else if (linkFlags & supportedScheme_GmLinkFlag) { + int tabMode = openTabMode_Sym(modState_Keys()); + if (isPinned_DocumentWidget_(d)) { + tabMode ^= otherRoot_OpenTabFlag; + } + interactingWithLink_DocumentWidget_(d, linkId); + postCommandf_Root(w->root, "open newtab:%d url:%s", + tabMode, + cstr_String(absoluteUrl_String( + d->mod.url, linkUrl_GmDocument(view->doc, linkId)))); + } + else { + const iString *url = absoluteUrl_String( + d->mod.url, linkUrl_GmDocument(view->doc, linkId)); + makeQuestion_Widget( + uiTextCaution_ColorEscape "${heading.openlink}", + format_CStr( + cstr_Lang("dlg.openlink.confirm"), + uiTextAction_ColorEscape, + cstr_String(url)), + (iMenuItem[]){ + { "${cancel}" }, + { uiTextCaution_ColorEscape "${dlg.openlink}", + 0, 0, format_CStr("!open default:1 url:%s", cstr_String(url)) } }, + 2); } } - setAnsiFlags_Text(allowAll_AnsiFlag); - } - endTarget_Paint(p); - if (prerenderExtra && didDraw) { - /* Just a run at a time. */ - break; - } - } - if (!prerenderExtra) { - clear_PtrSet(d->invalidRuns); - } - } - return didDraw; -} - -static void prerender_DocumentWidget_(iAny *context) { - iAssert(isInstance_Object(context, &Class_DocumentWidget)); - if (current_Root() == NULL) { - /* The widget has probably been removed from the widget tree, pending destruction. - Tickers are not cancelled until the widget is actually destroyed. */ - return; - } - const iDocumentWidget *d = context; - iDrawContext ctx = { - .view = &d->view, - .docBounds = documentBounds_DocumentView_(&d->view), - .vis = visibleRange_DocumentView_(&d->view), - .showLinkNumbers = (d->flags & showLinkNumbers_DocumentWidgetFlag) != 0 - }; -// printf("%u prerendering\n", SDL_GetTicks()); - if (d->view.visBuf->buffers[0].texture) { - makePaletteGlobal_GmDocument(d->view.doc); - if (render_DocumentView_(&d->view, &ctx, iTrue /* just fill up progressively */)) { - /* Something was drawn, should check later if there is still more to do. */ - addTicker_App(prerender_DocumentWidget_, context); - } - } -} - -static void checkPendingInvalidation_DocumentWidget_(const iDocumentWidget *d) { - if (d->flags & invalidationPending_DocumentWidgetFlag && - !isAffectedByVisualOffset_Widget(constAs_Widget(d))) { -// printf("%p visoff: %d\n", d, left_Rect(bounds_Widget(w)) - left_Rect(boundsWithoutVisualOffset_Widget(w))); - iDocumentWidget *m = (iDocumentWidget *) d; /* Hrrm, not const... */ - m->flags &= ~invalidationPending_DocumentWidgetFlag; - invalidate_DocumentWidget_(m); - } -} - -static void draw_DocumentView_(const iDocumentView *d) { - const iWidget *w = constAs_Widget(d->owner); - const iRect bounds = bounds_Widget(w); - const iRect boundsWithoutVisOff = boundsWithoutVisualOffset_Widget(w); - const iRect clipBounds = intersect_Rect(bounds, boundsWithoutVisOff); - /* Each document has its own palette, but the drawing routines rely on a global one. - As we're now drawing a document, ensure that the right palette is in effect. - Document theme colors can be used elsewhere, too, but first a document's palette - must be made global. */ - makePaletteGlobal_GmDocument(d->doc); - if (d->drawBufs->flags & updateTimestampBuf_DrawBufsFlag) { - updateTimestampBuf_DocumentView_(d); - } - if (d->drawBufs->flags & updateSideBuf_DrawBufsFlag) { - updateSideIconBuf_DocumentView_(d); - } - const iRect docBounds = documentBounds_DocumentView_(d); - const iRangei vis = visibleRange_DocumentView_(d); - iDrawContext ctx = { - .view = d, - .docBounds = docBounds, - .vis = vis, - .showLinkNumbers = (d->owner->flags & showLinkNumbers_DocumentWidgetFlag) != 0, - }; - init_Paint(&ctx.paint); - render_DocumentView_(d, &ctx, iFalse /* just the mandatory parts */); - iBanner *banner = d->owner->banner; - int yTop = docBounds.pos.y + viewPos_DocumentView_(d); - const iBool isDocEmpty = size_GmDocument(d->doc).y == 0; - const iBool isTouchSelecting = (flags_Widget(w) & touchDrag_WidgetFlag) != 0; - if (!isDocEmpty || !isEmpty_Banner(banner)) { - const int docBgColor = isDocEmpty ? tmBannerBackground_ColorId : tmBackground_ColorId; - setClip_Paint(&ctx.paint, clipBounds); - if (!isDocEmpty) { - draw_VisBuf(d->visBuf, init_I2(bounds.pos.x, yTop), ySpan_Rect(bounds)); - } - /* Text markers. */ - if (!isEmpty_Range(&d->owner->foundMark) || !isEmpty_Range(&d->owner->selectMark)) { - SDL_Renderer *render = renderer_Window(get_Window()); - ctx.firstMarkRect = zero_Rect(); - ctx.lastMarkRect = zero_Rect(); - SDL_SetRenderDrawBlendMode(render, - isDark_ColorTheme(colorTheme_App()) ? SDL_BLENDMODE_ADD - : SDL_BLENDMODE_BLEND); - ctx.viewPos = topLeft_Rect(docBounds); - /* Marker starting outside the visible range? */ - if (d->visibleRuns.start) { - if (!isEmpty_Range(&d->owner->selectMark) && - d->owner->selectMark.start < d->visibleRuns.start->text.start && - d->owner->selectMark.end > d->visibleRuns.start->text.start) { - ctx.inSelectMark = iTrue; - } - if (isEmpty_Range(&d->owner->foundMark) && - d->owner->foundMark.start < d->visibleRuns.start->text.start && - d->owner->foundMark.end > d->visibleRuns.start->text.start) { - ctx.inFoundMark = iTrue; + if (d->selectMark.start && !(d->flags & (selectLines_DocumentWidgetFlag | + selectWords_DocumentWidgetFlag))) { + d->selectMark = iNullRange; + refresh_Widget(w); } } - render_GmDocument(d->doc, vis, drawMark_DrawContext_, &ctx); - SDL_SetRenderDrawBlendMode(render, SDL_BLENDMODE_NONE); - /* Selection range pins. */ - if (isTouchSelecting) { - drawPin_Paint(&ctx.paint, ctx.firstMarkRect, 0, tmQuote_ColorId); - drawPin_Paint(&ctx.paint, ctx.lastMarkRect, 1, tmQuote_ColorId); - } - } - drawMedia_DocumentView_(d, &ctx.paint); - /* Fill the top and bottom, in case the document is short. */ - if (yTop > top_Rect(bounds)) { - fillRect_Paint(&ctx.paint, - (iRect){ bounds.pos, init_I2(bounds.size.x, yTop - top_Rect(bounds)) }, - !isEmpty_Banner(banner) ? tmBannerBackground_ColorId - : docBgColor); - } - /* Banner. */ - if (!isDocEmpty || numItems_Banner(banner) > 0) { - /* Fill the part between the banner and the top of the document. */ - fillRect_Paint(&ctx.paint, - (iRect){ init_I2(left_Rect(bounds), - top_Rect(docBounds) + viewPos_DocumentView_(d) - - documentTopPad_DocumentView_(d)), - init_I2(bounds.size.x, documentTopPad_DocumentView_(d)) }, - docBgColor); - setPos_Banner(banner, addY_I2(topLeft_Rect(docBounds), - -pos_SmoothScroll(&d->scrollY))); - draw_Banner(banner); - } - const int yBottom = yTop + size_GmDocument(d->doc).y; - if (yBottom < bottom_Rect(bounds)) { - fillRect_Paint(&ctx.paint, - init_Rect(bounds.pos.x, yBottom, bounds.size.x, bottom_Rect(bounds) - yBottom), - !isDocEmpty ? docBgColor : tmBannerBackground_ColorId); - } - unsetClip_Paint(&ctx.paint); - drawSideElements_DocumentView_(d); - /* Alt text. */ - const float altTextOpacity = value_Anim(&d->altTextOpacity) * 6 - 5; - if (d->hoverAltPre && altTextOpacity > 0) { - const iGmPreMeta *meta = preMeta_GmDocument(d->doc, preId_GmRun(d->hoverAltPre)); - if (meta->flags & topLeft_GmPreMetaFlag && ~meta->flags & decoration_GmRunFlag && - !isEmpty_Range(&meta->altText)) { - const int margin = 3 * gap_UI / 2; - const int altFont = uiLabel_FontId; - const int wrap = docBounds.size.x - 2 * margin; - iInt2 pos = addY_I2(add_I2(docBounds.pos, meta->pixelRect.pos), - viewPos_DocumentView_(d)); - const iInt2 textSize = measureWrapRange_Text(altFont, wrap, meta->altText).bounds.size; - pos.y -= textSize.y + gap_UI; - pos.y = iMax(pos.y, top_Rect(bounds)); - const iRect altRect = { pos, init_I2(docBounds.size.x, textSize.y) }; - ctx.paint.alpha = altTextOpacity * 255; - if (altTextOpacity < 1) { - SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND); - } - fillRect_Paint(&ctx.paint, altRect, tmBackgroundAltText_ColorId); - drawRect_Paint(&ctx.paint, altRect, tmFrameAltText_ColorId); - setOpacity_Text(altTextOpacity); - drawWrapRange_Text(altFont, addX_I2(pos, margin), wrap, - tmQuote_ColorId, meta->altText); - SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE); - setOpacity_Text(1.0f); + return iTrue; + case aborted_ClickResult: + if (d->grabbedPlayer) { + setGrabbedPlayer_DocumentWidget_(d, NULL); + return iTrue; } - } - /* Touch selection indicator. */ - if (isTouchSelecting) { - iRect rect = { topLeft_Rect(bounds), - init_I2(width_Rect(bounds), lineHeight_Text(uiLabelBold_FontId)) }; - fillRect_Paint(&ctx.paint, rect, uiTextAction_ColorId); - const iRangecc mark = selectMark_DocumentWidget_(d->owner); - drawCentered_Text(uiLabelBold_FontId, - rect, - iFalse, - uiBackground_ColorId, - "%zu bytes selected", /* TODO: i18n */ - size_Range(&mark)); + return iTrue; + default: + break; + } + return processEvent_Widget(w, ev); +} + +static void checkPendingInvalidation_DocumentWidget_(const iDocumentWidget *d) { + if (d->flags & invalidationPending_DocumentWidgetFlag && + !isAffectedByVisualOffset_Widget(constAs_Widget(d))) { + // printf("%p visoff: %d\n", d, left_Rect(bounds_Widget(w)) - left_Rect(boundsWithoutVisualOffset_Widget(w))); + iDocumentWidget *m = (iDocumentWidget *) d; /* Hrrm, not const... */ + m->flags &= ~invalidationPending_DocumentWidgetFlag; + invalidate_DocumentWidget_(m); + } +} + +static void prerender_DocumentWidget_(iAny *context) { + iAssert(isInstance_Object(context, &Class_DocumentWidget)); + if (current_Root() == NULL) { + /* The widget has probably been removed from the widget tree, pending destruction. + Tickers are not cancelled until the widget is actually destroyed. */ + return; + } + const iDocumentWidget *d = context; + iDrawContext ctx = { + .view = &d->view, + .docBounds = documentBounds_DocumentView_(&d->view), + .vis = visibleRange_DocumentView_(&d->view), + .showLinkNumbers = (d->flags & showLinkNumbers_DocumentWidgetFlag) != 0 + }; + // printf("%u prerendering\n", SDL_GetTicks()); + if (d->view.visBuf->buffers[0].texture) { + makePaletteGlobal_GmDocument(d->view.doc); + if (render_DocumentView_(&d->view, &ctx, iTrue /* just fill up progressively */)) { + /* Something was drawn, should check later if there is still more to do. */ + addTicker_App(prerender_DocumentWidget_, context); } } } @@ -5554,6 +5436,128 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) { /*----------------------------------------------------------------------------------------------*/ +void init_DocumentWidget(iDocumentWidget *d) { + iWidget *w = as_Widget(d); + init_Widget(w); + setId_Widget(w, format_CStr("document%03d", ++docEnum_)); + setFlags_Widget(w, hover_WidgetFlag | noBackground_WidgetFlag, iTrue); +#if defined (iPlatformAppleDesktop) + iBool enableSwipeNavigation = iTrue; /* swipes on the trackpad */ +#else + iBool enableSwipeNavigation = (deviceType_App() != desktop_AppDeviceType); +#endif + if (enableSwipeNavigation) { + setFlags_Widget(w, leftEdgeDraggable_WidgetFlag | rightEdgeDraggable_WidgetFlag | + horizontalOffset_WidgetFlag, iTrue); + } + init_PersistentDocumentState(&d->mod); + d->flags = 0; + d->phoneToolbar = findWidget_App("toolbar"); + d->footerButtons = NULL; + iZap(d->certExpiry); + d->certFingerprint = new_Block(0); + d->certFlags = 0; + d->certSubject = new_String(); + d->state = blank_RequestState; + d->titleUser = new_String(); + d->request = NULL; + d->isRequestUpdated = iFalse; + d->media = new_ObjectList(); + d->banner = new_Banner(); + setOwner_Banner(d->banner, d); + d->redirectCount = 0; + d->ordinalBase = 0; + d->wheelSwipeState = none_WheelSwipeState; + d->selectMark = iNullRange; + d->foundMark = iNullRange; + d->contextLink = NULL; + d->sourceStatus = none_GmStatusCode; + init_String(&d->sourceHeader); + init_String(&d->sourceMime); + init_Block(&d->sourceContent, 0); + iZap(d->sourceTime); + d->sourceGempub = NULL; + d->initNormScrollY = 0; + d->grabbedPlayer = NULL; + d->mediaTimer = 0; + init_String(&d->pendingGotoHeading); + init_String(&d->linePrecedingLink); + init_Click(&d->click, d, SDL_BUTTON_LEFT); + d->linkInfo = (deviceType_App() == desktop_AppDeviceType ? new_LinkInfo() : NULL); + init_DocumentView(&d->view); + setOwner_DocumentView_(&d->view, d); + addChild_Widget(w, iClob(d->scroll = new_ScrollWidget())); + d->menu = NULL; /* created when clicking */ + d->playerMenu = NULL; + d->copyMenu = NULL; + d->translation = NULL; + addChildFlags_Widget(w, + iClob(new_IndicatorWidget()), + resizeToParentWidth_WidgetFlag | resizeToParentHeight_WidgetFlag); +#if !defined (iPlatformAppleDesktop) /* in system menu */ + addAction_Widget(w, reload_KeyShortcut, "navigate.reload"); + addAction_Widget(w, closeTab_KeyShortcut, "tabs.close"); + addAction_Widget(w, SDLK_d, KMOD_PRIMARY, "bookmark.add"); + addAction_Widget(w, subscribeToPage_KeyModifier, "feeds.subscribe"); +#endif + addAction_Widget(w, navigateBack_KeyShortcut, "navigate.back"); + addAction_Widget(w, navigateForward_KeyShortcut, "navigate.forward"); + addAction_Widget(w, navigateParent_KeyShortcut, "navigate.parent"); + addAction_Widget(w, navigateRoot_KeyShortcut, "navigate.root"); +} + +void cancelAllRequests_DocumentWidget(iDocumentWidget *d) { + iForEach(ObjectList, i, d->media) { + iMediaRequest *mr = i.object; + cancel_GmRequest(mr->req); + } + if (d->request) { + cancel_GmRequest(d->request); + } +} + +void deinit_DocumentWidget(iDocumentWidget *d) { + // printf("\n* * * * * * * *\nDEINIT DOCUMENT: %s\n* * * * * * * *\n\n", + // cstr_String(&d->widget.id)); fflush(stdout); + cancelAllRequests_DocumentWidget(d); + pauseAllPlayers_Media(media_GmDocument(d->view.doc), iTrue); + removeTicker_App(animate_DocumentWidget_, d); + removeTicker_App(prerender_DocumentWidget_, d); + remove_Periodic(periodic_App(), d); + delete_Translation(d->translation); + deinit_DocumentView(&d->view); + delete_LinkInfo(d->linkInfo); + iRelease(d->media); + iRelease(d->request); + delete_Gempub(d->sourceGempub); + deinit_String(&d->linePrecedingLink); + deinit_String(&d->pendingGotoHeading); + deinit_Block(&d->sourceContent); + deinit_String(&d->sourceMime); + deinit_String(&d->sourceHeader); + delete_Banner(d->banner); + if (d->mediaTimer) { + SDL_RemoveTimer(d->mediaTimer); + } + delete_Block(d->certFingerprint); + delete_String(d->certSubject); + delete_String(d->titleUser); + deinit_PersistentDocumentState(&d->mod); +} + +void setSource_DocumentWidget(iDocumentWidget *d, const iString *source) { + setUrl_GmDocument(d->view.doc, d->mod.url); + const int docWidth = documentWidth_DocumentView_(&d->view); + setSource_GmDocument(d->view.doc, + source, + docWidth, + width_Widget(d), + isFinished_GmRequest(d->request) ? final_GmDocumentUpdate + : partial_GmDocumentUpdate); + setWidth_Banner(d->banner, docWidth); + documentWasChanged_DocumentWidget_(d); +} + iHistory *history_DocumentWidget(iDocumentWidget *d) { return d->mod.history; } -- cgit v1.2.3 From 1fc35259649d3f3206e0b28e51ca2e0ca7776d33 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Mon, 20 Dec 2021 06:33:53 +0200 Subject: Cleanup The "opened from sidebar" flag is unnecessary now. --- src/app.c | 3 +-- src/history.c | 12 ++++-------- src/history.h | 6 ++---- src/ui/documentwidget.c | 30 ++++++++---------------------- src/ui/documentwidget.h | 2 -- src/ui/sidebarwidget.c | 1 - 6 files changed, 15 insertions(+), 39 deletions(-) diff --git a/src/app.c b/src/app.c index 2590da0d..eea3689f 100644 --- a/src/app.c +++ b/src/app.c @@ -2780,8 +2780,7 @@ iBool handleCommand_App(const char *cmd) { urlEncodePath_String(url); } setUrlFlags_DocumentWidget(doc, url, - (isHistory ? useCachedContentIfAvailable_DocumentWidgetSetUrlFlag : 0) | - (fromSidebar ? openedFromSidebar_DocumentWidgetSetUrlFlag : 0)); + isHistory ? useCachedContentIfAvailable_DocumentWidgetSetUrlFlag : 0); /* Optionally, jump to a text in the document. This will only work if the document is already available, e.g., it's from "about:" or restored from cache. */ const iRangecc gotoHeading = range_Command(cmd, "gotoheading"); diff --git a/src/history.c b/src/history.c index 91416020..837b38cb 100644 --- a/src/history.c +++ b/src/history.c @@ -37,7 +37,7 @@ void init_RecentUrl(iRecentUrl *d) { d->normScrollY = 0; d->cachedResponse = NULL; d->cachedDoc = NULL; - d->flags.openedFromSidebar = iFalse; + d->flags = 0; } void deinit_RecentUrl(iRecentUrl *d) { @@ -181,7 +181,7 @@ void serialize_History(const iHistory *d, iStream *outs) { const iRecentUrl *item = i.value; serialize_String(&item->url, outs); write32_Stream(outs, item->normScrollY * 1.0e6f); - writeU16_Stream(outs, item->flags.openedFromSidebar ? iBit(1) : 0); + writeU16_Stream(outs, item->flags); if (item->cachedResponse) { write8_Stream(outs, 1); serialize_GmResponse(item->cachedResponse, outs); @@ -205,10 +205,7 @@ void deserialize_History(iHistory *d, iStream *ins) { set_String(&item.url, canonicalUrl_String(&item.url)); item.normScrollY = (float) read32_Stream(ins) / 1.0e6f; if (version_Stream(ins) >= addedRecentUrlFlags_FileVersion) { - uint16_t flags = readU16_Stream(ins); - if (flags & iBit(1)) { - item.flags.openedFromSidebar = iTrue; - } + item.flags = readU16_Stream(ins); } if (read8_Stream(ins)) { item.cachedResponse = new_GmResponse(); @@ -409,7 +406,7 @@ void setCachedResponse_History(iHistory *d, const iGmResponse *response) { unlock_Mutex(d->mtx); } -void setCachedDocument_History(iHistory *d, iGmDocument *doc, iBool openedFromSidebar) { +void setCachedDocument_History(iHistory *d, iGmDocument *doc) { lock_Mutex(d->mtx); iRecentUrl *item = mostRecentUrl_History(d); iAssert(size_GmDocument(doc).x > 0); @@ -421,7 +418,6 @@ void setCachedDocument_History(iHistory *d, iGmDocument *doc, iBool openedFromSi cstr_String(url_GmDocument(doc))); } #endif - item->flags.openedFromSidebar = openedFromSidebar; if (item->cachedDoc != doc) { iRelease(item->cachedDoc); item->cachedDoc = ref_Object(doc); diff --git a/src/history.h b/src/history.h index bfb88cf4..383c132b 100644 --- a/src/history.h +++ b/src/history.h @@ -39,9 +39,7 @@ struct Impl_RecentUrl { float normScrollY; /* normalized to document height */ iGmResponse *cachedResponse; /* kept in memory for quicker back navigation */ iGmDocument *cachedDoc; /* cached copy of the presentation: layout and media (not serialized) */ - struct { - uint8_t openedFromSidebar : 1; - } flags; + uint16_t flags; }; iDeclareType(MemInfo) @@ -65,7 +63,7 @@ void clear_History (iHistory *); void add_History (iHistory *, const iString *url); void replace_History (iHistory *, const iString *url); void setCachedResponse_History (iHistory *, const iGmResponse *response); -void setCachedDocument_History (iHistory *, iGmDocument *doc, iBool openedFromSidebar); +void setCachedDocument_History (iHistory *, iGmDocument *doc); iBool goBack_History (iHistory *); iBool goForward_History (iHistory *); iRecentUrl *precedingLocked_History (iHistory *); /* requires manual lock/unlock! */ diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 4af3dd72..3bd9f059 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -220,13 +220,12 @@ enum iDocumentWidgetFlag { movingSelectMarkEnd_DocumentWidgetFlag = iBit(11), otherRootByDefault_DocumentWidgetFlag = iBit(12), /* links open to other root by default */ urlChanged_DocumentWidgetFlag = iBit(13), - openedFromSidebar_DocumentWidgetFlag = iBit(14), - drawDownloadCounter_DocumentWidgetFlag = iBit(15), - fromCache_DocumentWidgetFlag = iBit(16), /* don't write anything to cache */ - animationPlaceholder_DocumentWidgetFlag = iBit(17), /* avoid slow operations */ - invalidationPending_DocumentWidgetFlag = iBit(18), /* invalidate as soon as convenient */ - leftWheelSwipe_DocumentWidgetFlag = iBit(19), /* swipe state flags are used on desktop */ - rightWheelSwipe_DocumentWidgetFlag = iBit(20), + drawDownloadCounter_DocumentWidgetFlag = iBit(14), + fromCache_DocumentWidgetFlag = iBit(15), /* don't write anything to cache */ + animationPlaceholder_DocumentWidgetFlag = iBit(16), /* avoid slow operations */ + invalidationPending_DocumentWidgetFlag = iBit(17), /* invalidate as soon as convenient */ + leftWheelSwipe_DocumentWidgetFlag = iBit(18), /* swipe state flags are used on desktop */ + rightWheelSwipe_DocumentWidgetFlag = iBit(19), eitherWheelSwipe_DocumentWidgetFlag = leftWheelSwipe_DocumentWidgetFlag | rightWheelSwipe_DocumentWidgetFlag, }; @@ -2144,9 +2143,7 @@ static void documentWasChanged_DocumentWidget_(iDocumentWidget *d) { } showOrHidePinningIndicator_DocumentWidget_(d); if (~d->flags & fromCache_DocumentWidgetFlag) { - setCachedDocument_History(d->mod.history, - d->view.doc, /* keeps a ref */ - (d->flags & openedFromSidebar_DocumentWidgetFlag) != 0); + setCachedDocument_History(d->mod.history, d->view.doc /* keeps a ref */); } } @@ -2905,14 +2902,11 @@ static void updateFromCachedResponse_DocumentWidget_(iDocumentWidget *d, float n static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) { const iRecentUrl *recent = constMostRecentUrl_History(d->mod.history); if (recent && recent->cachedResponse && equalCase_String(&recent->url, d->mod.url)) { - iChangeFlags(d->flags, - openedFromSidebar_DocumentWidgetFlag, - recent->flags.openedFromSidebar); updateFromCachedResponse_DocumentWidget_( d, recent->normScrollY, recent->cachedResponse, recent->cachedDoc); if (!recent->cachedDoc) { /* We have a cached copy now. */ - setCachedDocument_History(d->mod.history, d->view.doc, iFalse); + setCachedDocument_History(d->mod.history, d->view.doc); } return iTrue; } @@ -5617,8 +5611,6 @@ void deserializeState_DocumentWidget(iDocumentWidget *d, iStream *ins) { } void setUrlFlags_DocumentWidget(iDocumentWidget *d, const iString *url, int setUrlFlags) { - iChangeFlags(d->flags, openedFromSidebar_DocumentWidgetFlag, - (setUrlFlags & openedFromSidebar_DocumentWidgetSetUrlFlag) != 0); const iBool allowCache = (setUrlFlags & useCachedContentIfAvailable_DocumentWidgetSetUrlFlag) != 0; setLinkNumberMode_DocumentWidget_(d, iFalse); setUrl_DocumentWidget_(d, urlFragmentStripped_String(url)); @@ -5631,7 +5623,6 @@ void setUrlFlags_DocumentWidget(iDocumentWidget *d, const iString *url, int setU void setUrlAndSource_DocumentWidget(iDocumentWidget *d, const iString *url, const iString *mime, const iBlock *source) { - d->flags &= ~openedFromSidebar_DocumentWidgetFlag; setLinkNumberMode_DocumentWidget_(d, iFalse); setUrl_DocumentWidget_(d, url); parseUser_DocumentWidget_(d); @@ -5673,11 +5664,6 @@ void setRedirectCount_DocumentWidget(iDocumentWidget *d, int count) { d->redirectCount = count; } -void setOpenedFromSidebar_DocumentWidget(iDocumentWidget *d, iBool fromSidebar) { - iChangeFlags(d->flags, openedFromSidebar_DocumentWidgetFlag, fromSidebar); -// setCachedDocument_History(d->mod.history, d->doc, fromSidebar); -} - iBool isRequestOngoing_DocumentWidget(const iDocumentWidget *d) { return d->request != NULL; } diff --git a/src/ui/documentwidget.h b/src/ui/documentwidget.h index 1405f19d..1bee8351 100644 --- a/src/ui/documentwidget.h +++ b/src/ui/documentwidget.h @@ -50,7 +50,6 @@ int documentWidth_DocumentWidget (const iDocumentWidget *); enum iDocumentWidgetSetUrlFlags { useCachedContentIfAvailable_DocumentWidgetSetUrlFlag = iBit(1), - openedFromSidebar_DocumentWidgetSetUrlFlag = iBit(2), }; void setOrigin_DocumentWidget (iDocumentWidget *, const iDocumentWidget *other); @@ -60,7 +59,6 @@ void setUrlAndSource_DocumentWidget (iDocumentWidget *, const iString *url, void setInitialScroll_DocumentWidget (iDocumentWidget *, float normScrollY); /* set after content received */ void setRedirectCount_DocumentWidget (iDocumentWidget *, int count); void setSource_DocumentWidget (iDocumentWidget *, const iString *sourceText); -void setOpenedFromSidebar_DocumentWidget(iDocumentWidget *, iBool fromSidebar); void takeRequest_DocumentWidget (iDocumentWidget *, iGmRequest *finishedRequest); /* ownership given */ diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index 4b4968a3..fe8ec939 100644 --- a/src/ui/sidebarwidget.c +++ b/src/ui/sidebarwidget.c @@ -965,7 +965,6 @@ static void itemClicked_SidebarWidget_(iSidebarWidget *d, iSidebarItem *item, si const iGmHeading *head = constAt_Array(headings_GmDocument(doc), item->id); postCommandf_App("document.goto loc:%p", head->text.start); dismissPortraitPhoneSidebars_Root(as_Widget(d)->root); - setOpenedFromSidebar_DocumentWidget(document_App(), iTrue); } break; } -- cgit v1.2.3 From c4df3339fb9d554075a044425b17fa34fe0c43c1 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Mon, 20 Dec 2021 06:35:23 +0200 Subject: LinkInfo: More concise presentation Bold hostname, including scheme if not `gemini`. Info is now on a single wrapped line. --- po/en.po | 6 ++-- res/lang/en.bin | Bin 26614 -> 26603 bytes res/lang/eo.bin | Bin 25568 -> 25567 bytes res/lang/isv.bin | Bin 25334 -> 25324 bytes res/lang/sk.bin | Bin 25670 -> 25660 bytes src/ui/linkinfo.c | 104 ++++++++++++++++++++++++++---------------------------- 6 files changed, 54 insertions(+), 56 deletions(-) diff --git a/po/en.po b/po/en.po index 481545cf..04b6ac19 100644 --- a/po/en.po +++ b/po/en.po @@ -3,7 +3,7 @@ msgstr "Preformatted text without a caption" # Link download progress message. msgid "doc.fetching" -msgstr "Fetching" +msgstr "Loading" #, c-format msgid "doc.archive" @@ -933,10 +933,10 @@ msgid "dlg.certimport.nokey" msgstr "No Private Key" msgid "link.hint.audio" -msgstr "Play Audio" +msgstr "Audio" msgid "link.hint.image" -msgstr "View Image" +msgstr "Image" msgid "bookmark.title.blank" msgstr "Blank Page" diff --git a/res/lang/en.bin b/res/lang/en.bin index 28c11997..942dfdc2 100644 Binary files a/res/lang/en.bin and b/res/lang/en.bin differ diff --git a/res/lang/eo.bin b/res/lang/eo.bin index d148e2f0..513c3a0b 100644 Binary files a/res/lang/eo.bin and b/res/lang/eo.bin differ diff --git a/res/lang/isv.bin b/res/lang/isv.bin index 56ed525c..26c23dc5 100644 Binary files a/res/lang/isv.bin and b/res/lang/isv.bin differ diff --git a/res/lang/sk.bin b/res/lang/sk.bin index bf13f594..4d91e8a6 100644 Binary files a/res/lang/sk.bin and b/res/lang/sk.bin differ diff --git a/src/ui/linkinfo.c b/src/ui/linkinfo.c index cb1404fc..fb608d41 100644 --- a/src/ui/linkinfo.c +++ b/src/ui/linkinfo.c @@ -51,10 +51,11 @@ iInt2 size_LinkInfo(const iLinkInfo *d) { return add_I2(d->buf->size, init_I2(2 * hPad_LinkInfo_, 2 * vPad_LinkInfo_)); } -iBool update_LinkInfo(iLinkInfo *d, const iGmDocument *doc, iGmLinkId linkId, int maxWidth) { +iBool update_LinkInfo(iLinkInfo *d, const iGmDocument *doc, iGmLinkId linkId, int maxWidth) { if (!d) { return iFalse; } + const iBool isAnimated = prefs_App()->uiAnimations; if (d->linkId != linkId || d->maxWidth != maxWidth) { d->linkId = linkId; d->maxWidth = maxWidth; @@ -62,79 +63,81 @@ iBool update_LinkInfo(iLinkInfo *d, const iGmDocument *doc, iGmLinkId linkId, in if (linkId) { /* Measure and draw. */ if (targetValue_Anim(&d->opacity) < 1) { - setValue_Anim(&d->opacity, 1, 75); + setValue_Anim(&d->opacity, 1, isAnimated ? 75 : 0); } const int avail = iMax(minWidth_LinkInfo_, maxWidth) - 2 * hPad_LinkInfo_; const iString *url = linkUrl_GmDocument(doc, linkId); iUrl parts; init_Url(&parts, url); - const int flags = linkFlags_GmDocument(doc, linkId); - const enum iGmLinkScheme scheme = scheme_GmLinkFlag(flags); - const iBool showHost = (flags & humanReadable_GmLinkFlag && - (!isEmpty_Range(&parts.host) || - scheme == mailto_GmLinkScheme)); - const iBool showImage = (flags & imageFileExtension_GmLinkFlag) != 0; - const iBool showAudio = (flags & audioFileExtension_GmLinkFlag) != 0; -// int fg = linkColor_GmDocument(doc, linkId, textHover_GmLinkPart); + const int flags = linkFlags_GmDocument(doc, linkId); + const enum iGmLinkScheme scheme = scheme_GmLinkFlag(flags); + const iBool isImage = (flags & imageFileExtension_GmLinkFlag) != 0; + const iBool isAudio = (flags & audioFileExtension_GmLinkFlag) != 0; + // int fg = linkColor_GmDocument(doc, linkId, + // textHover_GmLinkPart); iString str; init_String(&str); - if ((showHost || - (flags & (imageFileExtension_GmLinkFlag | audioFileExtension_GmLinkFlag))) && - scheme != mailto_GmLinkScheme) { + /* Most important info first: the identity that will be used. */ + const iGmIdentity *ident = identityForUrl_GmCerts(certs_App(), url); + if (ident) { + appendFormat_String(&str, person_Icon " %s", + //escape_Color(tmBannerItemTitle_ColorId), + cstr_String(name_GmIdentity(ident))); + } + /* Possibly inlined content. */ + if (isImage || isAudio) { if (!isEmpty_String(&str)) { appendCStr_String(&str, "\n"); } - if (showHost && scheme != gemini_GmLinkScheme) { - append_String( - &str, collect_String(upper_String(collectNewRange_String(parts.scheme)))); - appendCStr_String(&str, " \u2014 "); - } - if (showHost) { - appendFormat_String(&str, "\x1b[1m%s\x1b[0m", cstr_Rangecc(parts.host)); - } - if (showImage || showAudio) { - appendFormat_String( - &str, - "%s%s", - showHost ? " \u2014" : "", - format_CStr(showImage ? photo_Icon " %s " : "\U0001f3b5 %s", - cstr_Lang(showImage ? "link.hint.image" : "link.hint.audio"))); - } + appendCStr_String( + &str, + format_CStr(isImage ? photo_Icon " %s " : "\U0001f3b5 %s", + cstr_Lang(isImage ? "link.hint.image" : "link.hint.audio"))); + } + if (!isEmpty_String(&str)) { + appendCStr_String(&str, " \u2014 "); + } + /* Indicate non-Gemini schemes. */ + if (scheme == mailto_GmLinkScheme) { + appendCStr_String(&str, envelope_Icon " "); + append_String(&str, url); + } + else if (scheme != gemini_GmLinkScheme && !isEmpty_Range(&parts.host)) { + appendCStr_String(&str, globe_Icon " \x1b[1m"); + appendRange_String(&str, (iRangecc){ constBegin_String(url), + parts.host.end }); + appendCStr_String(&str, "\x1b[0m"); + appendRange_String(&str, (iRangecc){ parts.path.start, constEnd_String(url) }); } + else if (scheme != gemini_GmLinkScheme) { + appendCStr_String(&str, globe_Icon " "); + append_String(&str, url); + } + else { + appendCStr_String(&str, "\x1b[1m"); + appendRange_String(&str, parts.host); + appendCStr_String(&str, "\x1b[0m"); + appendRange_String(&str, (iRangecc){ parts.path.start, constEnd_String(url) }); + } + /* Date of last visit. */ if (flags & visited_GmLinkFlag) { iDate date; init_Date(&date, linkTime_GmDocument(doc, linkId)); if (!isEmpty_String(&str)) { appendCStr_String(&str, " \u2014 "); } -// appendCStr_String(&str, escape_Color(tmQuoteIcon_ColorId)); iString *dateStr = format_Date(&date, "%b %d"); append_String(&str, dateStr); delete_String(dateStr); } - /* Identity that will be used. */ - const iGmIdentity *ident = identityForUrl_GmCerts(certs_App(), url); - if (ident) { - if (!isEmpty_String(&str)) { - appendCStr_String(&str, " \u2014 "); - } - appendFormat_String(&str, person_Icon " %s", - //escape_Color(tmBannerItemTitle_ColorId), - cstr_String(name_GmIdentity(ident))); - } - /* Show scheme and host. */ - if (!isEmpty_String(&str)) { - appendCStr_String(&str, "\n"); - } - appendRange_String(&str, range_String(url)); - /* Draw the text. */ + /* Draw to a buffer, wrapped. */ iWrapText wt = { .text = range_String(&str), .maxWidth = avail, .mode = word_WrapTextMode }; d->buf = new_TextBuf(&wt, uiLabel_FontId, tmQuote_ColorId); deinit_String(&str); } else { if (targetValue_Anim(&d->opacity) > 0) { - setValue_Anim(&d->opacity, 0, 150); + setValue_Anim(&d->opacity, 0, isAnimated ? 150 : 0); } } return iTrue; @@ -144,13 +147,8 @@ iBool update_LinkInfo(iLinkInfo *d, const iGmDocument *doc, iGmLinkId linkId, in void invalidate_LinkInfo(iLinkInfo *d) { if (targetValue_Anim(&d->opacity) > 0) { - setValue_Anim(&d->opacity, 0, 150); + setValue_Anim(&d->opacity, 0, prefs_App()->uiAnimations ? 150 : 0); } - - // if (d->buf) { -// delete_TextBuf(d->buf); -// d->buf = NULL; -// } } void draw_LinkInfo(const iLinkInfo *d, iInt2 topLeft) { -- cgit v1.2.3 From aa6afd93fae919d8717d8108eb9b5c37aa3e88be Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Mon, 20 Dec 2021 22:08:07 +0200 Subject: DocumentWidget: Cleaning up after refactoring --- src/ui/documentwidget.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 3bd9f059..e93fb586 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -997,7 +997,7 @@ static iBool scrollWideBlock_DocumentView_(iDocumentView *d, iInt2 mousePos, int for (const iGmRun *r = range.start; r != range.end; r++) { insert_PtrSet(d->invalidRuns, r); } - refresh_Widget(d); + refresh_Widget(d->owner); d->owner->selectMark = iNullRange; d->owner->foundMark = iNullRange; } @@ -1008,7 +1008,7 @@ static iBool scrollWideBlock_DocumentView_(iDocumentView *d, iInt2 mousePos, int } setValueEased_Anim(&d->animWideRunOffset, *offset, duration); d->animWideRunRange = range; - addTicker_App(refreshWhileScrolling_DocumentWidget_, d); + addTicker_App(refreshWhileScrolling_DocumentWidget_, d->owner); } else { d->animWideRunId = 0; -- cgit v1.2.3 From 3db345897d13df3657e2c6fd90f3bfdeb81df4a0 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Mon, 20 Dec 2021 22:08:13 +0200 Subject: Banner: Margins --- src/ui/banner.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui/banner.c b/src/ui/banner.c index 218b0981..2b0b852e 100644 --- a/src/ui/banner.c +++ b/src/ui/banner.c @@ -64,8 +64,8 @@ struct Impl_Banner { iDefineTypeConstruction(Banner) #define itemGap_Banner_ (3 * gap_UI) -#define itemVPad_Banner_ (3 * gap_UI) -#define itemHPad_Banner_ (4 * gap_UI) +#define itemVPad_Banner_ (2 * gap_UI) +#define itemHPad_Banner_ (3 * gap_UI) #define bottomPad_Banner_ (4 * gap_UI) static void updateHeight_Banner_(iBanner *d) { -- cgit v1.2.3 From d2aff7411994d66e216b8e6ee55a29e38e715e19 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 23 Dec 2021 13:07:52 +0200 Subject: LinkInfo: Show port, too --- src/ui/linkinfo.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ui/linkinfo.c b/src/ui/linkinfo.c index fb608d41..89c7fbf3 100644 --- a/src/ui/linkinfo.c +++ b/src/ui/linkinfo.c @@ -116,6 +116,10 @@ iBool update_LinkInfo(iLinkInfo *d, const iGmDocument *doc, iGmLinkId linkId, in else { appendCStr_String(&str, "\x1b[1m"); appendRange_String(&str, parts.host); + if (!isEmpty_Range(&parts.port)) { + appendCStr_String(&str, ":"); + appendRange_String(&str, parts.port); + } appendCStr_String(&str, "\x1b[0m"); appendRange_String(&str, (iRangecc){ parts.path.start, constEnd_String(url) }); } -- cgit v1.2.3 From 5d94fa0053adf3281dfc5ccde123e3f322d346a2 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 11 Dec 2021 19:16:43 +0200 Subject: Android: Setting up the build --- CMakeLists.txt | 13 +++++++++---- Depends-Android.cmake | 31 +++++++++++++++++++++++++++++++ Depends.cmake | 12 +++++++++--- src/main.c | 4 +++- 4 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 Depends-Android.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index ba63bf78..0d094ea7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -217,7 +217,7 @@ set (SOURCES src/ui/scrollwidget.c src/ui/scrollwidget.h src/ui/sidebarwidget.c - src/ui/sidebarwidget.h + src/ui/sidebarwidget.h src/ui/text.c src/ui/text.h src/ui/touch.c @@ -307,7 +307,12 @@ else () endif () # Target. -add_executable (app ${SOURCES} ${RESOURCES} ${EMB_FONTS}) +if (NOT ANDROID) + add_executable (app ${SOURCES} ${RESOURCES} ${EMB_FONTS}) +else () + # The whole app becomes one shared library, based on this static one. + add_library (app STATIC ${SOURCES}) +endif () set_property (TARGET app PROPERTY C_STANDARD 11) if (TARGET ext-deps) add_dependencies (app ext-deps) @@ -414,7 +419,7 @@ if (APPLE) XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "fi.skyjake.lagrange" ) if (IOS) - target_compile_definitions (app PUBLIC + target_compile_definitions (app PUBLIC LAGRANGE_IOS_VERSION="${IOS_BUNDLE_VERSION}" LAGRANGE_IOS_BUILD_DATE="${IOS_BUILD_DATE}" ) @@ -437,7 +442,7 @@ endif () if (MSYS) target_link_libraries (app PUBLIC d2d1 uuid dwmapi) # querying DPI if (ENABLE_WINSPARKLE) - target_link_libraries (app PUBLIC winsparkle) + target_link_libraries (app PUBLIC winsparkle) endif () endif () if (UNIX) diff --git a/Depends-Android.cmake b/Depends-Android.cmake new file mode 100644 index 00000000..74635620 --- /dev/null +++ b/Depends-Android.cmake @@ -0,0 +1,31 @@ +if (NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/lib/the_Foundation/.git) + message (FATAL_ERROR "'lib/the_Foundation' Git submodule not found") +endif () + +# the_Foundation is checked out as a submodule, make sure it's up to date. +find_package (Git) +if (GIT_FOUND) + execute_process ( + COMMAND ${GIT_EXECUTABLE} submodule update + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + OUTPUT_VARIABLE subout + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + if (subout) + message (FATAL_ERROR "'lib/the_Foundation' Git submodule has been updated, please re-run CMake.\n") + endif () +endif () + +set (INSTALL_THE_FOUNDATION OFF) +set (TFDN_STATIC_LIBRARY ON CACHE BOOL "") +set (TFDN_ENABLE_INSTALL OFF CACHE BOOL "") +set (TFDN_ENABLE_TESTS OFF CACHE BOOL "") +set (TFDN_ENABLE_WEBREQUEST OFF CACHE BOOL "") +add_subdirectory (lib/the_Foundation) +add_library (the_Foundation::the_Foundation ALIAS the_Foundation) +if (NOT OPENSSL_FOUND) + message (FATAL_ERROR "Lagrange requires OpenSSL for TLS. Please check if pkg-config can find 'openssl'.") +endif () +if (NOT ZLIB_FOUND) + message (FATAL_ERROR "Lagrange requires zlib for reading compressed archives. Please check if pkg-config can find 'zlib'.") +endif () \ No newline at end of file diff --git a/Depends.cmake b/Depends.cmake index 10e434e7..9e69e69d 100644 --- a/Depends.cmake +++ b/Depends.cmake @@ -5,6 +5,12 @@ if (IOS) return () endif () +if (ANDROID) + include (Depends-Android.cmake) + return () +endif () + +find_package (PkgConfig) find_program (MESON_EXECUTABLE meson DOC "Meson build system") find_program (NINJA_EXECUTABLE ninja DOC "Ninja build tool") include (ExternalProject) @@ -45,7 +51,7 @@ else () endif () if (APPLE AND CMAKE_OSX_DEPLOYMENT_TARGET) - set (_dependMacOpts + set (_dependMacOpts -Dc_args=-mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET} -Dc_link_args=-mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET} -Dcpp_args=-mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET} @@ -58,8 +64,8 @@ if (ENABLE_HARFBUZZ) if (NOT ENABLE_HARFBUZZ_MINIMAL AND PKG_CONFIG_FOUND) pkg_check_modules (HARFBUZZ IMPORTED_TARGET harfbuzz) endif () - if (EXISTS ${CMAKE_SOURCE_DIR}/lib/harfbuzz/CMakeLists.txt AND - (ENABLE_HARFBUZZ_MINIMAL OR NOT HARFBUZZ_FOUND)) + if (EXISTS ${CMAKE_SOURCE_DIR}/lib/harfbuzz/CMakeLists.txt AND + (ENABLE_HARFBUZZ_MINIMAL OR NOT HARFBUZZ_FOUND)) # Build HarfBuzz with minimal dependencies. if (MESON_EXECUTABLE AND NINJA_EXECUTABLE) set (_dst ${CMAKE_BINARY_DIR}/lib/harfbuzz) diff --git a/src/main.c b/src/main.c index cb5e060b..f765f82f 100644 --- a/src/main.c +++ b/src/main.c @@ -67,9 +67,11 @@ int main(int argc, char **argv) { "ECDHE-RSA-AES128-GCM-SHA256:" "DHE-RSA-AES256-GCM-SHA384"); SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1"); + SDL_SetHint(SDL_HINT_MAC_BACKGROUND_APP, "1"); SDL_SetHint(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, "1"); +#if SDL_VERSION_ATLEAST(2, 0, 8) SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0"); - SDL_SetHint(SDL_HINT_MAC_BACKGROUND_APP, "1"); +#endif #if 0 SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "1"); /* debugging! */ #endif -- cgit v1.2.3 From 74de1ab589b80b7719e97341ec98869ecf3da8aa Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Mon, 13 Dec 2021 10:57:32 +0200 Subject: Android: Updated CMake project to work as a subdirectory Android builds produce a static library instead of a regular executable. --- CMakeLists.txt | 4 +++- Resources.cmake | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0d094ea7..4f414496 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -93,7 +93,7 @@ set (RESOURCES res/shadow.png res/fontpack.ini ) -file (GLOB FONTS RELATIVE ${CMAKE_SOURCE_DIR} res/fonts/*) +file (GLOB FONTS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} res/fonts/*) list (APPEND RESOURCES ${FONTS}) if (IOS) list (APPEND RESOURCES @@ -472,6 +472,8 @@ elseif (HAIKU) target_compile_definitions (app PUBLIC LAGRANGE_EMB_BIN="${CMAKE_INSTALL_PREFIX}/resources.lgr") install (FILES ${EMB_BIN} DESTINATION .) +elseif (ANDROID) + file (COPY ${EMB_BIN} DESTINATION ${CMAKE_SOURCE_DIR}/../app/src/main/assets) elseif (UNIX AND NOT APPLE) include (GNUInstallDirs) set_target_properties (app PROPERTIES diff --git a/Resources.cmake b/Resources.cmake index fc5d20c6..15ef15ea 100644 --- a/Resources.cmake +++ b/Resources.cmake @@ -16,11 +16,11 @@ function (make_resources dst) file (REMOVE ${dst}) get_filename_component (dstName ${dst} NAME) message (STATUS " ${dstName}") - set (versionTempPath ${CMAKE_SOURCE_DIR}/res/VERSION) + set (versionTempPath ${CMAKE_CURRENT_SOURCE_DIR}/res/VERSION) file (WRITE ${versionTempPath} ${PROJECT_VERSION}) execute_process ( COMMAND ${ZIP_EXECUTABLE} -1 ${dst} VERSION ${files} - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/res + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/res OUTPUT_QUIET ) file (REMOVE ${versionTempPath}) -- cgit v1.2.3 From e39721581f082a20f1ef3a6f81b83c5e489cf7c7 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Mon, 13 Dec 2021 10:58:59 +0200 Subject: Android: Various fixes to get things up and running Resource paths, runtime data, ignore mouse events. Assume that the Java side tells us the display pixel density via a command line argument. --- src/app.c | 39 ++++++++++++++++++++++++---- src/resources.c | 20 +++++++++++++-- src/ui/documentwidget.c | 68 ++++++++++++++++++++++++------------------------- src/ui/touch.c | 4 +++ src/ui/window.c | 10 +++++++- 5 files changed, 99 insertions(+), 42 deletions(-) diff --git a/src/app.c b/src/app.c index eea3689f..3ee2345c 100644 --- a/src/app.c +++ b/src/app.c @@ -92,7 +92,10 @@ static const char *defaultDataDir_App_ = "~/Library/Application Support"; #define EMB_BIN "../resources.lgr" static const char *defaultDataDir_App_ = "~/AppData/Roaming/fi.skyjake.Lagrange"; #endif -#if defined (iPlatformLinux) || defined (iPlatformOther) +#if defined (iPlatformAndroidMobile) +#define EMB_BIN "resources.lgr" /* loaded from assets with SDL_rwops */ +static const char *defaultDataDir_App_ = NULL; /* will ask SDL */ +#elif defined (iPlatformLinux) || defined (iPlatformOther) #define EMB_BIN "../../share/lagrange/resources.lgr" #define EMB_BIN2 "../../../share/lagrange/resources.lgr" static const char *defaultDataDir_App_ = "~/.config/lagrange"; @@ -139,6 +142,9 @@ struct Impl_App { int autoReloadTimer; iPeriodic periodic; int warmupFrames; /* forced refresh just after resuming from background; FIXME: shouldn't be needed */ +#if defined (iPlatformAndroidMobile) + float displayDensity; +#endif /* Preferences: */ iBool commandEcho; /* --echo */ iBool forceSoftwareRender; /* --sw */ @@ -307,7 +313,10 @@ static const char *dataDir_App_(void) { return userDir; } #endif - return defaultDataDir_App_; + if (defaultDataDir_App_) { + return defaultDataDir_App_; + } + return SDL_GetPrefPath("Jaakko Keränen", "fi.skyjake.lagrange"); } static const char *downloadDir_App_(void) { @@ -715,7 +724,7 @@ static iBool hasCommandLineOpenableScheme_(const iRangecc uri) { } static void init_App_(iApp *d, int argc, char **argv) { -#if defined (iPlatformLinux) +#if defined (iPlatformLinux) && !defined (iPlatformAndroid) d->isRunningUnderWindowSystem = !iCmpStr(SDL_GetCurrentVideoDriver(), "x11") || !iCmpStr(SDL_GetCurrentVideoDriver(), "wayland"); #else @@ -763,6 +772,8 @@ static void init_App_(iApp *d, int argc, char **argv) { } } init_Lang(); + iStringList *openCmds = new_StringList(); +#if !defined (iPlatformAndroidMobile) /* Configure the valid command line options. */ { defineValues_CommandLine(&d->args, "close-tab", 0); defineValues_CommandLine(&d->args, "echo;E", 0); @@ -777,7 +788,6 @@ static void init_App_(iApp *d, int argc, char **argv) { defineValues_CommandLine(&d->args, "sw", 0); defineValues_CommandLine(&d->args, "version;V", 0); } - iStringList *openCmds = new_StringList(); /* Handle command line options. */ { if (contains_CommandLine(&d->args, "help")) { puts(cstr_Block(&blobArghelp_Resources)); @@ -826,6 +836,7 @@ static void init_App_(iApp *d, int argc, char **argv) { } } } +#endif #if defined (LAGRANGE_ENABLE_IPC) /* Only one instance is allowed to run at a time; the runtime files (bookmarks, etc.) are not shareable. */ { @@ -860,7 +871,7 @@ static void init_App_(iApp *d, int argc, char **argv) { /* Must scale by UI scaling factor. */ mulfv_I2(&d->initialWindowRect.size, desktopDPI_Win32()); #endif -#if defined (iPlatformLinux) +#if defined (iPlatformLinux) && !defined (iPlatformAndroid) /* Scale by the primary (?) monitor DPI. */ if (isRunningUnderWindowSystem_App()) { float vdpi; @@ -1325,6 +1336,15 @@ void processEvents_App(enum iAppEventMode eventMode) { } ev.key.keysym.mod = mapMods_Keys(ev.key.keysym.mod & ~KMOD_CAPS); } +#if defined (iPlatformAndroidMobile) + /* Ignore all mouse events; just use touch. */ + if (ev.type == SDL_MOUSEBUTTONDOWN || + ev.type == SDL_MOUSEBUTTONUP || + ev.type == SDL_MOUSEMOTION || + ev.type == SDL_MOUSEWHEEL) { + continue; + } +#endif /* Scroll events may be per-pixel or mouse wheel steps. */ if (ev.type == SDL_MOUSEWHEEL) { #if defined (iPlatformMsys) @@ -1773,6 +1793,8 @@ enum iAppDeviceType deviceType_App(void) { return tablet_AppDeviceType; #elif defined (iPlatformAppleMobile) return isPhone_iOS() ? phone_AppDeviceType : tablet_AppDeviceType; +#elif defined (iPlatformAndroidMobile) + return phone_AppDeviceType; /* TODO: Java side could tell us via cmdline if this is a tablet. */ #else return desktop_AppDeviceType; #endif @@ -3408,3 +3430,10 @@ void closePopups_App(void) { } } } + +#if defined (iPlatformAndroidMobile) +float displayDensity_Android(void) { + iApp *d = &app_; + return toFloat_String(at_CommandLine(&d->args, 1)); +} +#endif diff --git a/src/resources.c b/src/resources.c index bb601cca..e3d92946 100644 --- a/src/resources.c +++ b/src/resources.c @@ -26,7 +26,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include static iArchive *archive_; - + iBlock blobAbout_Resources; iBlock blobHelp_Resources; iBlock blobLagrange_Resources; @@ -101,7 +101,23 @@ static struct { iBool init_Resources(const char *path) { archive_ = new_Archive(); - if (openFile_Archive(archive_, collectNewCStr_String(path))) { + iBool ok = iFalse; +#if defined (iPlatformAndroidMobile) + /* Resources are bundled as assets so they cannot be loaded as a regular file. + Fortunately, SDL implements a file wrapper. */ + SDL_RWops *io = SDL_RWFromFile(path, "rb"); + if (io) { + iBlock buf; + init_Block(&buf, (size_t) SDL_RWsize(io)); + SDL_RWread(io, data_Block(&buf), size_Block(&buf), 1); + SDL_RWclose(io); + ok = openData_Archive(archive_, &buf); + deinit_Block(&buf); + } +#else + ok = openFile_Archive(archive_, collectNewCStr_String(path)); +#endif + if (ok) { iVersion appVer; init_Version(&appVer, range_CStr(LAGRANGE_APP_VERSION)); iVersion resVer; diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index e93fb586..b20ae672 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -225,7 +225,7 @@ enum iDocumentWidgetFlag { animationPlaceholder_DocumentWidgetFlag = iBit(16), /* avoid slow operations */ invalidationPending_DocumentWidgetFlag = iBit(17), /* invalidate as soon as convenient */ leftWheelSwipe_DocumentWidgetFlag = iBit(18), /* swipe state flags are used on desktop */ - rightWheelSwipe_DocumentWidgetFlag = iBit(19), + rightWheelSwipe_DocumentWidgetFlag = iBit(19), eitherWheelSwipe_DocumentWidgetFlag = leftWheelSwipe_DocumentWidgetFlag | rightWheelSwipe_DocumentWidgetFlag, }; @@ -242,8 +242,8 @@ enum iWheelSwipeState { /* TODO: DocumentView is supposed to be useful on its own; move to a separate source file. */ iDeclareType(DocumentView) - -struct Impl_DocumentView { + +struct Impl_DocumentView { iDocumentWidget *owner; /* TODO: Convert to an abstract provider of metrics? */ iGmDocument * doc; int pageMargin; @@ -253,7 +253,7 @@ struct Impl_DocumentView { iGmRunRange visibleRuns; iPtrArray visibleLinks; iPtrArray visiblePre; - iPtrArray visibleMedia; /* currently playing audio / ongoing downloads */ + iPtrArray visibleMedia; /* currently playing audio / ongoing downloads */ iPtrArray visibleWideRuns; /* scrollable blocks; TODO: merge into `visiblePre` */ const iGmRun * hoverPre; /* for clicking */ const iGmRun * hoverAltPre; /* for drawing alt text */ @@ -263,7 +263,7 @@ struct Impl_DocumentView { uint16_t animWideRunId; iGmRunRange animWideRunRange; iDrawBufs * drawBufs; /* dynamic state for drawing */ - iVisBuf * visBuf; + iVisBuf * visBuf; iVisBufMeta * visBufMeta; iGmRunRange renderRuns; iPtrSet * invalidRuns; @@ -272,7 +272,7 @@ struct Impl_DocumentView { struct Impl_DocumentWidget { iWidget widget; int flags; /* internal behavior, see enum iDocumentWidgetFlag */ - + /* User interface: */ enum iDocumentLinkOrdinalMode ordinalMode; size_t ordinalBase; @@ -293,7 +293,7 @@ struct Impl_DocumentWidget { enum iWheelSwipeState wheelSwipeState; iString pendingGotoHeading; iString linePrecedingLink; - + /* Network request: */ enum iRequestState state; iGmRequest * request; @@ -304,7 +304,7 @@ struct Impl_DocumentWidget { iString * certSubject; int redirectCount; iObjectList * media; /* inline media requests */ - + /* Document: */ iPersistentDocumentState mod; iString * titleUser; @@ -316,12 +316,12 @@ struct Impl_DocumentWidget { iGempub * sourceGempub; /* NULL unless the page is Gempub content */ iBanner * banner; float initNormScrollY; - + /* Rendering: */ iDocumentView view; iLinkInfo * linkInfo; - - /* Widget structure: */ + + /* Widget structure: */ iScrollWidget *scroll; iWidget * footerButtons; iWidget * menu; @@ -332,7 +332,7 @@ struct Impl_DocumentWidget { }; iDefineObjectConstruction(DocumentWidget) - + /* Sorted by proximity to F and J. */ static const int homeRowKeys_[] = { 'f', 'd', 's', 'a', @@ -344,7 +344,7 @@ static const int homeRowKeys_[] = { 'g', 'h', 'b', 't', 'y', -}; +}; static int docEnum_ = 0; static void animate_DocumentWidget_ (void *ticker); @@ -909,7 +909,7 @@ static void updateTimestampBuf_DocumentView_(const iDocumentView *d) { static void invalidate_DocumentView_(iDocumentView *d) { invalidate_VisBuf(d->visBuf); - clear_PtrSet(d->invalidRuns); + clear_PtrSet(d->invalidRuns); } static void documentRunsInvalidated_DocumentView_(iDocumentView *d) { @@ -928,11 +928,11 @@ static void resetScroll_DocumentView_(iDocumentView *d) { } static void updateWidth_DocumentView_(iDocumentView *d) { - updateWidth_GmDocument(d->doc, documentWidth_DocumentView_(d), width_Widget(d->owner)); + updateWidth_GmDocument(d->doc, documentWidth_DocumentView_(d), width_Widget(d->owner)); } static void updateWidthAndRedoLayout_DocumentView_(iDocumentView *d) { - setWidth_GmDocument(d->doc, documentWidth_DocumentView_(d), width_Widget(d->owner)); + setWidth_GmDocument(d->doc, documentWidth_DocumentView_(d), width_Widget(d->owner)); } static void clampScroll_DocumentView_(iDocumentView *d) { @@ -1025,7 +1025,7 @@ static iRangecc sourceLoc_DocumentView_(const iDocumentView *d, iInt2 pos) { } iDeclareType(MiddleRunParams) - + struct Impl_MiddleRunParams { int midY; const iGmRun *closest; @@ -1126,7 +1126,7 @@ static iRect runRect_DocumentView_(const iDocumentView *d, const iGmRun *run) { } iDeclareType(DrawContext) - + struct Impl_DrawContext { const iDocumentView *view; iRect widgetBounds; @@ -1260,7 +1260,7 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { isInlineImageCaption = iFalse; } #endif - /* While this is consistent, it's a bit excessive to indicate that an inlined image + /* While this is consistent, it's a bit excessive to indicate that an inlined image is open: the image itself is the indication. */ const iBool isInlineImageCaption = iFalse; if (run->linkId && (linkFlags & isOpen_GmLinkFlag || isInlineImageCaption)) { @@ -1285,7 +1285,7 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { } fillRect_Paint(&d->paint, wideRect, bg); } - else { + else { /* Normal background for other runs. There are cases when runs get drawn multiple times, e.g., at the buffer boundary, and there are slightly overlapping characters in monospace blocks. Clearing the background here ensures a cleaner visual appearance @@ -2095,7 +2095,7 @@ static void invalidate_DocumentWidget_(iDocumentWidget *d) { } static iRangecc siteText_DocumentWidget_(const iDocumentWidget *d) { - return isEmpty_String(d->titleUser) ? urlHost_String(d->mod.url) + return isEmpty_String(d->titleUser) ? urlHost_String(d->mod.url) : range_String(d->titleUser); } @@ -2161,7 +2161,7 @@ static void updateBanner_DocumentWidget_(iDocumentWidget *d) { static void updateTheme_DocumentWidget_(iDocumentWidget *d) { if (document_App() != d || category_GmStatusCode(d->sourceStatus) == categoryInput_GmStatusCode) { return; - } + } d->view.drawBufs->flags |= updateTimestampBuf_DrawBufsFlag; updateBanner_DocumentWidget_(d); } @@ -2620,7 +2620,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, appendFormat_String(&str, cstr_Lang("doc.archive"), cstr_Rangecc(baseName_Path(d->mod.url))); - appendCStr_String(&str, "\n"); + appendCStr_String(&str, "\n"); } appendCStr_String(&str, "\n"); iString *localPath = localFilePathFromUrl_String(d->mod.url); @@ -2768,7 +2768,7 @@ static void updateTrust_DocumentWidget_(iDocumentWidget *d, const iGmResponse *r } else if (~d->certFlags & timeVerified_GmCertFlag) { updateTextCStr_LabelWidget(lock, isDarkMode ? orange_ColorEscape warning_Icon - : black_ColorEscape warning_Icon); + : black_ColorEscape warning_Icon); } else { updateTextCStr_LabelWidget(lock, green_ColorEscape closedLock_Icon); @@ -3067,7 +3067,7 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) { it is only displayed as an input dialog. */ visitUrl_Visited(visited_App(), d->mod.url, transient_VisitedUrlFlag); iUrl parts; - init_Url(&parts, d->mod.url); + init_Url(&parts, d->mod.url); iWidget *dlg = makeValueInput_Widget( as_Widget(d), NULL, @@ -3132,7 +3132,7 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) { setFont_LabelWidget(menu, font_LabelWidget((iLabelWidget *) lastChild_Widget(buttons))); setTextColor_LabelWidget(menu, uiTextAction_ColorId); } - } + } setValidator_InputWidget(findChild_Widget(dlg, "input"), inputQueryValidator_, d); setSensitiveContent_InputWidget(findChild_Widget(dlg, "input"), statusCode == sensitiveInput_GmStatusCode); @@ -3491,7 +3491,7 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { } /* The temporary "swipein" will display the previous page until the finger is lifted. */ iDocumentWidget *swipeIn = findChild_Widget(swipeParent, "swipein"); - if (!swipeIn) { + if (!swipeIn) { swipeIn = new_DocumentWidget(); swipeIn->flags |= animationPlaceholder_DocumentWidgetFlag; setId_Widget(as_Widget(swipeIn), "swipein"); @@ -3531,7 +3531,7 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { iWidget *swipeParent = swipeParent_DocumentWidget_(d); if (findChild_Widget(swipeParent, "swipeout")) { return iTrue; /* too fast, previous animation hasn't finished */ - } + } /* Setup the drag. `d` will be moving with the finger. */ animSpan = 0; postCommand_Widget(d, "navigate.forward"); @@ -3694,7 +3694,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) else if (equal_Command(cmd, "window.resized") || equal_Command(cmd, "font.changed") || equal_Command(cmd, "keyroot.changed")) { if (equal_Command(cmd, "font.changed")) { - invalidateCachedLayout_History(d->mod.history); + invalidateCachedLayout_History(d->mod.history); } /* Alt/Option key may be involved in window size changes. */ setLinkNumberMode_DocumentWidget_(d, iFalse); @@ -4056,7 +4056,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) } return wasHandled; } - else if (equal_Command(cmd, "document.upload") && d == document_App()) { + else if (equal_Command(cmd, "document.upload") && d == document_App()) { if (findChild_Widget(root_Widget(w), "upload")) { return iTrue; /* already open */ } @@ -4124,7 +4124,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) if (equalCase_Rangecc(urlScheme_String(d->mod.url), "titan")) { /* Reopen so the Upload dialog gets shown. */ postCommandf_App("open url:%s", cstr_String(d->mod.url)); - return iTrue; + return iTrue; } fetch_DocumentWidget_(d); return iTrue; @@ -4416,7 +4416,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) if (argLabel_Command(cmd, "ttf")) { iAssert(!cmp_String(&d->sourceMime, "font/ttf")); installFontFile_Fonts(collect_String(suffix_Command(cmd, "name")), &d->sourceContent); - postCommand_App("open url:about:fonts"); + postCommand_App("open url:about:fonts"); } else { const iString *id = idFromUrl_FontPack(d->mod.url); @@ -5435,7 +5435,7 @@ void init_DocumentWidget(iDocumentWidget *d) { init_Widget(w); setId_Widget(w, format_CStr("document%03d", ++docEnum_)); setFlags_Widget(w, hover_WidgetFlag | noBackground_WidgetFlag, iTrue); -#if defined (iPlatformAppleDesktop) +#if defined (iPlatformAppleDesktop) iBool enableSwipeNavigation = iTrue; /* swipes on the trackpad */ #else iBool enableSwipeNavigation = (deviceType_App() != desktop_AppDeviceType); @@ -5671,7 +5671,7 @@ iBool isRequestOngoing_DocumentWidget(const iDocumentWidget *d) { void takeRequest_DocumentWidget(iDocumentWidget *d, iGmRequest *finishedRequest) { cancelRequest_DocumentWidget_(d, iFalse /* don't post anything */); const iString *url = url_GmRequest(finishedRequest); - + add_History(d->mod.history, url); setUrl_DocumentWidget_(d, url); d->state = fetching_RequestState; diff --git a/src/ui/touch.c b/src/ui/touch.c index 0749bc7c..3d318dfb 100644 --- a/src/ui/touch.c +++ b/src/ui/touch.c @@ -42,7 +42,11 @@ iDeclareType(TouchState) static const uint32_t longPressSpanMs_ = 500; static const uint32_t shortPressSpanMs_ = 250; +#if defined (iPlatformAndroidMobile) +static const int tapRadiusPt_ = 30; /* inaccurate sensors? */ +#else static const int tapRadiusPt_ = 10; +#endif enum iTouchEdge { none_TouchEdge, diff --git a/src/ui/window.c b/src/ui/window.c index 0e13a57f..7f3371c8 100644 --- a/src/ui/window.c +++ b/src/ui/window.c @@ -263,6 +263,10 @@ static float pixelRatio_Window_(const iWindow *d) { # define baseDPI_Window 96.0f #endif +#if defined (iPlatformAndroidMobile) +float displayDensity_Android(void); +#endif + static float displayScale_Window_(const iWindow *d) { /* The environment variable LAGRANGE_OVERRIDE_DPI can be used to override the automatic display DPI detection. If not set, or is an empty string, ignore it. @@ -289,6 +293,8 @@ static float displayScale_Window_(const iWindow *d) { #elif defined (iPlatformMsys) iUnused(d); return desktopDPI_Win32(); +#elif defined (iPlatformAndroidMobile) + return displayDensity_Android(); #else if (isRunningUnderWindowSystem_App()) { float vdpi = 0.0f; @@ -457,7 +463,7 @@ void init_Window(iWindow *d, enum iWindowType type, iRect rect, uint32_t flags) d->mouseGrab = NULL; d->focus = NULL; d->pendingCursor = NULL; - d->isExposed = iFalse; + d->isExposed = (deviceType_App() != desktop_AppDeviceType); d->isMinimized = iFalse; d->isInvalidated = iFalse; /* set when posting event, to avoid repeated events */ d->isMouseInside = iTrue; @@ -541,6 +547,8 @@ void init_MainWindow(iMainWindow *d, iRect rect) { SDL_SetHint(SDL_HINT_RENDER_DRIVER, "metal"); flags |= SDL_WINDOW_METAL; d->base.isExposed = iTrue; +#elif defined (iPlatformAndroidMobile) + d->base.isExposed = iTrue; #else if (!forceSoftwareRender_App()) { flags |= SDL_WINDOW_OPENGL; -- cgit v1.2.3 From 011cff2b2fbba3910bb9aeae527602749ec7a814 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 24 Dec 2021 08:53:36 +0200 Subject: Updated the_Foundation --- lib/the_Foundation | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/the_Foundation b/lib/the_Foundation index 92cbb828..cd0a22f5 160000 --- a/lib/the_Foundation +++ b/lib/the_Foundation @@ -1 +1 @@ -Subproject commit 92cbb82857c0794b250abe835b98c9ee1b9f9c1c +Subproject commit cd0a22f5f003b723ecc038f287a320fb9b7c1d88 -- cgit v1.2.3 From f4650ecca043b630259470f34deed100b66929c6 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 25 Dec 2021 06:36:48 +0200 Subject: SDL: Updated patch for 2.0.18 There were a few changes, particularly with the precise wheel XY now being available. Lagrange v1.10 will listen to Cocoa wheel events directly, though, so these can be removed later on. --- sdl2.0.18-macos-ios.diff | 129 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 sdl2.0.18-macos-ios.diff diff --git a/sdl2.0.18-macos-ios.diff b/sdl2.0.18-macos-ios.diff new file mode 100644 index 00000000..8c143226 --- /dev/null +++ b/sdl2.0.18-macos-ios.diff @@ -0,0 +1,129 @@ +diff --git a/src/events/SDL_mouse.c b/src/events/SDL_mouse.c +index 539f17138..a5a5b00fd 100644 +--- a/src/events/SDL_mouse.c ++++ b/src/events/SDL_mouse.c +@@ -709,8 +709,8 @@ SDL_SendMouseWheel(SDL_Window * window, SDL_MouseID mouseID, float x, float y, S + event.type = SDL_MOUSEWHEEL; + event.wheel.windowID = mouse->focus ? mouse->focus->id : 0; + event.wheel.which = mouseID; +- event.wheel.x = integral_x; +- event.wheel.y = integral_y; ++ event.wheel.x = x; //integral_x; ++ event.wheel.y = y; //integral_y; + event.wheel.preciseX = x; + event.wheel.preciseY = y; + event.wheel.direction = (Uint32)direction; +diff --git a/src/video/cocoa/SDL_cocoamouse.m b/src/video/cocoa/SDL_cocoamouse.m +index e9d832d64..4cfa3624b 100644 +--- a/src/video/cocoa/SDL_cocoamouse.m ++++ b/src/video/cocoa/SDL_cocoamouse.m +@@ -463,10 +463,16 @@ + (NSCursor *)invisibleCursor + } + + SDL_MouseID mouseID = mouse->mouseID; +- CGFloat x = -[event deltaX]; +- CGFloat y = [event deltaY]; ++ CGFloat x = -[event scrollingDeltaX]; ++ CGFloat y = [event scrollingDeltaY]; + SDL_MouseWheelDirection direction = SDL_MOUSEWHEEL_NORMAL; + ++ /* HACK: Make a distinction between precise and imprecise scrolling. ++ Trackpad seems to be mouseID 0. */ ++ if (![event hasPreciseScrollingDeltas]) { ++ mouseID = 1; ++ } ++ + if ([event respondsToSelector:@selector(isDirectionInvertedFromDevice)]) { + if ([event isDirectionInvertedFromDevice] == YES) { + direction = SDL_MOUSEWHEEL_FLIPPED; +diff --git a/src/video/cocoa/SDL_cocoawindow.h b/src/video/cocoa/SDL_cocoawindow.h +index 489db169f..79a41dd50 100644 +--- a/src/video/cocoa/SDL_cocoawindow.h ++++ b/src/video/cocoa/SDL_cocoawindow.h +@@ -110,6 +110,8 @@ typedef enum + /* Touch event handling */ + -(void) handleTouches:(NSTouchPhase) phase withEvent:(NSEvent*) theEvent; + ++-(void) syncMouseButtonAndKeyboardModifierState; ++ + @end + /* *INDENT-ON* */ + +diff --git a/src/video/cocoa/SDL_cocoawindow.m b/src/video/cocoa/SDL_cocoawindow.m +index f09088c45..7379c2269 100644 +--- a/src/video/cocoa/SDL_cocoawindow.m ++++ b/src/video/cocoa/SDL_cocoawindow.m +@@ -1236,6 +1236,25 @@ - (void)otherMouseDown:(NSEvent *)theEvent + [self mouseDown:theEvent]; + } + ++- (void)syncMouseButtonAndKeyboardModifierState { ++ SDL_Mouse *mouse = SDL_GetMouse(); ++ if (mouse) { ++ for (int i = 0; i < mouse->num_sources; i++) { ++ if (mouse->sources[i].mouseID == mouse->mouseID) { ++ mouse->sources[i].buttonstate = 0; ++ } ++ } ++ } ++ SDL_SendKeyboardKey(SDL_RELEASED, SDL_SCANCODE_LGUI); ++ SDL_SendKeyboardKey(SDL_RELEASED, SDL_SCANCODE_RGUI); ++ SDL_SendKeyboardKey(SDL_RELEASED, SDL_SCANCODE_LSHIFT); ++ SDL_SendKeyboardKey(SDL_RELEASED, SDL_SCANCODE_RSHIFT); ++ SDL_SendKeyboardKey(SDL_RELEASED, SDL_SCANCODE_LCTRL); ++ SDL_SendKeyboardKey(SDL_RELEASED, SDL_SCANCODE_RCTRL); ++ SDL_SendKeyboardKey(SDL_RELEASED, SDL_SCANCODE_LALT); ++ SDL_SendKeyboardKey(SDL_RELEASED, SDL_SCANCODE_RALT); ++} ++ + - (void)mouseUp:(NSEvent *)theEvent + { + SDL_Mouse *mouse = SDL_GetMouse(); +diff --git a/src/video/uikit/SDL_uikitviewcontroller.h b/src/video/uikit/SDL_uikitviewcontroller.h +index f7f4c9de6..50c72aad0 100644 +--- a/src/video/uikit/SDL_uikitviewcontroller.h ++++ b/src/video/uikit/SDL_uikitviewcontroller.h +@@ -58,10 +58,13 @@ + #if !TARGET_OS_TV + - (NSUInteger)supportedInterfaceOrientations; + - (BOOL)prefersStatusBarHidden; ++- (void)setStatusStyle:(UIStatusBarStyle)style; ++- (UIStatusBarStyle)preferredStatusBarStyle; + - (BOOL)prefersHomeIndicatorAutoHidden; + - (UIRectEdge)preferredScreenEdgesDeferringSystemGestures; + + @property (nonatomic, assign) int homeIndicatorHidden; ++@property (nonatomic, assign) UIStatusBarStyle statusBarStyle; + #endif + + #if SDL_IPHONE_KEYBOARD +diff --git a/src/video/uikit/SDL_uikitviewcontroller.m b/src/video/uikit/SDL_uikitviewcontroller.m +index b2db4037d..d39f8085c 100644 +--- a/src/video/uikit/SDL_uikitviewcontroller.m ++++ b/src/video/uikit/SDL_uikitviewcontroller.m +@@ -105,6 +105,7 @@ - (instancetype)initWithSDLWindow:(SDL_Window *)_window + #endif + + #if !TARGET_OS_TV ++ self.statusBarStyle = UIStatusBarStyleDefault; + SDL_AddHintCallback(SDL_HINT_IOS_HIDE_HOME_INDICATOR, + SDL_HideHomeIndicatorHintChanged, + (__bridge void *) self); +@@ -230,6 +231,17 @@ - (BOOL)prefersHomeIndicatorAutoHidden + return hidden; + } + ++- (void)setStatusStyle:(UIStatusBarStyle)style ++{ ++ self.statusBarStyle = style; ++ [self setNeedsStatusBarAppearanceUpdate]; ++} ++ ++- (UIStatusBarStyle)preferredStatusBarStyle ++{ ++ return self.statusBarStyle; ++} ++ + - (UIRectEdge)preferredScreenEdgesDeferringSystemGestures + { + if (self.homeIndicatorHidden >= 0) { -- cgit v1.2.3 From e6a1ff50654f251d6f7b495e46d78d48a0f2a754 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 25 Dec 2021 06:42:56 +0200 Subject: SDL line drawing regression was fixed in 2.0.18 --- src/ui/labelwidget.c | 2 +- src/ui/paint.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui/labelwidget.c b/src/ui/labelwidget.c index 4ace6204..446cdc8a 100644 --- a/src/ui/labelwidget.c +++ b/src/ui/labelwidget.c @@ -349,7 +349,7 @@ static void draw_LabelWidget_(const iLabelWidget *d) { bottomRight_Rect(frameRect), bottomLeft_Rect(frameRect) }; -#if SDL_VERSION_ATLEAST(2, 0, 16) +#if SDL_COMPILEDVERSION == SDL_VERSIONNUM(2, 0, 16) if (isOpenGLRenderer_Window()) { /* A very curious regression in SDL 2.0.16. */ points[3].x--; diff --git a/src/ui/paint.c b/src/ui/paint.c index 439bdd37..c3b22343 100644 --- a/src/ui/paint.c +++ b/src/ui/paint.c @@ -113,7 +113,7 @@ void drawRect_Paint(const iPaint *d, iRect rect, int color) { { left_Rect(rect), br.y }, { left_Rect(rect), top_Rect(rect) } }; -#if SDL_VERSION_ATLEAST(2, 0, 16) +#if SDL_COMPILEDVERSION == SDL_VERSIONNUM(2, 0, 16) if (isOpenGLRenderer_Window()) { /* A very curious regression in SDL 2.0.16. */ edges[3].y--; -- cgit v1.2.3 From 1c29e992b9c7d17a4e0ca2c394fdf605706c961f Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 25 Dec 2021 07:27:29 +0200 Subject: Cleanup Warnings about lost precision. --- src/audio/player.c | 4 ++-- src/feeds.c | 6 +++--- src/gmcerts.c | 2 +- src/gmrequest.c | 5 +++-- src/media.c | 2 +- src/sitespec.c | 2 +- src/ui/color.c | 2 +- src/ui/command.c | 2 +- src/ui/mobile.c | 2 +- src/ui/paint.c | 2 +- src/ui/scrollwidget.c | 4 ++-- src/ui/util.c | 2 +- src/ui/window.c | 6 +++--- src/visited.c | 2 +- 14 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/audio/player.c b/src/audio/player.c index bf853e3f..de430b17 100644 --- a/src/audio/player.c +++ b/src/audio/player.c @@ -192,7 +192,7 @@ static enum iDecoderStatus decodeVorbis_Decoder_(iDecoder *d) { int error; int consumed; d->vorbis = stb_vorbis_open_pushdata( - constData_Block(input), size_Block(input), &consumed, &error, NULL); + constData_Block(input), (int) size_Block(input), &consumed, &error, NULL); if (!d->vorbis) { return needMoreInput_DecoderStatus; } @@ -225,7 +225,7 @@ static enum iDecoderStatus decodeVorbis_Decoder_(iDecoder *d) { lock_Mutex(&d->input->mtx); d->totalInputSize = size_Block(input); int error = 0; - stb_vorbis *vrb = stb_vorbis_open_memory(constData_Block(input), size_Block(input), + stb_vorbis *vrb = stb_vorbis_open_memory(constData_Block(input), (int) size_Block(input), &error, NULL); if (vrb) { d->totalSamples = stb_vorbis_stream_length_in_samples(vrb); diff --git a/src/feeds.c b/src/feeds.c index b7ce739b..7fe13617 100644 --- a/src/feeds.c +++ b/src/feeds.c @@ -437,7 +437,7 @@ static iThreadResult fetch_Feeds_(iThread *thread) { iZap(work); iBool gotNew = iFalse; postCommand_App("feeds.update.started"); - const int totalJobs = size_PtrArray(&d->jobs); + const size_t totalJobs = size_PtrArray(&d->jobs); int numFinishedJobs = 0; while (!d->stopWorker) { /* Start new jobs. */ @@ -476,7 +476,7 @@ static iThreadResult fetch_Feeds_(iThread *thread) { } } if (doNotify) { - postCommandf_App("feeds.update.progress arg:%d total:%d", numFinishedJobs, totalJobs); + postCommandf_App("feeds.update.progress arg:%d total:%zu", numFinishedJobs, totalJobs); } /* Stop if everything has finished. */ if (ongoing == 0 && isEmpty_PtrArray(&d->jobs)) { @@ -614,7 +614,7 @@ static void load_Feeds_(iFeeds *d) { /* TODO: Cleanup needed... All right, this could maybe use a bit more robust, structured format. The code below is messy. */ - const uint32_t feedId = strtoul(line.start, NULL, 16); + const uint32_t feedId = (uint32_t) strtoul(line.start, NULL, 16); if (!nextSplit_Rangecc(range_Block(src), "\n", &line)) { goto aborted; } diff --git a/src/gmcerts.c b/src/gmcerts.c index ed4759be..8f7bf181 100644 --- a/src/gmcerts.c +++ b/src/gmcerts.c @@ -90,7 +90,7 @@ void serialize_GmIdentity(const iGmIdentity *d, iStream *outs) { writeU32_Stream(outs, d->icon); serialize_String(&d->notes, outs); write32_Stream(outs, d->flags); - writeU32_Stream(outs, size_StringSet(d->useUrls)); + writeU32_Stream(outs, (uint32_t) size_StringSet(d->useUrls)); iConstForEach(StringSet, i, d->useUrls) { serialize_String(i.value, outs); } diff --git a/src/gmrequest.c b/src/gmrequest.c index 23845475..a9c5919d 100644 --- a/src/gmrequest.c +++ b/src/gmrequest.c @@ -790,7 +790,8 @@ void submit_GmRequest(iGmRequest *d) { cstr_String(containerUrl)); appendFormat_String(page, "# %s\n\n", cstr_Rangecc(containerName)); appendFormat_String(page, - cstrCount_Lang("archive.summary.n", numEntries_Archive(arch)), + cstrCount_Lang("archive.summary.n", + (int) numEntries_Archive(arch)), numEntries_Archive(arch), (double) sourceSize_Archive(arch) / 1.0e6); appendCStr_String(page, "\n\n"); @@ -802,7 +803,7 @@ void submit_GmRequest(iGmRequest *d) { } else if (size_StringSet(contents) > 1) { appendFormat_String(page, cstrCount_Lang("dir.summary.n", - size_StringSet(contents)), + (int) size_StringSet(contents)), size_StringSet(contents)); appendCStr_String(page, "\n\n"); } diff --git a/src/media.c b/src/media.c index a3f381ec..c02090b0 100644 --- a/src/media.c +++ b/src/media.c @@ -144,7 +144,7 @@ void makeTexture_GmImage(iGmImage *d) { } else { imgData = stbi_load_from_memory( - constData_Block(data), size_Block(data), &d->size.x, &d->size.y, NULL, 4); + constData_Block(data), (int) size_Block(data), &d->size.x, &d->size.y, NULL, 4); if (!imgData) { fprintf(stderr, "[media] image load failed: %s\n", stbi_failure_reason()); } diff --git a/src/sitespec.c b/src/sitespec.c index f8b77c86..fe80ad13 100644 --- a/src/sitespec.c +++ b/src/sitespec.c @@ -141,7 +141,7 @@ static void handleIniKeyValue_SiteSpec_(void *context, const iString *table, con set_String(&d->loadParams->titanIdentity, value->value.string); } else if (!cmp_String(key, "dismissWarnings") && value->type == int64_TomlType) { - d->loadParams->dismissWarnings = value->value.int64; + d->loadParams->dismissWarnings = (int) value->value.int64; } else if (!cmp_String(key, "usedIdentities") && value->type == string_TomlType) { iRangecc seg = iNullRange; diff --git a/src/ui/color.c b/src/ui/color.c index ed17f580..3c2f0339 100644 --- a/src/ui/color.c +++ b/src/ui/color.c @@ -832,7 +832,7 @@ void ansiColors_Color(iRangecc escapeSequence, int fgDefault, int bgDefault, int rgb[3] = { 0, 0, 0 }; iForIndices(i, rgb) { if (ch >= escapeSequence.end) break; - rgb[i] = strtoul(ch + 1, &endPtr, 10); + rgb[i] = (int) strtoul(ch + 1, &endPtr, 10); ch = endPtr; } dst->r = iClamp(rgb[0], 0, 255); diff --git a/src/ui/command.c b/src/ui/command.c index d6c668db..a4868ca9 100644 --- a/src/ui/command.c +++ b/src/ui/command.c @@ -80,7 +80,7 @@ uint32_t argU32Label_Command(const char *cmd, const char *label) { init_Token(&tok, label); const iRangecc ptr = find_Token(&tok, cmd); if (ptr.start) { - return strtoul(ptr.end, NULL, 10); + return (uint32_t) strtoul(ptr.end, NULL, 10); } return 0; } diff --git a/src/ui/mobile.c b/src/ui/mobile.c index bffc2177..08d8dba2 100644 --- a/src/ui/mobile.c +++ b/src/ui/mobile.c @@ -210,7 +210,7 @@ static iBool topPanelHandler_(iWidget *topPanel, const char *cmd) { /* Animate the current panel in. */ if (child == panel && isPortrait) { setupSheetTransition_Mobile(panel, iTrue); - panelIndex = childIndex; + panelIndex = (int) childIndex; } childIndex++; } diff --git a/src/ui/paint.c b/src/ui/paint.c index c3b22343..5e66f521 100644 --- a/src/ui/paint.c +++ b/src/ui/paint.c @@ -175,7 +175,7 @@ void drawLines_Paint(const iPaint *d, const iInt2 *points, size_t n, int color) for (size_t i = 0; i < n; i++) { offsetPoints[i] = add_I2(points[i], origin_Paint); } - SDL_RenderDrawLines(renderer_Paint_(d), (const SDL_Point *) offsetPoints, n); + SDL_RenderDrawLines(renderer_Paint_(d), (const SDL_Point *) offsetPoints, (int) n); free(offsetPoints); } diff --git a/src/ui/scrollwidget.c b/src/ui/scrollwidget.c index b6f73b6c..651669c6 100644 --- a/src/ui/scrollwidget.c +++ b/src/ui/scrollwidget.c @@ -107,7 +107,7 @@ static iRect bounds_ScrollWidget_(const iScrollWidget *d) { static iRect thumbRect_ScrollWidget_(const iScrollWidget *d) { const iRect bounds = bounds_ScrollWidget_(d); iRect rect = init_Rect(bounds.pos.x, bounds.pos.y, bounds.size.x, 0); - const int total = size_Range(&d->range); + const int total = (int) size_Range(&d->range); if (total > 0) { const int tsize = thumbSize_ScrollWidget_(d); // iAssert(tsize <= height_Rect(bounds)); @@ -197,7 +197,7 @@ static iBool processEvent_ScrollWidget_(iScrollWidget *d, const SDL_Event *ev) { case drag_ClickResult: { const iRect bounds = bounds_ScrollWidget_(d); const int offset = delta_Click(&d->click).y; - const int total = size_Range(&d->range); + const int total = (int) size_Range(&d->range); int dpos = (float) offset / (float) (height_Rect(bounds) - thumbSize_ScrollWidget_(d)) * total; d->thumb = iClamp(d->startThumb + dpos, d->range.start, d->range.end); postCommand_Widget(w, "scroll.moved arg:%d", d->thumb); diff --git a/src/ui/util.c b/src/ui/util.c index 58e49230..4ce40ae4 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -1427,7 +1427,7 @@ static iBool tabSwitcher_(iWidget *tabs, const char *cmd) { iWidget *nextTabs = findChild_Widget(otherRoot_Window(get_Window(), tabs->root)->widget, "doctabs"); iWidget *nextPages = findChild_Widget(nextTabs, "tabs.pages"); - tabIndex = (dir < 0 ? childCount_Widget(nextPages) - 1 : 0); + tabIndex = (int) (dir < 0 ? childCount_Widget(nextPages) - 1 : 0); showTabPage_Widget(nextTabs, child_Widget(nextPages, tabIndex)); postCommand_App("keyroot.next"); } diff --git a/src/ui/window.c b/src/ui/window.c index 7f3371c8..d694146a 100644 --- a/src/ui/window.c +++ b/src/ui/window.c @@ -442,7 +442,7 @@ void create_Window_(iWindow *d, iRect rect, uint32_t flags) { static SDL_Surface *loadImage_(const iBlock *data, int resized) { int w = 0, h = 0, num = 4; stbi_uc *pixels = stbi_load_from_memory( - constData_Block(data), size_Block(data), &w, &h, &num, STBI_rgb_alpha); + constData_Block(data), (int) size_Block(data), &w, &h, &num, STBI_rgb_alpha); if (resized) { stbi_uc *rsPixels = malloc(num * resized * resized); stbir_resize_uint8(pixels, w, h, 0, rsPixels, resized, resized, 0, num); @@ -1549,9 +1549,9 @@ void setSplitMode_MainWindow(iMainWindow *d, int splitFlags) { iWidget *docTabs1 = findChild_Widget(w->roots[newRootIndex]->widget, "doctabs"); /* If the old root has multiple tabs, move the current one to the new split. */ if (tabCount_Widget(docTabs0) >= 2) { - int movedIndex = tabPageIndex_Widget(docTabs0, moved); + size_t movedIndex = tabPageIndex_Widget(docTabs0, moved); removeTabPage_Widget(docTabs0, movedIndex); - showTabPage_Widget(docTabs0, tabPage_Widget(docTabs0, iMax(movedIndex - 1, 0))); + showTabPage_Widget(docTabs0, tabPage_Widget(docTabs0, iMax((int) movedIndex - 1, 0))); iRelease(removeTabPage_Widget(docTabs1, 0)); /* delete the default tab */ setRoot_Widget(as_Widget(moved), w->roots[newRootIndex]); prependTabPage_Widget(docTabs1, iClob(moved), "", 0, 0); diff --git a/src/visited.c b/src/visited.c index 4552a053..83e09071 100644 --- a/src/visited.c +++ b/src/visited.c @@ -105,7 +105,7 @@ void load_Visited(iVisited *d, const char *dirPath) { char *endp = NULL; const unsigned long long ts = strtoull(line.start, &endp, 10); if (ts == 0) break; - const uint32_t flags = strtoul(skipSpace_CStr(endp), &endp, 16); + const uint32_t flags = (uint32_t) strtoul(skipSpace_CStr(endp), &endp, 16); const char *urlStart = skipSpace_CStr(endp); iVisitedUrl item; item.when.ts = (struct timespec){ .tv_sec = ts }; -- cgit v1.2.3 From 38f1502c2d643c602c66be72656e79f5854f85e6 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 25 Dec 2021 07:43:51 +0200 Subject: Mobile: Fixed pull-to-refresh --- src/ui/documentwidget.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index b20ae672..313fe2ee 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -480,9 +480,6 @@ void init_DocumentView(iDocumentView *d) { d->invalidRuns = new_PtrSet(); d->drawBufs = new_DrawBufs(); d->pageMargin = 5; - if (deviceType_App() != desktop_AppDeviceType) { - d->scrollY.flags |= pullDownAction_SmoothScrollFlag; /* pull to refresh */ - } d->hoverPre = NULL; d->hoverAltPre = NULL; d->hoverLink = NULL; @@ -523,6 +520,9 @@ void deinit_DocumentView(iDocumentView *d) { static void setOwner_DocumentView_(iDocumentView *d, iDocumentWidget *doc) { d->owner = doc; init_SmoothScroll(&d->scrollY, as_Widget(doc), scrollBegan_DocumentWidget_); + if (deviceType_App() != desktop_AppDeviceType) { + d->scrollY.flags |= pullDownAction_SmoothScrollFlag; /* pull to refresh */ + } } static void resetWideRuns_DocumentView_(iDocumentView *d) { -- cgit v1.2.3 From 11a3590215674b9b1278629f24954ed1e5f586a3 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 25 Dec 2021 07:49:16 +0200 Subject: "Idle sleep" seems unnecessary with SDL 2.0.18 --- CMakeLists.txt | 2 +- README.md | 2 +- res/about/ios-version.gmi | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4f414496..134f9e8f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,7 +42,7 @@ option (ENABLE_FRIBIDI "Use the GNU FriBidi library for bidirectional t option (ENABLE_FRIBIDI_BUILD "Build the GNU FriBidi library (if OFF, try pkg-config)" OFF) option (ENABLE_HARFBUZZ "Use the HarfBuzz library to shape text" ON) option (ENABLE_HARFBUZZ_MINIMAL "Build the HarfBuzz library with minimal dependencies (if OFF, try pkg-config)" OFF) -option (ENABLE_IDLE_SLEEP "While idle, sleep in the main thread instead of waiting for events" ON) +option (ENABLE_IDLE_SLEEP "While idle, sleep in the main thread instead of waiting for events" OFF) option (ENABLE_IPC "Use IPC to communicate between running instances" ON) option (ENABLE_KERNING "Enable kerning in font renderer (slower)" ON) option (ENABLE_MAC_MENUS "Use native context menus (macOS)" ON) diff --git a/README.md b/README.md index 9e68bf26..b6fc2258 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ Note that the `install` target also deploys an XDG .desktop file for launching t | `ENABLE_BINCAT_SH` | Merge resource files (fonts, etc.) together using a Bash shell script. By default this is **OFF**, so _res/bincat.c_ is compiled as a native executable for this purpose. However, when cross-compiling, native binaries built during the CMake run may be targeted for the wrong architecture. Set this to **ON** if you are having problems with bincat while running CMake. | | `ENABLE_CUSTOM_FRAME` | Draw a custom window frame. (Only on Microsoft Windows.) The custom frame is more in line with the visual style of the rest of the UI, but does not implement all of the native window behaviors (e.g., snapping, system menu). | | `ENABLE_DOWNLOAD_EDIT` | Allow changing the Downloads directory via the Preferences dialog. This should be set to **OFF** in sandboxed environments where downloaded files must be saved into a specific place. | -| `ENABLE_IDLE_SLEEP` | Sleep in the main thread instead of waiting for events. On some platforms, `SDL_WaitEvent()` may have a relatively high CPU usage. Setting this to **ON** polls for events periodically but otherwise keeps the main thread sleeping, reducing CPU usage. The drawback is that there is a slightly increased latency reacting to new events after idle mode ends. | +| `ENABLE_IDLE_SLEEP` | Sleep in the main thread instead of waiting for events. On some platforms, when using SDL 2.0.16 or earlier, `SDL_WaitEvent()` may have a relatively high CPU usage. Setting this to **ON** polls for events periodically but otherwise keeps the main thread sleeping, reducing CPU usage. The drawback is that there is a slightly increased latency reacting to new events after idle mode ends. | | `ENABLE_FRIBIDI` | Use the GNU FriBidi library for processing bidirectional text. FriBidi implements the Unicode Bidirectional Algorithm to determine text directions. | | `ENABLE_FRIBIDI_BUILD` | Compile the GNU FriBidi library as part of the build. If set to **OFF**, `pkg-config` is used instead to locate the library. | | `ENABLE_HARFBUZZ` | Use the HarfBuzz library for shaping Unicode text. This is required for correctly rendering complex scripts and combining glyphs. If disabled, a simplified text shaping algorithm is used that only works for non-complex languages like English. | diff --git a/res/about/ios-version.gmi b/res/about/ios-version.gmi index 6cdaef3e..89e54e66 100644 --- a/res/about/ios-version.gmi +++ b/res/about/ios-version.gmi @@ -11,6 +11,7 @@ * Improved apperance of opened links highlighting. It no longer goes under the side elements on the page. * Gempub: Open books in 1:2 split mode instead of 1:1. * Fixed opening links in split view mode. Links would open on the wrong side. +* Upgraded SDL to version 2.0.18. ## 1.10 (6) * Added "Paste Preceding Line" in the input prompt dialog. Potential use cases include user-editable sections on a page and suggested/example input values. -- cgit v1.2.3 From 2007dc961480151721e646aac468a0ae5639b18f Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 25 Dec 2021 14:36:57 +0200 Subject: Mobile: Link info in context menu --- src/ui/documentwidget.c | 4 +- src/ui/labelwidget.c | 4 +- src/ui/linkinfo.c | 141 ++++++++++++++++++++++++------------------------ src/ui/linkinfo.h | 2 + src/ui/util.c | 4 +- 5 files changed, 82 insertions(+), 73 deletions(-) diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 313fe2ee..81aa382f 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -4851,8 +4851,10 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e iBool isNative = iFalse; if (deviceType_App() != desktop_AppDeviceType) { /* Show the link as the first, non-interactive item. */ + iString *infoText = collectNew_String(); + infoText_LinkInfo(d->view.doc, d->contextLink->linkId, infoText); pushBack_Array(&items, &(iMenuItem){ - format_CStr("```%s", cstr_String(linkUrl)), + format_CStr("```%s", cstr_String(infoText)), 0, 0, NULL }); } if (willUseProxy_App(scheme) || isGemini || diff --git a/src/ui/labelwidget.c b/src/ui/labelwidget.c index 446cdc8a..44ae3eec 100644 --- a/src/ui/labelwidget.c +++ b/src/ui/labelwidget.c @@ -330,6 +330,7 @@ static void draw_LabelWidget_(const iLabelWidget *d) { init_Paint(&p); int bg, fg, frame, frame2, iconColor, metaColor; getColors_LabelWidget_(d, &bg, &fg, &frame, &frame2, &iconColor, &metaColor); + setBaseAttributes_Text(d->font, fg); const enum iColorId colorEscape = parseEscape_Color(cstr_String(&d->label), NULL); const iBool isCaution = (colorEscape == uiTextCaution_ColorId); if (bg >= 0) { @@ -452,7 +453,8 @@ static void draw_LabelWidget_(const iLabelWidget *d) { iTrue, iconColor, d->flags.chevron ? rightAngle_Icon : check_Icon); - } + } + setBaseAttributes_Text(-1, -1); unsetClip_Paint(&p); drawChildren_Widget(w); } diff --git a/src/ui/linkinfo.c b/src/ui/linkinfo.c index 89c7fbf3..cb48c7ea 100644 --- a/src/ui/linkinfo.c +++ b/src/ui/linkinfo.c @@ -51,6 +51,73 @@ iInt2 size_LinkInfo(const iLinkInfo *d) { return add_I2(d->buf->size, init_I2(2 * hPad_LinkInfo_, 2 * vPad_LinkInfo_)); } +void infoText_LinkInfo(const iGmDocument *doc, iGmLinkId linkId, iString *text_out) { + const iString *url = linkUrl_GmDocument(doc, linkId); + iUrl parts; + init_Url(&parts, url); + const int flags = linkFlags_GmDocument(doc, linkId); + const enum iGmLinkScheme scheme = scheme_GmLinkFlag(flags); + const iBool isImage = (flags & imageFileExtension_GmLinkFlag) != 0; + const iBool isAudio = (flags & audioFileExtension_GmLinkFlag) != 0; + /* Most important info first: the identity that will be used. */ + const iGmIdentity *ident = identityForUrl_GmCerts(certs_App(), url); + if (ident) { + appendFormat_String(text_out, person_Icon " %s", + //escape_Color(tmBannerItemTitle_ColorId), + cstr_String(name_GmIdentity(ident))); + } + /* Possibly inlined content. */ + if (isImage || isAudio) { + if (!isEmpty_String(text_out)) { + appendCStr_String(text_out, "\n"); + } + appendCStr_String( + text_out, + format_CStr(isImage ? photo_Icon " %s " : "\U0001f3b5 %s", + cstr_Lang(isImage ? "link.hint.image" : "link.hint.audio"))); + } + if (!isEmpty_String(text_out)) { + appendCStr_String(text_out, " \u2014 "); + } + /* Indicate non-Gemini schemes. */ + if (scheme == mailto_GmLinkScheme) { + appendCStr_String(text_out, envelope_Icon " "); + append_String(text_out, url); + } + else if (scheme != gemini_GmLinkScheme && !isEmpty_Range(&parts.host)) { + appendCStr_String(text_out, globe_Icon " \x1b[1m"); + appendRange_String(text_out, (iRangecc){ constBegin_String(url), + parts.host.end }); + appendCStr_String(text_out, "\x1b[0m"); + appendRange_String(text_out, (iRangecc){ parts.path.start, constEnd_String(url) }); + } + else if (scheme != gemini_GmLinkScheme) { + appendCStr_String(text_out, globe_Icon " "); + append_String(text_out, url); + } + else { + appendCStr_String(text_out, "\x1b[1m"); + appendRange_String(text_out, parts.host); + if (!isEmpty_Range(&parts.port)) { + appendCStr_String(text_out, ":"); + appendRange_String(text_out, parts.port); + } + appendCStr_String(text_out, "\x1b[0m"); + appendRange_String(text_out, (iRangecc){ parts.path.start, constEnd_String(url) }); + } + /* Date of last visit. */ + if (flags & visited_GmLinkFlag) { + iDate date; + init_Date(&date, linkTime_GmDocument(doc, linkId)); + if (!isEmpty_String(text_out)) { + appendCStr_String(text_out, " \u2014 "); + } + iString *dateStr = format_Date(&date, "%b %d"); + append_String(text_out, dateStr); + delete_String(dateStr); + } +} + iBool update_LinkInfo(iLinkInfo *d, const iGmDocument *doc, iGmLinkId linkId, int maxWidth) { if (!d) { return iFalse; @@ -61,80 +128,14 @@ iBool update_LinkInfo(iLinkInfo *d, const iGmDocument *doc, iGmLinkId linkId, in d->maxWidth = maxWidth; invalidate_LinkInfo(d); if (linkId) { - /* Measure and draw. */ - if (targetValue_Anim(&d->opacity) < 1) { - setValue_Anim(&d->opacity, 1, isAnimated ? 75 : 0); - } - const int avail = iMax(minWidth_LinkInfo_, maxWidth) - 2 * hPad_LinkInfo_; - const iString *url = linkUrl_GmDocument(doc, linkId); - iUrl parts; - init_Url(&parts, url); - const int flags = linkFlags_GmDocument(doc, linkId); - const enum iGmLinkScheme scheme = scheme_GmLinkFlag(flags); - const iBool isImage = (flags & imageFileExtension_GmLinkFlag) != 0; - const iBool isAudio = (flags & audioFileExtension_GmLinkFlag) != 0; - // int fg = linkColor_GmDocument(doc, linkId, - // textHover_GmLinkPart); iString str; init_String(&str); - /* Most important info first: the identity that will be used. */ - const iGmIdentity *ident = identityForUrl_GmCerts(certs_App(), url); - if (ident) { - appendFormat_String(&str, person_Icon " %s", - //escape_Color(tmBannerItemTitle_ColorId), - cstr_String(name_GmIdentity(ident))); - } - /* Possibly inlined content. */ - if (isImage || isAudio) { - if (!isEmpty_String(&str)) { - appendCStr_String(&str, "\n"); - } - appendCStr_String( - &str, - format_CStr(isImage ? photo_Icon " %s " : "\U0001f3b5 %s", - cstr_Lang(isImage ? "link.hint.image" : "link.hint.audio"))); - } - if (!isEmpty_String(&str)) { - appendCStr_String(&str, " \u2014 "); - } - /* Indicate non-Gemini schemes. */ - if (scheme == mailto_GmLinkScheme) { - appendCStr_String(&str, envelope_Icon " "); - append_String(&str, url); - } - else if (scheme != gemini_GmLinkScheme && !isEmpty_Range(&parts.host)) { - appendCStr_String(&str, globe_Icon " \x1b[1m"); - appendRange_String(&str, (iRangecc){ constBegin_String(url), - parts.host.end }); - appendCStr_String(&str, "\x1b[0m"); - appendRange_String(&str, (iRangecc){ parts.path.start, constEnd_String(url) }); - } - else if (scheme != gemini_GmLinkScheme) { - appendCStr_String(&str, globe_Icon " "); - append_String(&str, url); - } - else { - appendCStr_String(&str, "\x1b[1m"); - appendRange_String(&str, parts.host); - if (!isEmpty_Range(&parts.port)) { - appendCStr_String(&str, ":"); - appendRange_String(&str, parts.port); - } - appendCStr_String(&str, "\x1b[0m"); - appendRange_String(&str, (iRangecc){ parts.path.start, constEnd_String(url) }); - } - /* Date of last visit. */ - if (flags & visited_GmLinkFlag) { - iDate date; - init_Date(&date, linkTime_GmDocument(doc, linkId)); - if (!isEmpty_String(&str)) { - appendCStr_String(&str, " \u2014 "); - } - iString *dateStr = format_Date(&date, "%b %d"); - append_String(&str, dateStr); - delete_String(dateStr); + infoText_LinkInfo(doc, linkId, &str); + if (targetValue_Anim(&d->opacity) < 1) { + setValue_Anim(&d->opacity, 1, isAnimated ? 75 : 0); } /* Draw to a buffer, wrapped. */ + const int avail = iMax(minWidth_LinkInfo_, maxWidth) - 2 * hPad_LinkInfo_; iWrapText wt = { .text = range_String(&str), .maxWidth = avail, .mode = word_WrapTextMode }; d->buf = new_TextBuf(&wt, uiLabel_FontId, tmQuote_ColorId); deinit_String(&str); diff --git a/src/ui/linkinfo.h b/src/ui/linkinfo.h index a1669f95..38b90b87 100644 --- a/src/ui/linkinfo.h +++ b/src/ui/linkinfo.h @@ -41,5 +41,7 @@ iBool update_LinkInfo (iLinkInfo *, const iGmDocument *doc, iGmLinkId link int maxWidth); /* returns true if changed */ void invalidate_LinkInfo (iLinkInfo *); +void infoText_LinkInfo (const iGmDocument *doc, iGmLinkId linkId, iString *text_out); + iInt2 size_LinkInfo (const iLinkInfo *); void draw_LinkInfo (const iLinkInfo *, iInt2 topLeft); diff --git a/src/ui/util.c b/src/ui/util.c index 4ce40ae4..ff127f25 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -774,7 +774,9 @@ void makeMenuItems_Widget(iWidget *menu, const iMenuItem *items, size_t n) { noBackground_WidgetFlag | frameless_WidgetFlag | alignLeft_WidgetFlag | drawKey_WidgetFlag | itemFlags); setWrap_LabelWidget(label, isInfo); - haveIcons |= checkIcon_LabelWidget(label); + if (!isInfo) { + haveIcons |= checkIcon_LabelWidget(label); + } setFlags_Widget(as_Widget(label), disabled_WidgetFlag, isDisabled); if (isInfo) { setFlags_Widget(as_Widget(label), resizeToParentWidth_WidgetFlag | -- cgit v1.2.3 From ca442991158d525f3a8e2f5c31492921059f4797 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 25 Dec 2021 15:37:18 +0200 Subject: Mobile: Same indication for hover links and open URLs --- src/ui/documentwidget.c | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 81aa382f..1b5b9af2 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -1243,9 +1243,9 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { const iGmDocument *doc = d->view->doc; const int linkFlags = linkFlags_GmDocument(doc, run->linkId); /* Hover state of a link. */ - iBool isHover = - (run->linkId && d->view->hoverLink && run->linkId == d->view->hoverLink->linkId && - ~run->flags & decoration_GmRunFlag); + const iBool isPartOfHover = (run->linkId && d->view->hoverLink && + run->linkId == d->view->hoverLink->linkId); + iBool isHover = (isPartOfHover && ~run->flags & decoration_GmRunFlag); /* Visible (scrolled) position of the run. */ const iInt2 visPos = addX_I2(add_I2(run->visBounds.pos, origin), /* Preformatted runs can be scrolled. */ @@ -1260,18 +1260,23 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { isInlineImageCaption = iFalse; } #endif + iBool isMobileHover = deviceType_App() != desktop_AppDeviceType && + (isPartOfHover || contains_PtrSet(d->view->invalidRuns, run)); /* While this is consistent, it's a bit excessive to indicate that an inlined image is open: the image itself is the indication. */ const iBool isInlineImageCaption = iFalse; - if (run->linkId && (linkFlags & isOpen_GmLinkFlag || isInlineImageCaption)) { + if (run->linkId && (linkFlags & isOpen_GmLinkFlag || isInlineImageCaption || isMobileHover)) { /* Open links get a highlighted background. */ int bg = tmBackgroundOpenLink_ColorId; + if (isMobileHover && !isPartOfHover) { + bg = tmBackground_ColorId; /* hover ended and was invalidated */ + } const int frame = tmFrameOpenLink_ColorId; const int pad = gap_Text; iRect wideRect = { init_I2(origin.x - pad, visPos.y), init_I2(d->docBounds.size.x + 2 * pad, height_Rect(run->visBounds)) }; - adjustEdges_Rect(&wideRect, + adjustEdges_Rect(&wideRect, run->flags & startOfLine_GmRunFlag ? -pad * 3 / 4 : 0, 0, run->flags & endOfLine_GmRunFlag ? pad * 3 / 4 : 0, 0); /* The first line is composed of two runs that may be drawn in either order, so -- cgit v1.2.3 From af356ea33f54e81b35ca24274cc33e6fc295e019 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 25 Dec 2021 15:37:29 +0200 Subject: App: Tweaking idle sleep for mobile --- src/app.c | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/app.c b/src/app.c index 3ee2345c..3b4c24f0 100644 --- a/src/app.c +++ b/src/app.c @@ -1208,10 +1208,6 @@ iLocalDef iBool isWaitingAllowed_App_(iApp *d) { } static iBool nextEvent_App_(iApp *d, enum iAppEventMode eventMode, SDL_Event *event) { - if (d->isSuspended) { - /* Do nothing except wait for the app to return to the foreground. */ - return SDL_WaitEvent(event); - } if (eventMode == waitForNewEvents_AppEventMode && isWaitingAllowed_App_(d)) { /* If there are periodic commands pending, wait only for a short while. */ if (!isEmpty_Periodic(&d->periodic)) { @@ -1462,10 +1458,10 @@ void processEvents_App(enum iAppEventMode eventMode) { deinit_PtrArray(&windows); #if defined (LAGRANGE_ENABLE_IDLE_SLEEP) if (d->isIdling && !gotEvents) { - /* This is where we spend most of our time when idle. 60 Hz still quite a lot but we + /* This is where we spend most of our time when idle. 30 Hz still quite a lot but we can't wait too long after the user tries to interact again with the app. In any - case, on macOS SDL_WaitEvent() seems to use 10x more CPU time than sleeping. */ - SDL_Delay(1000 / 60); + case, on iOS SDL_WaitEvent() seems to use 10x more CPU time than sleeping (2.0.18). */ + SDL_Delay(1000 / 30); } #endif backToMainLoop:; -- cgit v1.2.3 From aa2ef95dc93c2cd2c4f9163836111333a12db217 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 25 Dec 2021 15:37:43 +0200 Subject: iOS: Default to idle sleep enabled --- CMakeLists.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 134f9e8f..a5b76eb4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,6 +34,10 @@ set (DEFAULT_RESIZE_DRAW ON) if (HAIKU) set (DEFAULT_RESIZE_DRAW OFF) endif () +set (DEFAULT_IDLE_SLEEP OFF) +if (IOS) + set (DEFAULT_IDLE_SLEEP ON) +endif () # Build configuration. option (ENABLE_CUSTOM_FRAME "Draw a custom window frame (Windows)" OFF) @@ -42,7 +46,7 @@ option (ENABLE_FRIBIDI "Use the GNU FriBidi library for bidirectional t option (ENABLE_FRIBIDI_BUILD "Build the GNU FriBidi library (if OFF, try pkg-config)" OFF) option (ENABLE_HARFBUZZ "Use the HarfBuzz library to shape text" ON) option (ENABLE_HARFBUZZ_MINIMAL "Build the HarfBuzz library with minimal dependencies (if OFF, try pkg-config)" OFF) -option (ENABLE_IDLE_SLEEP "While idle, sleep in the main thread instead of waiting for events" OFF) +option (ENABLE_IDLE_SLEEP "While idle, sleep in the main thread instead of waiting for events" ${DEFAULT_IDLE_SLEEP}) option (ENABLE_IPC "Use IPC to communicate between running instances" ON) option (ENABLE_KERNING "Enable kerning in font renderer (slower)" ON) option (ENABLE_MAC_MENUS "Use native context menus (macOS)" ON) -- cgit v1.2.3 From 0e41c654ec0e1cfc6c4f30d90551a9722c727489 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 26 Dec 2021 06:20:54 +0200 Subject: Mobile: Removed "Show URL on hover" --- src/ui/util.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/util.c b/src/ui/util.c index ff127f25..39cfce13 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -2476,7 +2476,7 @@ iWidget *makePreferences_Widget(void) { { "title id:heading.prefs.interface" }, { "dropdown device:0 id:prefs.returnkey", 0, 0, (const void *) returnKeyBehaviors }, { "padding device:1" }, - { "toggle id:prefs.hoverlink" }, + //{ "toggle id:prefs.hoverlink" }, { "toggle device:2 id:prefs.hidetoolbarscroll" }, { "heading id:heading.prefs.sizing" }, { "input id:prefs.uiscale maxlen:8" }, -- cgit v1.2.3 From 1ef6a1abe1b0a5f6fc04f44a73ede0c47ca6885b Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 25 Dec 2021 07:54:05 +0200 Subject: GmDocument: Detect an invalid link IssueID #427 # Conflicts: # res/about/version.gmi --- src/gmdocument.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/gmdocument.c b/src/gmdocument.c index 0197ed99..fd13bc82 100644 --- a/src/gmdocument.c +++ b/src/gmdocument.c @@ -238,7 +238,12 @@ enum iGmLineType lineType_Rangecc(const iRangecc line) { return text_GmLineType; } if (startsWith_Rangecc(line, "=>")) { - return link_GmLineType; + iRangecc trim = line; + trim_Rangecc(&trim); + if (size_Range(&trim) > 2) { + return link_GmLineType; + } + return text_GmLineType; } if (startsWith_Rangecc(line, "###")) { return heading3_GmLineType; -- cgit v1.2.3 From 0f168910bc41ebc6ccfc2111b44a0f0a1efcb790 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 26 Dec 2021 06:44:09 +0200 Subject: Mobile: Tweaking link highlight on hover --- src/ui/documentwidget.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 1b5b9af2..0977da4a 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -1261,7 +1261,9 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { } #endif iBool isMobileHover = deviceType_App() != desktop_AppDeviceType && - (isPartOfHover || contains_PtrSet(d->view->invalidRuns, run)); + (isPartOfHover || contains_PtrSet(d->view->invalidRuns, run)) && + (~run->flags & decoration_GmRunFlag || run->flags & startOfLine_GmRunFlag + /* highlight link icon but not image captions */); /* While this is consistent, it's a bit excessive to indicate that an inlined image is open: the image itself is the indication. */ const iBool isInlineImageCaption = iFalse; -- cgit v1.2.3 From cf0b3924268ffe03d8c30dfa553deba79802e41f Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 26 Dec 2021 06:44:54 +0200 Subject: iOS: Bumped version and updated release notes --- CMakeLists.txt | 4 ++-- res/about/ios-version.gmi | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a5b76eb4..32f8afb2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,8 +25,8 @@ project (Lagrange set (COPYRIGHT_YEAR 2021) if (IOS) set (PROJECT_VERSION 1.10) - set (IOS_BUNDLE_VERSION 6) - set (IOS_BUILD_DATE "2021-12-17") + set (IOS_BUNDLE_VERSION 7) + set (IOS_BUILD_DATE "2021-12-26") endif () # Defaults that depend on environment. diff --git a/res/about/ios-version.gmi b/res/about/ios-version.gmi index 89e54e66..f7d88066 100644 --- a/res/about/ios-version.gmi +++ b/res/about/ios-version.gmi @@ -7,8 +7,10 @@ # Release notes ## 1.10 (7) +* Link context menu shows used identity and date of last visit in addition to the URL. +* Removed the "Show URL on hover" setting. URLs are shown in the link context menu. * Inline image metadata goes under the image instead of possibly overlapping the image caption text. -* Improved apperance of opened links highlighting. It no longer goes under the side elements on the page. +* Improved appearance of hover/open link highlighting. It no longer goes under the side elements on the page. * Gempub: Open books in 1:2 split mode instead of 1:1. * Fixed opening links in split view mode. Links would open on the wrong side. * Upgraded SDL to version 2.0.18. -- cgit v1.2.3 From 5e31f66313e2dc15fb9e75395504feb0992c3feb Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Mon, 27 Dec 2021 10:00:17 +0200 Subject: Reserved characters in URLs Making URL encoding a little less convoluted. Now when sending out a request, the URL is fully encoded except for reserved characters. In the internal representation, non-ASCII characters are in decoded form (i.e., IRI). This means that if the user enters a URL in the input field manually, its non-ASCII characters will be percent encoded as well. However, in this case the user is expected to manually escape all reserved characters because the input field can't tell the difference between what is intended to be a reserved separator and what isn't. For example, a server might expect &-separated fields, and if the user enters such fields manually in the URL field, they shouldn't be converted to %26. When forming a query URL in the input dialog, user-entered text is fully percent-encoded because in that case the input is just a generic text string. IssueID #410 --- src/app.c | 6 ------ src/gmrequest.c | 6 ++++-- src/gmutil.c | 30 ++++++++++++++++++++++++++---- src/gmutil.h | 2 ++ src/ui/inputwidget.c | 10 ++++++++-- 5 files changed, 40 insertions(+), 14 deletions(-) diff --git a/src/app.c b/src/app.c index 3b4c24f0..73cc35ee 100644 --- a/src/app.c +++ b/src/app.c @@ -2791,12 +2791,6 @@ iBool handleCommand_App(const char *cmd) { setRedirectCount_DocumentWidget(doc, redirectCount); setOrigin_DocumentWidget(doc, origin); showCollapsed_Widget(findWidget_App("document.progress"), iFalse); - if (prefs_App()->decodeUserVisibleURLs) { - urlDecodePath_String(url); - } - else { - urlEncodePath_String(url); - } setUrlFlags_DocumentWidget(doc, url, isHistory ? useCachedContentIfAvailable_DocumentWidgetSetUrlFlag : 0); /* Optionally, jump to a text in the document. This will only work if the document diff --git a/src/gmrequest.c b/src/gmrequest.c index a9c5919d..c23e8499 100644 --- a/src/gmrequest.c +++ b/src/gmrequest.c @@ -585,8 +585,10 @@ void setUrl_GmRequest(iGmRequest *d, const iString *url) { /* TODO: Gemini spec allows UTF-8 encoded URLs, but still need to percent-encode non-ASCII characters? Could be a server-side issue, e.g., if they're using a URL parser meant for the web. */ - urlEncodePath_String(&d->url); - urlEncodeSpaces_String(&d->url); + /* Encode everything except already-percent encoded characters. */ + iString *enc = urlEncodeExclude_String(&d->url, "%" URL_RESERVED_CHARS); + set_String(&d->url, enc); + delete_String(enc); d->identity = identityForUrl_GmCerts(d->certs, &d->url); } diff --git a/src/gmutil.c b/src/gmutil.c index 79462e41..98e4d4d6 100644 --- a/src/gmutil.c +++ b/src/gmutil.c @@ -330,6 +330,28 @@ void urlEncodePath_String(iString *d) { delete_String(encoded); } +void urlEncodeQuery_String(iString *d) { + iUrl url; + init_Url(&url, d); + if (isEmpty_Range(&url.query)) { + return; + } + iString encoded; + init_String(&encoded); + appendRange_String(&encoded, (iRangecc){ constBegin_String(d), url.query.start }); + iString query; + url.query.start++; /* omit the question mark */ + initRange_String(&query, url.query); + iString *encQuery = urlEncode_String(&query); /* fully encoded */ + appendCStr_String(&encoded, "?"); + append_String(&encoded, encQuery); + delete_String(encQuery); + deinit_String(&query); + appendRange_String(&encoded, (iRangecc){ url.query.end, constEnd_String(d) }); + set_String(d, &encoded); + deinit_String(&encoded); +} + iBool isKnownScheme_Rangecc(iRangecc scheme) { if (isKnownUrlScheme_Rangecc(scheme)) { return iTrue; @@ -667,20 +689,20 @@ const iString *canonicalUrl_String(const iString *d) { iString *canon = NULL; iUrl parts; init_Url(&parts, d); - /* Colons are in decoded form in the URL path. */ + /* Colons (0x3a) are in decoded form in the URL path. */ if (iStrStrN(parts.path.start, "%3A", size_Range(&parts.path)) || iStrStrN(parts.path.start, "%3a", size_Range(&parts.path))) { /* This is done separately to avoid the copy if %3A is not present; it's rare. */ canon = copy_String(d); urlDecodePath_String(canon); - iString *dec = maybeUrlDecodeExclude_String(canon, "%/?:;#&+= "); /* decode everything else in all parts */ + iString *dec = maybeUrlDecodeExclude_String(canon, "% " URL_RESERVED_CHARS); /* decode everything else in all parts */ if (dec) { set_String(canon, dec); delete_String(dec); } } else { - canon = maybeUrlDecodeExclude_String(d, "%/?:;#&+= "); + canon = maybeUrlDecodeExclude_String(d, "% " URL_RESERVED_CHARS); } /* `canon` may now be NULL if nothing was decoded. */ if (indexOfCStr_String(canon ? canon : d, " ") != iInvalidPos || @@ -689,7 +711,7 @@ const iString *canonicalUrl_String(const iString *d) { canon = copy_String(d); } urlEncodeSpaces_String(canon); - } + } return canon ? collect_String(canon) : d; } diff --git a/src/gmutil.h b/src/gmutil.h index 6d337eeb..15bb7b2e 100644 --- a/src/gmutil.h +++ b/src/gmutil.h @@ -100,6 +100,7 @@ iRegExp * newGemtextLink_RegExp (void); #define GEMINI_DEFAULT_PORT ((uint16_t) 1965) #define GEMINI_DEFAULT_PORT_CSTR "1965" +#define URL_RESERVED_CHARS ":/?#[]@!$&'()*+,;=" /* RFC 3986 */ struct Impl_Url { iRangecc scheme; @@ -131,6 +132,7 @@ const iString * urlFragmentStripped_String(const iString *); const iString * urlQueryStripped_String (const iString *); void urlDecodePath_String (iString *); void urlEncodePath_String (iString *); +void urlEncodeQuery_String (iString *); iString * makeFileUrl_String (const iString *localFilePath); const char * makeFileUrl_CStr (const char *localFilePath); iString * localFilePathFromUrl_String(const iString *); diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index b94e0c27..24983d69 100644 --- a/src/ui/inputwidget.c +++ b/src/ui/inputwidget.c @@ -1100,9 +1100,15 @@ static void updateBuffered_InputWidget_(iInputWidget *d) { void setText_InputWidget(iInputWidget *d, const iString *text) { if (!d) return; if (d->inFlags & isUrl_InputWidgetFlag) { - /* If user wants URLs encoded, also Punycode the domain. */ - if (!prefs_App()->decodeUserVisibleURLs) { + if (prefs_App()->decodeUserVisibleURLs) { iString *enc = collect_String(copy_String(text)); + urlDecodePath_String(enc); + text = enc; + } + else { + /* The user wants URLs encoded, also Punycode the domain. */ + iString *enc = collect_String(copy_String(text)); + urlEncodePath_String(enc); /* Prevent address bar spoofing (mentioned as IDN homograph attack in https://github.com/skyjake/lagrange/issues/73) */ punyEncodeUrlHost_String(enc); -- cgit v1.2.3 From 9d409b24cc23f6b58ca868375e65101ae75b593f Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 11 Dec 2021 19:27:17 +0200 Subject: Text: Fixed simple text renderer --- src/ui/text.c | 4 ++-- src/ui/text_simple.c | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ui/text.c b/src/ui/text.c index fc552db9..116c5eba 100644 --- a/src/ui/text.c +++ b/src/ui/text.c @@ -1882,7 +1882,7 @@ iInt2 tryAdvance_Text(int fontId, iRangecc text, int width, const char **endPos) } iInt2 tryAdvanceNoWrap_Text(int fontId, iRangecc text, int width, const char **endPos) { - if (width <= 1) { + if (width && width <= 1) { *endPos = text.start; return zero_I2(); } @@ -2081,7 +2081,7 @@ iTextMetrics draw_WrapText(iWrapText *d, int fontId, iInt2 pos, int color) { const int width = d->mode == word_WrapTextMode ? tryAdvance_Text(fontId, text, d->maxWidth, &endPos).x : tryAdvanceNoWrap_Text(fontId, text, d->maxWidth, &endPos).x; - notify_WrapText_(d, endPos, (iTextAttrib){ .colorId = color }, 0, width); + notify_WrapText_(d, endPos, (iTextAttrib){ .fgColorId = color }, 0, width); drawRange_Text(fontId, pos, color, (iRangecc){ text.start, endPos }); text.start = endPos; pos.y += lineHeight_Text(fontId); diff --git a/src/ui/text_simple.c b/src/ui/text_simple.c index 81fb94a5..8560c138 100644 --- a/src/ui/text_simple.c +++ b/src/ui/text_simple.c @@ -61,7 +61,7 @@ static iRect runSimple_Font_(iFont *d, const iRunArgs *args) { and other non-complex LTR scripts. Composed glyphs are not supported (must rely on text being in a pre-composed form). This algorithm is used if HarfBuzz is not available. */ const iInt2 orig = args->pos; - iTextAttrib attrib = { .colorId = args->color }; + iTextAttrib attrib = { .fgColorId = args->color }; iRect bounds = { orig, init_I2(0, d->height) }; float xpos = orig.x; float xposMax = xpos; @@ -118,8 +118,9 @@ static iRect runSimple_Font_(iFont *d, const iRunArgs *args) { if (match_RegExp(activeText_->ansiEscape, chPos, args->text.end - chPos, &m)) { if (mode & draw_RunMode && ~mode & permanentColorFlag_RunMode) { /* Change the color. */ - const iColor clr = - ansiForeground_Color(capturedRange_RegExpMatch(&m, 1), tmParagraph_ColorId); + iColor clr; + ansiColors_Color(capturedRange_RegExpMatch(&m, 1), tmParagraph_ColorId, + none_ColorId, &clr, NULL); SDL_SetTextureColorMod(activeText_->cache, clr.r, clr.g, clr.b); if (args->mode & fillBackground_RunMode) { SDL_SetRenderDrawColor(activeText_->render, clr.r, clr.g, clr.b, 0); -- cgit v1.2.3 From 50519588fabb404c7494b1e7934795af15cb8c00 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 23 Dec 2021 14:18:57 +0200 Subject: Android: Added missing header --- src/resources.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/resources.c b/src/resources.c index e3d92946..80af4b4b 100644 --- a/src/resources.c +++ b/src/resources.c @@ -25,6 +25,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include #include +#include + static iArchive *archive_; iBlock blobAbout_Resources; -- cgit v1.2.3 From 958791305cfde2404e604d7f1f66d5fe0502e565 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 26 Dec 2021 12:43:06 +0200 Subject: Android: Logging events --- src/app.c | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/app.c b/src/app.c index 73cc35ee..f5f7ae2b 100644 --- a/src/app.c +++ b/src/app.c @@ -71,6 +71,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #if defined (iPlatformAppleMobile) # include "ios.h" #endif +#if defined (iPlatformAndroidMobile) +#include +#endif #if defined (iPlatformMsys) # include "win32.h" #endif @@ -1690,13 +1693,20 @@ void postCommand_Root(iRoot *d, const char *command) { ev.user.data1 = strdup(command); ev.user.data2 = d; /* all events are root-specific */ SDL_PushEvent(&ev); + iWindow *win = get_Window(); +#if defined (iPlatformAndroid) + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "%s[command] {%d} %s", + app_.isLoadingPrefs ? "[Prefs] " : "", + (d == NULL || win == NULL ? 0 : d == win->roots[0] ? 1 : 2), + command); +#else if (app_.commandEcho) { - iWindow *win = get_Window(); printf("%s[command] {%d} %s\n", app_.isLoadingPrefs ? "[Prefs] " : "", (d == NULL || win == NULL ? 0 : d == win->roots[0] ? 1 : 2), command); fflush(stdout); } +#endif } void postCommandf_Root(iRoot *d, const char *command, ...) { -- cgit v1.2.3 From 001aeda1724eea630dd141c642bbdd61e2b9b69a Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Mon, 27 Dec 2021 13:23:45 +0200 Subject: Android: Keyboard height; other fixes Send a notification from the Java side when the software keyboard is shown. --- CMakeLists.txt | 2 +- src/app.c | 13 +++++++++++++ src/ui/documentwidget.c | 5 ----- src/ui/inputwidget.c | 6 +++--- src/ui/lookupwidget.c | 6 ++++-- src/ui/root.c | 5 ++++- src/ui/window.c | 7 +++++++ 7 files changed, 32 insertions(+), 12 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 32f8afb2..94b70ea0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,7 +31,7 @@ endif () # Defaults that depend on environment. set (DEFAULT_RESIZE_DRAW ON) -if (HAIKU) +if (HAIKU OR ANDROID) set (DEFAULT_RESIZE_DRAW OFF) endif () set (DEFAULT_IDLE_SLEEP OFF) diff --git a/src/app.c b/src/app.c index f5f7ae2b..27c8bef7 100644 --- a/src/app.c +++ b/src/app.c @@ -3432,8 +3432,21 @@ void closePopups_App(void) { } #if defined (iPlatformAndroidMobile) + float displayDensity_Android(void) { iApp *d = &app_; return toFloat_String(at_CommandLine(&d->args, 1)); } + +#include + +JNIEXPORT void JNICALL Java_fi_skyjake_lagrange_SDLActivity_postAppCommand( + JNIEnv* env, jclass jcls, + jstring command) +{ + const char *cmd = (*env)->GetStringUTFChars(env, command, NULL); + postCommand_Root(NULL, cmd); + (*env)->ReleaseStringUTFChars(env, command, cmd); +} + #endif diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 0977da4a..04257a1c 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -3100,11 +3100,6 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) { NULL); insertChildAfter_Widget(buttons, iClob(lineBreak), 0); } - else { -#if !defined (iPlatformAppleMobile) - lineBreak = new_LabelWidget("${dlg.input.linebreak}", "text.insert arg:10"); -#endif - } if (lineBreak) { setFlags_Widget(as_Widget(lineBreak), frameless_WidgetFlag, iTrue); setTextColor_LabelWidget(lineBreak, uiTextDim_ColorId); diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index 24983d69..2de16e6e 100644 --- a/src/ui/inputwidget.c +++ b/src/ui/inputwidget.c @@ -1269,8 +1269,8 @@ void end_InputWidget(iInputWidget *d, iBool accept) { if (!accept) { /* Overwrite the edited lines. */ splitToLines_(&d->oldText, &d->lines); - SDL_StopTextInput(); } + SDL_StopTextInput(); enableEditorKeysInMenus_(iTrue); d->inFlags &= ~isMarking_InputWidgetFlag; startOrStopCursorTimer_InputWidget_(d, iFalse); @@ -2153,11 +2153,11 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { updateAfterVisualOffsetChange_InputWidget_(d, w->root); } } - else if (d->sysCtrl && isCommand_UserEvent(ev, "menu.opened")) { +#endif + if (deviceType_App() != desktop_AppDeviceType && isCommand_UserEvent(ev, "menu.opened")) { setFocus_Widget(NULL); return iFalse; } -#endif if (isCommand_Widget(w, ev, "focus.gained")) { if (contentBounds_InputWidget_(d).size.x < minWidth_InputWidget_) { setFocus_Widget(NULL); diff --git a/src/ui/lookupwidget.c b/src/ui/lookupwidget.c index c654e3cf..f14170ad 100644 --- a/src/ui/lookupwidget.c +++ b/src/ui/lookupwidget.c @@ -676,17 +676,19 @@ static iBool processEvent_LookupWidget_(iLookupWidget *d, const SDL_Event *ev) { max_I2(zero_I2(), addX_I2(bottomLeft_Rect(bounds_Widget(url)), -extraWidth / 2)))); -#if defined(iPlatformAppleMobile) +#if defined(iPlatformMobile) /* TODO: Check this again. */ /* Adjust height based on keyboard size. */ { w->rect.size.y = bottom_Rect(visibleRect_Root(root)) - top_Rect(bounds_Widget(w)); +# if defined (iPlatformAppleMobile) if (deviceType_App() == phone_AppDeviceType) { - float l, r; + float l = 0.0f, r = 0.0f; safeAreaInsets_iOS(&l, NULL, &r, NULL); w->rect.size.x = size_Root(root).x - l - r; w->rect.pos.x = l; /* TODO: Need to use windowToLocal_Widget? */ } +# endif } #endif arrange_Widget(w); diff --git a/src/ui/root.c b/src/ui/root.c index 9e264993..36ac948e 100644 --- a/src/ui/root.c +++ b/src/ui/root.c @@ -512,7 +512,10 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) { else { addChild_Widget(root, iClob(sidebar)); setWidth_SidebarWidget(sidebar, (float) width_Widget(root) / (float) gap_UI); - const int midHeight = height_Widget(root) / 2;// + lineHeight_Text(uiLabelLarge_FontId); + int midHeight = height_Widget(root) / 2;// + lineHeight_Text(uiLabelLarge_FontId); +#if defined (iPlatformAndroidMobile) + midHeight += 2 * lineHeight_Text(uiLabelLarge_FontId); +#endif setMidHeight_SidebarWidget(sidebar, midHeight); setFixedSize_Widget(as_Widget(sidebar), init_I2(-1, midHeight)); setPos_Widget(as_Widget(sidebar), init_I2(0, height_Widget(root) - midHeight)); diff --git a/src/ui/window.c b/src/ui/window.c index d694146a..953d5ea4 100644 --- a/src/ui/window.c +++ b/src/ui/window.c @@ -1008,6 +1008,13 @@ iBool processEvent_Window(iWindow *d, const SDL_Event *ev) { postCommand_App("media.player.update"); /* in case a player needs updating */ return iTrue; } + if (event.type == SDL_USEREVENT && isCommand_UserEvent(ev, "window.sysframe") && mw) { + /* This command is sent on Android to update the keyboard height. */ + const char *cmd = command_UserEvent(ev); + setKeyboardHeight_MainWindow(mw, argLabel_Command(cmd, "fullheight") - + argLabel_Command(cmd, "bottom")); + return iTrue; + } if (processEvent_Touch(&event)) { return iTrue; } -- cgit v1.2.3 From 337ab6b40fe072bed91582a43e64aedda5c4ad40 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Mon, 27 Dec 2021 13:45:17 +0200 Subject: Text: Fixed color escapes in the simple text renderer --- src/ui/text_simple.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ui/text_simple.c b/src/ui/text_simple.c index 8560c138..71942cf1 100644 --- a/src/ui/text_simple.c +++ b/src/ui/text_simple.c @@ -118,8 +118,9 @@ static iRect runSimple_Font_(iFont *d, const iRunArgs *args) { if (match_RegExp(activeText_->ansiEscape, chPos, args->text.end - chPos, &m)) { if (mode & draw_RunMode && ~mode & permanentColorFlag_RunMode) { /* Change the color. */ - iColor clr; - ansiColors_Color(capturedRange_RegExpMatch(&m, 1), tmParagraph_ColorId, + iColor clr = get_Color(args->color); + ansiColors_Color(capturedRange_RegExpMatch(&m, 1), + activeText_->baseFgColorId, none_ColorId, &clr, NULL); SDL_SetTextureColorMod(activeText_->cache, clr.r, clr.g, clr.b); if (args->mode & fillBackground_RunMode) { -- cgit v1.2.3 From 666033098e74e044beb6d8b9e032e79b62d48c57 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Mon, 27 Dec 2021 13:45:36 +0200 Subject: Android: Replace toolbar Back button with Close Tab These should be user-configurable, though. --- src/ui/root.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/ui/root.c b/src/ui/root.c index 36ac948e..3e0124d8 100644 --- a/src/ui/root.c +++ b/src/ui/root.c @@ -1542,10 +1542,19 @@ void createUserInterface_Root(iRoot *d) { arrangeHeight_WidgetFlag | arrangeHorizontal_WidgetFlag | commandOnClick_WidgetFlag | drawBackgroundToBottom_WidgetFlag, iTrue); +#if defined (iPlatformAndroidMobile) + /* Android has a system-provided back button (or gesture?), or in the toolbar we can have + a different in the place of Back. */ + setId_Widget(addChildFlags_Widget(toolBar, + iClob(newLargeIcon_LabelWidget(close_Icon, "tabs.close")), + frameless_WidgetFlag), + "toolbar.close"); +#else setId_Widget(addChildFlags_Widget(toolBar, iClob(newLargeIcon_LabelWidget(backArrow_Icon, "navigate.back")), frameless_WidgetFlag), "toolbar.back"); +#endif setId_Widget(addChildFlags_Widget(toolBar, iClob(newLargeIcon_LabelWidget(forwardArrow_Icon, "navigate.forward")), frameless_WidgetFlag), -- cgit v1.2.3 From a0111b0560c96fcc25758d2384f6ee53171ccd69 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Mon, 27 Dec 2021 14:39:32 +0200 Subject: Android: Don't suggest to download fonts --- src/app.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.c b/src/app.c index 27c8bef7..b04b6388 100644 --- a/src/app.c +++ b/src/app.c @@ -420,7 +420,7 @@ static void loadPrefs_App_(iApp *d) { iRelease(f); /* Upgrade checks. */ if (cmp_Version(&upgradedFromAppVersion, &(iVersion){ 1, 8, 0 }) < 0) { -#if !defined (iPlatformAppleMobile) +#if !defined (iPlatformAppleMobile) && !defined (iPlatformAndroidMobile) /* When upgrading to v1.8.0, the old hardcoded font library is gone and that means UI strings may not have the right fonts available for the UI to remain usable. */ -- cgit v1.2.3 From 4481447226cf7b85231656d1798d5995c35933bb Mon Sep 17 00:00:00 2001 From: Emir Date: Sun, 26 Dec 2021 13:40:05 +0000 Subject: Translated using Weblate (Turkish) Currently translated at 100.0% (617 of 617 strings) Translation: Lagrange/User Interface Translate-URL: http://weblate.skyjake.fi/projects/lagrange/ui/tr/ --- po/tr.po | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/po/tr.po b/po/tr.po index c5fbbd0b..f317eb7d 100644 --- a/po/tr.po +++ b/po/tr.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" -"PO-Revision-Date: 2021-12-04 10:50+0000\n" +"PO-Revision-Date: 2021-12-27 13:50+0000\n" "Last-Translator: Emir \n" "Language-Team: Turkish \n" "Language: tr\n" @@ -1596,8 +1596,8 @@ msgstr "KB" #, c-format msgid "page.timestamp" msgstr "" -"Alınma tarihi ve zamanı: %I.%M %p,\n" -"%d %b %Y" +"Alındı: %d %b %Y,\n" +"%I.%M %p" msgid "feeds.entry.markread" msgstr "Okundu olarak imle" -- cgit v1.2.3 From 336b9d7272ed8b1a9dccee27dec20e3377ee0c74 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 28 Dec 2021 07:52:25 +0200 Subject: Viewing unsupported files in another app After receiving content of unsupported type, show a footer action button to open it in another app. IssueID #135 --- CMakeLists.txt | 1 + po/en.po | 3 ++ res/lang/cs.bin | Bin 31510 -> 31549 bytes res/lang/de.bin | Bin 30517 -> 30556 bytes res/lang/en.bin | Bin 26603 -> 26642 bytes res/lang/eo.bin | Bin 25567 -> 25606 bytes res/lang/es.bin | Bin 30341 -> 30380 bytes res/lang/es_MX.bin | Bin 27673 -> 27712 bytes res/lang/fi.bin | Bin 30174 -> 30213 bytes res/lang/fr.bin | Bin 31321 -> 31360 bytes res/lang/gl.bin | Bin 29526 -> 29565 bytes res/lang/hu.bin | Bin 31346 -> 31385 bytes res/lang/ia.bin | Bin 28673 -> 28712 bytes res/lang/ie.bin | Bin 29261 -> 29300 bytes res/lang/isv.bin | Bin 25324 -> 25363 bytes res/lang/pl.bin | Bin 29949 -> 29988 bytes res/lang/ru.bin | Bin 44709 -> 44748 bytes res/lang/sk.bin | Bin 25660 -> 25699 bytes res/lang/sr.bin | Bin 44135 -> 44174 bytes res/lang/tok.bin | Bin 27383 -> 27422 bytes res/lang/tr.bin | Bin 29567 -> 29606 bytes res/lang/uk.bin | Bin 44054 -> 44093 bytes res/lang/zh_Hans.bin | Bin 25568 -> 25607 bytes res/lang/zh_Hant.bin | Bin 25766 -> 25805 bytes src/app.c | 19 ++++++++----- src/app.h | 1 + src/ui/documentwidget.c | 74 +++++++++++++++++++++++++++++++++--------------- 27 files changed, 68 insertions(+), 30 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 94b70ea0..37afe9d2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -330,6 +330,7 @@ target_include_directories (app PUBLIC target_compile_options (app PUBLIC -Werror=implicit-function-declaration -Werror=incompatible-pointer-types + -Wno-deprecated-declarations ${SDL2_CFLAGS} -DSTB_VORBIS_NO_STDIO=1 -DSTB_VORBIS_NO_INTEGER_CONVERSION=1 diff --git a/po/en.po b/po/en.po index 04b6ac19..61d7d3f7 100644 --- a/po/en.po +++ b/po/en.po @@ -196,6 +196,9 @@ msgstr "Find on Page" msgid "macos.menu.find" msgstr "Find" +msgid "menu.open.external" +msgstr "Open in Another App" + # Used on iOS. "Files" refers to Apple's iOS app where you can pick an iCloud folder. msgid "menu.save.files" msgstr "Save to Files" diff --git a/res/lang/cs.bin b/res/lang/cs.bin index 97197051..ec75dc3a 100644 Binary files a/res/lang/cs.bin and b/res/lang/cs.bin differ diff --git a/res/lang/de.bin b/res/lang/de.bin index d2dceda6..96fd003f 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 942dfdc2..26e3c36a 100644 Binary files a/res/lang/en.bin and b/res/lang/en.bin differ diff --git a/res/lang/eo.bin b/res/lang/eo.bin index 513c3a0b..2df0e6b8 100644 Binary files a/res/lang/eo.bin and b/res/lang/eo.bin differ diff --git a/res/lang/es.bin b/res/lang/es.bin index 92037aea..be919a78 100644 Binary files a/res/lang/es.bin and b/res/lang/es.bin differ diff --git a/res/lang/es_MX.bin b/res/lang/es_MX.bin index 7b7e1219..d88b3386 100644 Binary files a/res/lang/es_MX.bin and b/res/lang/es_MX.bin differ diff --git a/res/lang/fi.bin b/res/lang/fi.bin index 243e9740..5dba618c 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 507cbb4c..830bbc72 100644 Binary files a/res/lang/fr.bin and b/res/lang/fr.bin differ diff --git a/res/lang/gl.bin b/res/lang/gl.bin index 447ed72d..4df0c57c 100644 Binary files a/res/lang/gl.bin and b/res/lang/gl.bin differ diff --git a/res/lang/hu.bin b/res/lang/hu.bin index c2a085ec..f4c826c8 100644 Binary files a/res/lang/hu.bin and b/res/lang/hu.bin differ diff --git a/res/lang/ia.bin b/res/lang/ia.bin index 5305304c..6a18bd7d 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 69ac42d4..84cc2cd7 100644 Binary files a/res/lang/ie.bin and b/res/lang/ie.bin differ diff --git a/res/lang/isv.bin b/res/lang/isv.bin index 26c23dc5..1a31a1f7 100644 Binary files a/res/lang/isv.bin and b/res/lang/isv.bin differ diff --git a/res/lang/pl.bin b/res/lang/pl.bin index 1ad99e5d..a003ce03 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 03092acf..807e032d 100644 Binary files a/res/lang/ru.bin and b/res/lang/ru.bin differ diff --git a/res/lang/sk.bin b/res/lang/sk.bin index 4d91e8a6..33c83c09 100644 Binary files a/res/lang/sk.bin and b/res/lang/sk.bin differ diff --git a/res/lang/sr.bin b/res/lang/sr.bin index 823818e2..7c4c8de1 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 50743783..601b07f3 100644 Binary files a/res/lang/tok.bin and b/res/lang/tok.bin differ diff --git a/res/lang/tr.bin b/res/lang/tr.bin index 543739cd..8cbaa556 100644 Binary files a/res/lang/tr.bin and b/res/lang/tr.bin differ diff --git a/res/lang/uk.bin b/res/lang/uk.bin index f866f86f..48adc59b 100644 Binary files a/res/lang/uk.bin and b/res/lang/uk.bin differ diff --git a/res/lang/zh_Hans.bin b/res/lang/zh_Hans.bin index 179d87db..759e618a 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 aad28410..2bfc817b 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 b04b6388..b0b36783 100644 --- a/src/app.c +++ b/src/app.c @@ -1021,7 +1021,7 @@ const iString *downloadDir_App(void) { return collect_String(cleaned_Path(&app_.prefs.strings[downloadDir_PrefsString])); } -const iString *downloadPathForUrl_App(const iString *url, const iString *mime) { +const iString *fileNameForUrl_App(const iString *url, const iString *mime) { /* Figure out a file name from the URL. */ iUrl parts; init_Url(&parts, url); @@ -1047,22 +1047,27 @@ const iString *downloadPathForUrl_App(const iString *url, const iString *mime) { } } if (startsWith_String(name, "~")) { - /* This would be interpreted as a reference to a home directory. */ + /* This might be interpreted as a reference to a home directory. */ remove_Block(&name->chars, 0, 1); } - iString *savePath = concat_Path(downloadDir_App(), name); - if (lastIndexOfCStr_String(savePath, ".") == iInvalidPos) { + if (lastIndexOfCStr_String(name, ".") == iInvalidPos) { + /* TODO: Needs the inverse of `mediaTypeFromFileExtension_String()`. */ /* No extension specified in URL. */ if (startsWith_String(mime, "text/gemini")) { - appendCStr_String(savePath, ".gmi"); + appendCStr_String(name, ".gmi"); } else if (startsWith_String(mime, "text/")) { - appendCStr_String(savePath, ".txt"); + appendCStr_String(name, ".txt"); } else if (startsWith_String(mime, "image/")) { - appendCStr_String(savePath, cstr_String(mime) + 6); + appendCStr_String(name, cstr_String(mime) + 6); } } + return name; +} + +const iString *downloadPathForUrl_App(const iString *url, const iString *mime) { + iString *savePath = concat_Path(downloadDir_App(), fileNameForUrl_App(url, mime)); if (fileExists_FileInfo(savePath)) { /* Make it unique. */ iDate now; diff --git a/src/app.h b/src/app.h index 50d3ac6b..0c336e65 100644 --- a/src/app.h +++ b/src/app.h @@ -110,6 +110,7 @@ enum iColorTheme colorTheme_App (void); const iString * schemeProxy_App (iRangecc scheme); iBool willUseProxy_App (const iRangecc scheme); const iString * searchQueryUrl_App (const iString *queryStringUnescaped); +const iString * fileNameForUrl_App (const iString *url, const iString *mime); const iString * downloadPathForUrl_App(const iString *url, const iString *mime); typedef void (*iTickerFunc)(iAny *); diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 04257a1c..3771dd6c 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -2260,6 +2260,11 @@ static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode 0, format_CStr("document.setmediatype mime:%s", mtype) }); } + pushBack_Array(&items, + &(iMenuItem){ export_Icon " ${menu.open.external}", + SDLK_RETURN, + KMOD_PRIMARY, + "document.save extview:1" }); pushBack_Array( &items, &(iMenuItem){ translateCStr_Lang(download_Icon " " saveToDownloads_Label), @@ -3337,9 +3342,8 @@ static iBool fetchNextUnfetchedImage_DocumentWidget_(iDocumentWidget *d) { return iFalse; } -static const iString *saveToDownloads_(const iString *url, const iString *mime, const iBlock *content, - iBool showDialog) { - const iString *savePath = downloadPathForUrl_App(url, mime); +static iBool saveToFile_(const iString *savePath, const iBlock *content, iBool showDialog) { + iBool ok = iFalse; /* Write the file. */ { iFile *f = new_File(savePath); if (open_File(f, writeOnly_FileMode)) { @@ -3351,21 +3355,21 @@ static const iString *saveToDownloads_(const iString *url, const iString *mime, exportDownloadedFile_iOS(savePath); #else if (showDialog) { - const iMenuItem items[2] = { - { "${dlg.save.opendownload}", 0, 0, - format_CStr("!open url:%s", cstrCollect_String(makeFileUrl_String(savePath))) }, - { "${dlg.message.ok}", 0, 0, "message.ok" }, - }; - makeMessage_Widget(uiHeading_ColorEscape "${heading.save}", - format_CStr("%s\n${dlg.save.size} %.3f %s", - cstr_String(path_File(f)), - isMega ? size / 1.0e6f : (size / 1.0e3f), - isMega ? "${mb}" : "${kb}"), - items, - iElemCount(items)); + const iMenuItem items[2] = { + { "${dlg.save.opendownload}", 0, 0, + format_CStr("!open url:%s", cstrCollect_String(makeFileUrl_String(savePath))) }, + { "${dlg.message.ok}", 0, 0, "message.ok" }, + }; + makeMessage_Widget(uiHeading_ColorEscape "${heading.save}", + format_CStr("%s\n${dlg.save.size} %.3f %s", + cstr_String(path_File(f)), + isMega ? size / 1.0e6f : (size / 1.0e3f), + isMega ? "${mb}" : "${kb}"), + items, + iElemCount(items)); } #endif - return savePath; + ok = iTrue; } else { makeSimpleMessage_Widget(uiTextCaution_ColorEscape "${heading.save.error}", @@ -3373,7 +3377,16 @@ static const iString *saveToDownloads_(const iString *url, const iString *mime, } iRelease(f); } - return collectNew_String(); + return ok; +} + +static const iString *saveToDownloads_(const iString *url, const iString *mime, const iBlock *content, + iBool showDialog) { + const iString *savePath = downloadPathForUrl_App(url, mime); + if (!saveToFile_(savePath, content, showDialog)) { + return collectNew_String(); + } + return savePath; } static void addAllLinks_(void *context, const iGmRun *run) { @@ -4111,12 +4124,27 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) "${dlg.save.incomplete}"); } else if (!isEmpty_Block(&d->sourceContent)) { - const iBool doOpen = argLabel_Command(cmd, "open"); - const iString *savePath = saveToDownloads_(d->mod.url, &d->sourceMime, - &d->sourceContent, !doOpen); - if (!isEmpty_String(savePath) && doOpen) { - postCommandf_Root( - w->root, "!open url:%s", cstrCollect_String(makeFileUrl_String(savePath))); + if (argLabel_Command(cmd, "extview")) { + iString *tmpPath = collectNewCStr_String(tmpnam(NULL)); + const iRangecc tmpDir = dirName_Path(tmpPath); + set_String( + tmpPath, + collect_String(concat_Path(collectNewRange_String(tmpDir), + fileNameForUrl_App(d->mod.url, &d->sourceMime)))); + if (saveToFile_(tmpPath, &d->sourceContent, iFalse)) { + /* TODO: Remember this temporary path and delete it when quitting the app. */ + postCommandf_Root(w->root, "!open default:1 url:%s", + cstrCollect_String(makeFileUrl_String(tmpPath))); + } + } + else { + const iBool doOpen = argLabel_Command(cmd, "open"); + const iString *savePath = saveToDownloads_(d->mod.url, &d->sourceMime, + &d->sourceContent, !doOpen); + if (!isEmpty_String(savePath) && doOpen) { + postCommandf_Root( + w->root, "!open url:%s", cstrCollect_String(makeFileUrl_String(savePath))); + } } } return iTrue; -- cgit v1.2.3 From e8f06bd0985ce2c9ac5ef02525672a426d559d18 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Wed, 29 Dec 2021 07:18:29 +0200 Subject: Inline download context menu; macOS: Show in Finder The inline downloads UI finally has some interactivity: left-clicking on a finished download opens it in the default viewer app, and right-clicking shows a context menu with relevant actions. --- po/en.po | 4 +++ res/lang/cs.bin | Bin 31549 -> 31582 bytes res/lang/de.bin | Bin 30556 -> 30589 bytes res/lang/en.bin | Bin 26642 -> 26675 bytes res/lang/eo.bin | Bin 25606 -> 25639 bytes res/lang/es.bin | Bin 30380 -> 30413 bytes res/lang/es_MX.bin | Bin 27712 -> 27745 bytes res/lang/fi.bin | Bin 30213 -> 30246 bytes res/lang/fr.bin | Bin 31360 -> 31393 bytes res/lang/gl.bin | Bin 29565 -> 29598 bytes res/lang/hu.bin | Bin 31385 -> 31418 bytes res/lang/ia.bin | Bin 28712 -> 28745 bytes res/lang/ie.bin | Bin 29300 -> 29333 bytes res/lang/isv.bin | Bin 25363 -> 25396 bytes res/lang/pl.bin | Bin 29988 -> 30021 bytes res/lang/ru.bin | Bin 44748 -> 44781 bytes res/lang/sk.bin | Bin 25699 -> 25732 bytes res/lang/sr.bin | Bin 44174 -> 44207 bytes res/lang/tok.bin | Bin 27422 -> 27455 bytes res/lang/tr.bin | Bin 29606 -> 29639 bytes res/lang/uk.bin | Bin 44093 -> 44126 bytes res/lang/zh_Hans.bin | Bin 25607 -> 25640 bytes res/lang/zh_Hant.bin | Bin 25805 -> 25838 bytes src/app.c | 68 ++++++++++++++++++++++++++++++------------------ src/app.h | 1 + src/gmcerts.c | 2 +- src/ui/certlistwidget.c | 10 ++++--- src/ui/documentwidget.c | 66 ++++++++++++++++++++++++++++++++-------------- src/ui/linkinfo.c | 2 +- src/ui/mediaui.c | 38 +++++++++++++++++++++++++++ 30 files changed, 142 insertions(+), 49 deletions(-) diff --git a/po/en.po b/po/en.po index 61d7d3f7..49f78eb8 100644 --- a/po/en.po +++ b/po/en.po @@ -854,6 +854,10 @@ msgstr "Bookmark Link…" msgid "link.download" msgstr "Download Linked File" +# Shows where a local file is using the Finder. +msgid "menu.reveal.macos" +msgstr "Show in Finder" + msgid "link.file.delete" msgstr "Delete File" diff --git a/res/lang/cs.bin b/res/lang/cs.bin index ec75dc3a..1c30a0a9 100644 Binary files a/res/lang/cs.bin and b/res/lang/cs.bin differ diff --git a/res/lang/de.bin b/res/lang/de.bin index 96fd003f..bf05a72e 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 26e3c36a..bd858ade 100644 Binary files a/res/lang/en.bin and b/res/lang/en.bin differ diff --git a/res/lang/eo.bin b/res/lang/eo.bin index 2df0e6b8..cc829562 100644 Binary files a/res/lang/eo.bin and b/res/lang/eo.bin differ diff --git a/res/lang/es.bin b/res/lang/es.bin index be919a78..f62291f1 100644 Binary files a/res/lang/es.bin and b/res/lang/es.bin differ diff --git a/res/lang/es_MX.bin b/res/lang/es_MX.bin index d88b3386..f6f88d6c 100644 Binary files a/res/lang/es_MX.bin and b/res/lang/es_MX.bin differ diff --git a/res/lang/fi.bin b/res/lang/fi.bin index 5dba618c..e69245be 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 830bbc72..7be665e5 100644 Binary files a/res/lang/fr.bin and b/res/lang/fr.bin differ diff --git a/res/lang/gl.bin b/res/lang/gl.bin index 4df0c57c..8c1fdf24 100644 Binary files a/res/lang/gl.bin and b/res/lang/gl.bin differ diff --git a/res/lang/hu.bin b/res/lang/hu.bin index f4c826c8..b06c8676 100644 Binary files a/res/lang/hu.bin and b/res/lang/hu.bin differ diff --git a/res/lang/ia.bin b/res/lang/ia.bin index 6a18bd7d..4c3b403d 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 84cc2cd7..5e431c29 100644 Binary files a/res/lang/ie.bin and b/res/lang/ie.bin differ diff --git a/res/lang/isv.bin b/res/lang/isv.bin index 1a31a1f7..54cc6774 100644 Binary files a/res/lang/isv.bin and b/res/lang/isv.bin differ diff --git a/res/lang/pl.bin b/res/lang/pl.bin index a003ce03..cf6a6b23 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 807e032d..87c82a7d 100644 Binary files a/res/lang/ru.bin and b/res/lang/ru.bin differ diff --git a/res/lang/sk.bin b/res/lang/sk.bin index 33c83c09..3ac6e18c 100644 Binary files a/res/lang/sk.bin and b/res/lang/sk.bin differ diff --git a/res/lang/sr.bin b/res/lang/sr.bin index 7c4c8de1..5c66460d 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 601b07f3..19b6d9df 100644 Binary files a/res/lang/tok.bin and b/res/lang/tok.bin differ diff --git a/res/lang/tr.bin b/res/lang/tr.bin index 8cbaa556..9a8babc9 100644 Binary files a/res/lang/tr.bin and b/res/lang/tr.bin differ diff --git a/res/lang/uk.bin b/res/lang/uk.bin index 48adc59b..ebcb11be 100644 Binary files a/res/lang/uk.bin and b/res/lang/uk.bin differ diff --git a/res/lang/zh_Hans.bin b/res/lang/zh_Hans.bin index 759e618a..ab4b338a 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 2bfc817b..f166156f 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 b0b36783..e5f9a41c 100644 --- a/src/app.c +++ b/src/app.c @@ -55,6 +55,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include #include #include +#include #include #include #include @@ -119,6 +120,7 @@ static const int idleThreshold_App_ = 1000; /* ms */ struct Impl_App { iCommandLine args; iString * execPath; + iStringSet * tempFilesPendingDeletion; iMimeHooks * mimehooks; iGmCerts * certs; iVisited * visited; @@ -735,6 +737,7 @@ static void init_App_(iApp *d, int argc, char **argv) { #endif d->isDarkSystemTheme = iTrue; /* will be updated by system later on, if supported */ d->isSuspended = iFalse; + d->tempFilesPendingDeletion = new_StringSet(); init_CommandLine(&d->args, argc, argv); /* Where was the app started from? We ask SDL first because the command line alone cannot be relied on (behavior differs depending on OS). */ { @@ -1005,8 +1008,13 @@ static void deinit_App(iApp *d) { #endif deinit_SortedArray(&d->tickers); deinit_Periodic(&d->periodic); - deinit_Lang(); + deinit_Lang(); iRecycle(); + /* Delete all temporary files created while running. */ + iConstForEach(StringSet, tmp, d->tempFilesPendingDeletion) { + remove(cstr_String(tmp.value)); + } + iRelease(d->tempFilesPendingDeletion); } const iString *execPath_App(void) { @@ -1082,6 +1090,17 @@ const iString *downloadPathForUrl_App(const iString *url, const iString *mime) { return collect_String(savePath); } +const iString *temporaryPathForUrl_App(const iString *url, const iString *mime) { + iApp *d = &app_; + iString *tmpPath = collectNewCStr_String(tmpnam(NULL)); + const iRangecc tmpDir = dirName_Path(tmpPath); + set_String( + tmpPath, + collect_String(concat_Path(collectNewRange_String(tmpDir), fileNameForUrl_App(url, mime)))); + insert_StringSet(d->tempFilesPendingDeletion, tmpPath); /* deleted in `deinit_App` */ + return tmpPath; +} + const iString *debugInfo_App(void) { extern char **environ; /* The environment variables. */ iApp *d = &app_; @@ -2687,7 +2706,9 @@ iBool handleCommand_App(const char *cmd) { } #endif else if (equal_Command(cmd, "downloads.open")) { - postCommandf_App("open url:%s", cstrCollect_String(makeFileUrl_String(downloadDir_App()))); + postCommandf_App("open newtab:%d url:%s", + argLabel_Command(cmd, "newtab"), + cstrCollect_String(makeFileUrl_String(downloadDir_App()))); return iTrue; } else if (equal_Command(cmd, "ca.file")) { @@ -2718,6 +2739,19 @@ iBool handleCommand_App(const char *cmd) { } return iTrue; } + else if (equal_Command(cmd, "reveal")) { + const iString *path = NULL; + if (hasLabel_Command(cmd, "path")) { + path = suffix_Command(cmd, "path"); + } + else if (hasLabel_Command(cmd, "url")) { + path = collect_String(localFilePathFromUrl_String(suffix_Command(cmd, "url"))); + } + if (path) { + revealPath_App(path); + } + return iTrue; + } else if (equal_Command(cmd, "open")) { const char *urlArg = suffixPtr_Command(cmd, "url"); if (!urlArg) { @@ -2726,9 +2760,8 @@ iBool handleCommand_App(const char *cmd) { if (findWidget_App("prefs")) { postCommand_App("prefs.dismiss"); } - iString *url = collectNewCStr_String(urlArg); - const iBool noProxy = argLabel_Command(cmd, "noproxy") != 0; - const iBool fromSidebar = argLabel_Command(cmd, "fromsidebar") != 0; + iString *url = collectNewCStr_String(urlArg); + const iBool noProxy = argLabel_Command(cmd, "noproxy") != 0; iUrl parts; init_Url(&parts, url); if (equal_Rangecc(parts.scheme, "about") && equal_Rangecc(parts.path, "command") && @@ -3354,26 +3387,11 @@ void openInDefaultBrowser_App(const iString *url) { void revealPath_App(const iString *path) { #if defined (iPlatformAppleDesktop) - const char *scriptPath = concatPath_CStr(dataDir_App_(), "revealfile.scpt"); - iFile *f = newCStr_File(scriptPath); - if (open_File(f, writeOnly_FileMode | text_FileMode)) { - /* AppleScript to select a specific file. */ - write_File(f, collect_Block(newCStr_Block("on run argv\n" - " tell application \"Finder\"\n" - " activate\n" - " reveal POSIX file (item 1 of argv) as text\n" - " end tell\n" - "end run\n"))); - close_File(f); - iProcess *proc = new_Process(); - setArguments_Process( - proc, - iClob(newStringsCStr_StringList( - "/usr/bin/osascript", scriptPath, cstr_String(path), NULL))); - start_Process(proc); - iRelease(proc); - } - iRelease(f); + iProcess *proc = new_Process(); + setArguments_Process( + proc, iClob(newStringsCStr_StringList("/usr/bin/open", "-R", cstr_String(path), NULL))); + start_Process(proc); + iRelease(proc); #elif defined (iPlatformLinux) || defined (iPlatformHaiku) iFileInfo *inf = iClob(new_FileInfo(path)); iRangecc target; diff --git a/src/app.h b/src/app.h index 0c336e65..d15e1f21 100644 --- a/src/app.h +++ b/src/app.h @@ -111,6 +111,7 @@ const iString * schemeProxy_App (iRangecc scheme); iBool willUseProxy_App (const iRangecc scheme); const iString * searchQueryUrl_App (const iString *queryStringUnescaped); const iString * fileNameForUrl_App (const iString *url, const iString *mime); +const iString * temporaryPathForUrl_App(const iString *url, const iString *mime); /* deleted before quitting */ const iString * downloadPathForUrl_App(const iString *url, const iString *mime); typedef void (*iTickerFunc)(iAny *); diff --git a/src/gmcerts.c b/src/gmcerts.c index 8f7bf181..7b05103b 100644 --- a/src/gmcerts.c +++ b/src/gmcerts.c @@ -654,7 +654,7 @@ void importIdentity_GmCerts(iGmCerts *d, iTlsCertificate *cert, const iString *n } static const char *certPath_GmCerts_(const iGmCerts *d, const iGmIdentity *identity) { - if (!(identity->flags & (temporary_GmIdentityFlag | imported_GmIdentityFlag))) { + if (!(identity->flags & temporary_GmIdentityFlag)) { const char *finger = cstrCollect_String(hexEncode_Block(&identity->fingerprint)); return concatPath_CStr(cstr_String(&d->saveDir), format_CStr("idents/%s", finger)); } diff --git a/src/ui/certlistwidget.c b/src/ui/certlistwidget.c index 31d8bac6..5a1c481b 100644 --- a/src/ui/certlistwidget.c +++ b/src/ui/certlistwidget.c @@ -97,17 +97,21 @@ static void updateContextMenu_CertListWidget_(iCertListWidget *d) { pushBack_Array(items, &(iMenuItem){ format_CStr("```%s", cstr_String(docUrl)) }); firstIndex = 1; } - pushBackN_Array(items, (iMenuItem[]){ + const iMenuItem ctxItems[] = { { person_Icon " ${ident.use}", 0, 0, "ident.use arg:1" }, { close_Icon " ${ident.stopuse}", 0, 0, "ident.use arg:0" }, { close_Icon " ${ident.stopuse.all}", 0, 0, "ident.use arg:0 clear:1" }, { "---", 0, 0, NULL }, { edit_Icon " ${menu.edit.notes}", 0, 0, "ident.edit" }, { "${ident.fingerprint}", 0, 0, "ident.fingerprint" }, +#if defined (iPlatformAppleDesktop) + { magnifyingGlass_Icon " ${menu.reveal.macos}", 0, 0, "ident.reveal" }, +#endif { export_Icon " ${ident.export}", 0, 0, "ident.export" }, { "---", 0, 0, NULL }, { delete_Icon " " uiTextCaution_ColorEscape "${ident.delete}", 0, 0, "ident.delete confirm:1" }, - }, 9); + }; + pushBackN_Array(items, ctxItems, iElemCount(ctxItems)); /* Used URLs. */ const iGmIdentity *ident = menuIdentity_CertListWidget_(d); if (ident) { @@ -244,7 +248,7 @@ static iBool processEvent_CertListWidget_(iCertListWidget *d, const SDL_Event *e if (ident) { const iString *crtPath = certificatePath_GmCerts(certs_App(), ident); if (crtPath) { - revealPath_App(crtPath); + postCommandf_App("reveal path:%s", cstr_String(crtPath)); } } return iTrue; diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 3771dd6c..9e5e6ea3 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -4125,16 +4125,16 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) } else if (!isEmpty_Block(&d->sourceContent)) { if (argLabel_Command(cmd, "extview")) { - iString *tmpPath = collectNewCStr_String(tmpnam(NULL)); - const iRangecc tmpDir = dirName_Path(tmpPath); - set_String( - tmpPath, - collect_String(concat_Path(collectNewRange_String(tmpDir), - fileNameForUrl_App(d->mod.url, &d->sourceMime)))); - if (saveToFile_(tmpPath, &d->sourceContent, iFalse)) { - /* TODO: Remember this temporary path and delete it when quitting the app. */ - postCommandf_Root(w->root, "!open default:1 url:%s", - cstrCollect_String(makeFileUrl_String(tmpPath))); + if (equalCase_Rangecc(urlScheme_String(d->mod.url), "file")) { + /* Already a file so just open it directly. */ + postCommandf_Root(w->root, "!open default:1 url:%s", cstr_String(d->mod.url)); + } + else { + const iString *tmpPath = temporaryPathForUrl_App(d->mod.url, &d->sourceMime); + if (saveToFile_(tmpPath, &d->sourceContent, iFalse)) { + postCommandf_Root(w->root, "!open default:1 url:%s", + cstrCollect_String(makeFileUrl_String(tmpPath))); + } } } else { @@ -4484,11 +4484,6 @@ static iBool processMediaEvents_DocumentWidget_(iDocumentWidget *d, const SDL_Ev ev->type != SDL_MOUSEMOTION) { return iFalse; } - if (ev->type == SDL_MOUSEBUTTONDOWN || ev->type == SDL_MOUSEBUTTONUP) { - if (ev->button.button != SDL_BUTTON_LEFT) { - return iFalse; - } - } if (d->grabbedPlayer) { /* Updated in the drag. */ return iFalse; @@ -4496,9 +4491,23 @@ static iBool processMediaEvents_DocumentWidget_(iDocumentWidget *d, const SDL_Ev const iInt2 mouse = init_I2(ev->button.x, ev->button.y); iConstForEach(PtrArray, i, &d->view.visibleMedia) { const iGmRun *run = i.ptr; + if (run->mediaType == download_MediaType) { + iDownloadUI ui; + init_DownloadUI(&ui, media_GmDocument(d->view.doc), mediaId_GmRun(run).id, + runRect_DocumentView_(&d->view, run)); + if (processEvent_DownloadUI(&ui, ev)) { + return iTrue; + } + continue; + } if (run->mediaType != audio_MediaType) { continue; } + if (ev->type == SDL_MOUSEBUTTONDOWN || ev->type == SDL_MOUSEBUTTONUP) { + if (ev->button.button != SDL_BUTTON_LEFT) { + return iFalse; + } + } /* TODO: move this to mediaui.c */ const iRect rect = runRect_DocumentView_(&d->view, run); iPlayer * plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run)); @@ -4842,6 +4851,9 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iFalse); return iTrue; } + if (processMediaEvents_DocumentWidget_(d, ev)) { + return iTrue; + } if (ev->type == SDL_MOUSEBUTTONDOWN) { if (ev->button.button == SDL_BUTTON_X1) { postCommand_Root(w->root, "navigate.back"); @@ -4923,6 +4935,23 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e if (deviceType_App() == phone_AppDeviceType) { removeN_Array(&items, size_Array(&items) - 2, iInvalidSize); } + if (equalCase_Rangecc(scheme, "file")) { + pushBack_Array(&items, &(iMenuItem){ "---" }); + pushBack_Array(&items, + &(iMenuItem){ export_Icon " ${menu.open.external}", + 0, + 0, + format_CStr("!open default:1 url:%s", + cstr_String(linkUrl)) }); +#if defined (iPlatformAppleDesktop) + pushBack_Array(&items, + &(iMenuItem){ "${menu.reveal.macos}", + 0, + 0, + format_CStr("!reveal url:%s", + cstr_String(linkUrl)) }); +#endif + } } else if (!willUseProxy_App(scheme)) { pushBack_Array( @@ -4959,7 +4988,8 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e cstr_String(linkUrl)) }, }, 3); - if (isNative && d->contextLink->mediaType != download_MediaType) { + if (isNative && d->contextLink->mediaType != download_MediaType && + !equalCase_Rangecc(scheme, "file")) { pushBackN_Array(&items, (iMenuItem[]){ { "---" }, { download_Icon " ${link.download}", 0, 0, "document.downloadlink" }, @@ -4979,6 +5009,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e } if (equalCase_Rangecc(scheme, "file")) { /* Local files may be deleted. */ + pushBack_Array(&items, &(iMenuItem){ "---" }); pushBack_Array( &items, &(iMenuItem){ delete_Icon " " uiTextCaution_ColorEscape @@ -5052,9 +5083,6 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e processContextMenuEvent_Widget(d->menu, ev, {}); } } - if (processMediaEvents_DocumentWidget_(d, ev)) { - return iTrue; - } if (processEvent_Banner(d->banner, ev)) { return iTrue; } diff --git a/src/ui/linkinfo.c b/src/ui/linkinfo.c index cb48c7ea..5102f9b3 100644 --- a/src/ui/linkinfo.c +++ b/src/ui/linkinfo.c @@ -92,7 +92,7 @@ void infoText_LinkInfo(const iGmDocument *doc, iGmLinkId linkId, iString *text_o appendRange_String(text_out, (iRangecc){ parts.path.start, constEnd_String(url) }); } else if (scheme != gemini_GmLinkScheme) { - appendCStr_String(text_out, globe_Icon " "); + appendCStr_String(text_out, scheme == file_GmLinkScheme ? "" : globe_Icon " "); append_String(text_out, url); } else { diff --git a/src/ui/mediaui.c b/src/ui/mediaui.c index 1c828194..2aec568f 100644 --- a/src/ui/mediaui.c +++ b/src/ui/mediaui.c @@ -238,6 +238,44 @@ void init_DownloadUI(iDownloadUI *d, const iMedia *media, uint16_t mediaId, iRec /*----------------------------------------------------------------------------------------------*/ iBool processEvent_DownloadUI(iDownloadUI *d, const SDL_Event *ev) { + if (ev->type == SDL_MOUSEBUTTONDOWN || ev->type == SDL_MOUSEBUTTONUP) { + const iInt2 mouse = init_I2(ev->button.x, ev->button.y); + if (!contains_Rect(d->bounds, mouse)) { + return iFalse; + } + float bytesPerSecond; + const iString *path; + iBool isFinished; + downloadStats_Media(d->media, (iMediaId){ download_MediaType, d->mediaId }, + &path, &bytesPerSecond, &isFinished); + if (isFinished) { + if (ev->button.button == SDL_BUTTON_RIGHT && ev->type == SDL_MOUSEBUTTONDOWN) { + const iMenuItem items[] = { + /* Items related to the file */ + { openTab_Icon " ${menu.opentab}", + 0, + 0, + format_CStr("!open newtab:1 url:%s", + cstrCollect_String(makeFileUrl_String(path))) }, +#if defined (iPlatformAppleDesktop) + { "${menu.reveal.macos}", + 0, + 0, + format_CStr("!reveal path:%s", cstr_String(path)) }, +#endif + { "---" }, + /* Generic items */ + { "${menu.downloads}", 0, 0, "downloads.open newtab:1" }, + }; + openMenu_Widget(makeMenu_Widget(get_Root()->widget, items, iElemCount(items)), + mouse); + return iTrue; + } + else if (ev->button.button == SDL_BUTTON_LEFT && ev->type == SDL_MOUSEBUTTONUP) { + postCommandf_App("open default:1 url:%s", cstrCollect_String(makeFileUrl_String(path))); + } + } + } return iFalse; } -- cgit v1.2.3 From c5f9a431f3b42c3a34578f5406b5e39a9eec9cd0 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Wed, 29 Dec 2021 19:43:48 +0200 Subject: iOS: Sharing file content and selected text --- po/en.po | 3 +++ res/lang/cs.bin | Bin 31582 -> 31599 bytes res/lang/de.bin | Bin 30589 -> 30606 bytes res/lang/en.bin | Bin 26675 -> 26692 bytes res/lang/eo.bin | Bin 25639 -> 25656 bytes res/lang/es.bin | Bin 30413 -> 30430 bytes res/lang/es_MX.bin | Bin 27745 -> 27762 bytes res/lang/fi.bin | Bin 30246 -> 30263 bytes res/lang/fr.bin | Bin 31393 -> 31410 bytes res/lang/gl.bin | Bin 29598 -> 29615 bytes res/lang/hu.bin | Bin 31418 -> 31435 bytes res/lang/ia.bin | Bin 28745 -> 28762 bytes res/lang/ie.bin | Bin 29333 -> 29350 bytes res/lang/isv.bin | Bin 25396 -> 25413 bytes res/lang/pl.bin | Bin 30021 -> 30038 bytes res/lang/ru.bin | Bin 44781 -> 44798 bytes res/lang/sk.bin | Bin 25732 -> 25749 bytes res/lang/sr.bin | Bin 44207 -> 44224 bytes res/lang/tok.bin | Bin 27455 -> 27472 bytes res/lang/tr.bin | Bin 29639 -> 29656 bytes res/lang/uk.bin | Bin 44126 -> 44143 bytes res/lang/zh_Hans.bin | Bin 25640 -> 25657 bytes res/lang/zh_Hant.bin | Bin 25838 -> 25855 bytes src/app.c | 28 +++++++++++++++++++--------- src/gmrequest.c | 2 ++ src/ios.h | 1 + src/ios.m | 15 ++++++++++++--- src/ui/documentwidget.c | 12 +++++++++++- src/ui/mediaui.c | 6 ++++++ 29 files changed, 54 insertions(+), 13 deletions(-) diff --git a/po/en.po b/po/en.po index 49f78eb8..425243cc 100644 --- a/po/en.po +++ b/po/en.po @@ -858,6 +858,9 @@ msgstr "Download Linked File" msgid "menu.reveal.macos" msgstr "Show in Finder" +msgid "menu.share" +msgstr "Share" + msgid "link.file.delete" msgstr "Delete File" diff --git a/res/lang/cs.bin b/res/lang/cs.bin index 1c30a0a9..47a5496f 100644 Binary files a/res/lang/cs.bin and b/res/lang/cs.bin differ diff --git a/res/lang/de.bin b/res/lang/de.bin index bf05a72e..b0967918 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 bd858ade..a918af83 100644 Binary files a/res/lang/en.bin and b/res/lang/en.bin differ diff --git a/res/lang/eo.bin b/res/lang/eo.bin index cc829562..5f5a056a 100644 Binary files a/res/lang/eo.bin and b/res/lang/eo.bin differ diff --git a/res/lang/es.bin b/res/lang/es.bin index f62291f1..91c1a9e6 100644 Binary files a/res/lang/es.bin and b/res/lang/es.bin differ diff --git a/res/lang/es_MX.bin b/res/lang/es_MX.bin index f6f88d6c..baf7e294 100644 Binary files a/res/lang/es_MX.bin and b/res/lang/es_MX.bin differ diff --git a/res/lang/fi.bin b/res/lang/fi.bin index e69245be..67ed5398 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 7be665e5..b4ecf798 100644 Binary files a/res/lang/fr.bin and b/res/lang/fr.bin differ diff --git a/res/lang/gl.bin b/res/lang/gl.bin index 8c1fdf24..005feca3 100644 Binary files a/res/lang/gl.bin and b/res/lang/gl.bin differ diff --git a/res/lang/hu.bin b/res/lang/hu.bin index b06c8676..4f9b6d8b 100644 Binary files a/res/lang/hu.bin and b/res/lang/hu.bin differ diff --git a/res/lang/ia.bin b/res/lang/ia.bin index 4c3b403d..d29bcee8 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 5e431c29..b648a56c 100644 Binary files a/res/lang/ie.bin and b/res/lang/ie.bin differ diff --git a/res/lang/isv.bin b/res/lang/isv.bin index 54cc6774..32b2cb39 100644 Binary files a/res/lang/isv.bin and b/res/lang/isv.bin differ diff --git a/res/lang/pl.bin b/res/lang/pl.bin index cf6a6b23..20fa1621 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 87c82a7d..6c2be568 100644 Binary files a/res/lang/ru.bin and b/res/lang/ru.bin differ diff --git a/res/lang/sk.bin b/res/lang/sk.bin index 3ac6e18c..0604ebab 100644 Binary files a/res/lang/sk.bin and b/res/lang/sk.bin differ diff --git a/res/lang/sr.bin b/res/lang/sr.bin index 5c66460d..38b56ffa 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 19b6d9df..c8097f29 100644 Binary files a/res/lang/tok.bin and b/res/lang/tok.bin differ diff --git a/res/lang/tr.bin b/res/lang/tr.bin index 9a8babc9..587774cc 100644 Binary files a/res/lang/tr.bin and b/res/lang/tr.bin differ diff --git a/res/lang/uk.bin b/res/lang/uk.bin index ebcb11be..18707608 100644 Binary files a/res/lang/uk.bin and b/res/lang/uk.bin differ diff --git a/res/lang/zh_Hans.bin b/res/lang/zh_Hans.bin index ab4b338a..39024cdc 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 f166156f..59c4dcea 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 e5f9a41c..bdc3a733 100644 --- a/src/app.c +++ b/src/app.c @@ -3364,25 +3364,32 @@ void openInDefaultBrowser_App(const iString *url) { return; } #endif -#if !defined (iPlatformAppleMobile) +#if defined (iPlatformAppleMobile) + if (equalCase_Rangecc(urlScheme_String(url), "file")) { + revealPath_App(collect_String(localFilePathFromUrl_String(url))); + } + return; +#endif iProcess *proc = new_Process(); - setArguments_Process(proc, + setArguments_Process(proc, iClob(newStringsCStr_StringList( #if defined (iPlatformAppleDesktop) - iClob(newStringsCStr_StringList("/usr/bin/env", "open", cstr_String(url), NULL)) + "/usr/bin/env", + "open", + cstr_String(url), #elif defined (iPlatformLinux) || defined (iPlatformOther) || defined (iPlatformHaiku) - iClob(newStringsCStr_StringList("/usr/bin/env", "xdg-open", cstr_String(url), NULL)) + "/usr/bin/env", + "xdg-open", + cstr_String(url), #elif defined (iPlatformMsys) - iClob(newStringsCStr_StringList( - concatPath_CStr(cstr_String(execPath_App()), "../urlopen.bat"), - cstr_String(url), - NULL)) + concatPath_CStr(cstr_String(execPath_App()), "../urlopen.bat"), + cstr_String(url), /* TODO: The prompt window is shown momentarily... */ #endif + NULL)) ); start_Process(proc); waitForFinished_Process(proc); /* TODO: test on Windows */ iRelease(proc); -#endif } void revealPath_App(const iString *path) { @@ -3392,6 +3399,9 @@ void revealPath_App(const iString *path) { proc, iClob(newStringsCStr_StringList("/usr/bin/open", "-R", cstr_String(path), NULL))); start_Process(proc); iRelease(proc); +#elif defined (iPlatformAppleMobile) + /* Use a share sheet. */ + openFileActivityView_iOS(path); #elif defined (iPlatformLinux) || defined (iPlatformHaiku) iFileInfo *inf = iClob(new_FileInfo(path)); iRangecc target; diff --git a/src/gmrequest.c b/src/gmrequest.c index c23e8499..3d5a4aef 100644 --- a/src/gmrequest.c +++ b/src/gmrequest.c @@ -694,9 +694,11 @@ void submit_GmRequest(iGmRequest *d) { setCStr_String(&resp->meta, "text/gemini"); iString *page = collectNew_String(); iString *parentDir = collectNewRange_String(dirName_Path(path)); +#if !defined (iPlatformMobile) appendFormat_String(page, "=> %s " upArrow_Icon " %s" iPathSeparator "\n\n", cstrCollect_String(makeFileUrl_String(parentDir)), cstr_String(parentDir)); +#endif appendFormat_String(page, "# %s\n", cstr_Rangecc(baseName_Path(path))); /* Make a directory index page. */ iPtrArray *sortedInfo = collectNew_PtrArray(); diff --git a/src/ios.h b/src/ios.h index bf742f1f..9860f7a2 100644 --- a/src/ios.h +++ b/src/ios.h @@ -39,6 +39,7 @@ void exportDownloadedFile_iOS(const iString *path); void pickFileForOpening_iOS (void); void pickFile_iOS (const char *command); /* ` path:%s` will be appended */ void openTextActivityView_iOS(const iString *text); +void openFileActivityView_iOS(const iString *path); iBool isPhone_iOS (void); void safeAreaInsets_iOS (float *left, float *top, float *right, float *bottom); diff --git a/src/ios.m b/src/ios.m index c9e843e4..69e09419 100644 --- a/src/ios.m +++ b/src/ios.m @@ -536,15 +536,24 @@ void pickFile_iOS(const char *command) { [viewController_(get_Window()) presentViewController:picker animated:YES completion:nil]; } -void openTextActivityView_iOS(const iString *text) { +static void openActivityView_(NSArray *activityItems) { UIActivityViewController *actView = [[UIActivityViewController alloc] - initWithActivityItems:@[ - [NSString stringWithUTF8String:cstr_String(text)]] + initWithActivityItems:activityItems applicationActivities:nil]; [viewController_(get_Window()) presentViewController:actView animated:YES completion:nil]; } +void openTextActivityView_iOS(const iString *text) { + openActivityView_(@[[NSString stringWithUTF8String:cstr_String(text)]]); +} + +void openFileActivityView_iOS(const iString *path) { + NSURL *url = [NSURL fileURLWithPath:[[NSString alloc] initWithCString:cstr_String(path) + encoding:NSUTF8StringEncoding]]; + openActivityView_(@[url]); +} + /*----------------------------------------------------------------------------------------------*/ enum iAVFAudioPlayerState { diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 9e5e6ea3..65de157b 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -3945,7 +3945,14 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) /* Full document. */ copied = copy_String(source_GmDocument(d->view.doc)); } - SDL_SetClipboardText(cstr_String(copied)); + if (argLabel_Command(cmd, "share")) { +#if defined (iPlatformAppleMobile) + openTextActivityView_iOS(copied); +#endif + } + else { + SDL_SetClipboardText(cstr_String(copied)); + } delete_String(copied); if (flags_Widget(w) & touchDrag_WidgetFlag) { postCommand_Widget(w, "document.select arg:0"); @@ -5227,6 +5234,9 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e } d->copyMenu = makeMenu_Widget(w, (iMenuItem[]){ { clipCopy_Icon " ${menu.copy}", 0, 0, "copy" }, +#if defined (iPlatformAppleMobile) + { export_Icon " ${menu.share}", 0, 0, "copy share:1" }, +#endif { "---" }, { close_Icon " ${menu.select.clear}", 0, 0, "document.select arg:0" }, }, 3); diff --git a/src/ui/mediaui.c b/src/ui/mediaui.c index 2aec568f..ab88c94a 100644 --- a/src/ui/mediaui.c +++ b/src/ui/mediaui.c @@ -262,6 +262,12 @@ iBool processEvent_DownloadUI(iDownloadUI *d, const SDL_Event *ev) { 0, 0, format_CStr("!reveal path:%s", cstr_String(path)) }, +#endif +#if defined (iPlatformAppleMobile) + { export_Icon " ${menu.share}", + 0, + 0, + format_CStr("!reveal path:%s", cstr_String(path)) }, #endif { "---" }, /* Generic items */ -- cgit v1.2.3 From 0e5047069b5782b39620af8a5fd31d57ba9809e5 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 30 Dec 2021 08:12:13 +0200 Subject: DocumentWidget: Update banner while loading The banner would sometimes appear only after the page had finished loading, because the site icon got lost when the GmDocument was switched. --- src/ui/banner.c | 1 - src/ui/documentwidget.c | 9 +++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ui/banner.c b/src/ui/banner.c index 2b0b852e..e8c2e6d2 100644 --- a/src/ui/banner.c +++ b/src/ui/banner.c @@ -76,7 +76,6 @@ static void updateHeight_Banner_(iBanner *d) { } const size_t numItems = size_Array(&d->items); if (numItems) { -// const int innerPad = gap_UI; iConstForEach(Array, i, &d->items) { const iBannerItem *item = i.value; d->rect.size.y += item->height; diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 65de157b..cbd32066 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -2129,12 +2129,17 @@ static void showOrHidePinningIndicator_DocumentWidget_(iDocumentWidget *d) { isPinned_DocumentWidget_(d)); } +static void updateBanner_DocumentWidget_(iDocumentWidget *d) { + setSite_Banner(d->banner, siteText_DocumentWidget_(d), siteIcon_GmDocument(d->view.doc)); +} + static void documentWasChanged_DocumentWidget_(iDocumentWidget *d) { iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iFalse); setFlags_Widget(as_Widget(d), touchDrag_WidgetFlag, iFalse); updateVisitedLinks_GmDocument(d->view.doc); documentRunsInvalidated_DocumentWidget_(d); updateWindowTitle_DocumentWidget_(d); + updateBanner_DocumentWidget_(d); updateVisible_DocumentView_(&d->view); d->view.drawBufs->flags |= updateSideBuf_DrawBufsFlag; invalidate_DocumentWidget_(d); @@ -2161,10 +2166,6 @@ static void replaceDocument_DocumentWidget_(iDocumentWidget *d, iGmDocument *new documentWasChanged_DocumentWidget_(d); } -static void updateBanner_DocumentWidget_(iDocumentWidget *d) { - setSite_Banner(d->banner, siteText_DocumentWidget_(d), siteIcon_GmDocument(d->view.doc)); -} - static void updateTheme_DocumentWidget_(iDocumentWidget *d) { if (document_App() != d || category_GmStatusCode(d->sourceStatus) == categoryInput_GmStatusCode) { return; -- cgit v1.2.3 From 084da73bd33bea50a811f72b15117be967a5f674 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 30 Dec 2021 09:25:33 +0200 Subject: DocumentWidget: Inlining "image/*" responses Image responses get inlined on gemtext pages. It would make sense to use this mechanism for all media since the MIME type is ultimately what matters in determining the appropriate presentation. The file extensions shouldn't matter. IssueID #373 --- src/app.c | 1 + src/history.c | 9 ++++++++ src/history.h | 1 + src/media.c | 11 +++++++++ src/media.h | 3 +++ src/ui/documentwidget.c | 60 ++++++++++++++++++++++++++++++++++++------------- 6 files changed, 69 insertions(+), 16 deletions(-) diff --git a/src/app.c b/src/app.c index bdc3a733..f10a4768 100644 --- a/src/app.c +++ b/src/app.c @@ -2828,6 +2828,7 @@ iBool handleCommand_App(const char *cmd) { const iBool isHistory = argLabel_Command(cmd, "history") != 0; int redirectCount = argLabel_Command(cmd, "redirect"); if (!isHistory) { + /* TODO: Shouldn't DocumentWidget manage history on its own? */ if (redirectCount) { replace_History(history, url); } diff --git a/src/history.c b/src/history.c index 837b38cb..56454009 100644 --- a/src/history.c +++ b/src/history.c @@ -310,6 +310,15 @@ void add_History(iHistory *d, const iString *url) { unlock_Mutex(d->mtx); } +void undo_History(iHistory *d) { + lock_Mutex(d->mtx); + if (!isEmpty_Array(&d->recent) || d->recentPos != 0) { + deinit_RecentUrl(back_Array(&d->recent)); + popBack_Array(&d->recent); + } + unlock_Mutex(d->mtx); +} + iRecentUrl *precedingLocked_History(iHistory *d) { /* NOTE: Manual lock and unlock are required when using this; returning an internal pointer. */ iBool ok = iFalse; diff --git a/src/history.h b/src/history.h index 383c132b..7959187d 100644 --- a/src/history.h +++ b/src/history.h @@ -61,6 +61,7 @@ void unlock_History (iHistory *); void clear_History (iHistory *); void add_History (iHistory *, const iString *url); +void undo_History (iHistory *); /* removes the most recent URL */ void replace_History (iHistory *, const iString *url); void setCachedResponse_History (iHistory *, const iGmResponse *response); void setCachedDocument_History (iHistory *, iGmDocument *doc); diff --git a/src/media.c b/src/media.c index c02090b0..4940c13e 100644 --- a/src/media.c +++ b/src/media.c @@ -629,6 +629,17 @@ void deinit_MediaRequest(iMediaRequest *d) { iRelease(d->req); } +iMediaRequest *newReused_MediaRequest(iDocumentWidget *doc, unsigned int linkId, + iGmRequest *request) { + iMediaRequest *d = new_Object(&Class_MediaRequest); + d->doc = doc; + d->linkId = linkId; + d->req = request; /* takes ownership */ + iConnect(GmRequest, d->req, updated, d, updated_MediaRequest_); + iConnect(GmRequest, d->req, finished, d, finished_MediaRequest_); + return d; +} + iDefineObjectConstructionArgs(MediaRequest, (iDocumentWidget *doc, unsigned int linkId, const iString *url, iBool enableFilters), diff --git a/src/media.h b/src/media.h index 3b329716..584c77eb 100644 --- a/src/media.h +++ b/src/media.h @@ -123,3 +123,6 @@ struct Impl_MediaRequest { iDeclareObjectConstructionArgs(MediaRequest, iDocumentWidget *doc, unsigned int linkId, const iString *url, iBool enableFilters) + +iMediaRequest * newReused_MediaRequest (iDocumentWidget *doc, unsigned int linkId, + iGmRequest *request); diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index cbd32066..f9efdd28 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -20,8 +20,8 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -/* TODO: This file is a little (!) too large. DocumentWidget could be split into - a couple of smaller objects. One for rendering the document, for instance. */ +/* TODO: Move DocumentView into a source file of its own. Consider cleaning up the network + request handling. */ #include "documentwidget.h" @@ -297,6 +297,7 @@ struct Impl_DocumentWidget { /* Network request: */ enum iRequestState state; iGmRequest * request; + iGmLinkId requestLinkId; /* ID of the link that initiated the current request */ iAtomicInt isRequestUpdated; /* request has new content, need to parse it */ int certFlags; iBlock * certFingerprint; @@ -1079,9 +1080,9 @@ static size_t visibleLinkOrdinal_DocumentView_(const iDocumentView *d, iGmLinkId } static void documentRunsInvalidated_DocumentWidget_(iDocumentWidget *d) { - d->foundMark = iNullRange; - d->selectMark = iNullRange; - d->contextLink = NULL; + d->foundMark = iNullRange; + d->selectMark = iNullRange; + d->contextLink = NULL; documentRunsInvalidated_DocumentView_(&d->view); } @@ -2136,6 +2137,7 @@ static void updateBanner_DocumentWidget_(iDocumentWidget *d) { static void documentWasChanged_DocumentWidget_(iDocumentWidget *d) { iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iFalse); setFlags_Widget(as_Widget(d), touchDrag_WidgetFlag, iFalse); + d->requestLinkId = 0; updateVisitedLinks_GmDocument(d->view.doc); documentRunsInvalidated_DocumentWidget_(d); updateWindowTitle_DocumentWidget_(d); @@ -2746,11 +2748,8 @@ static void fetch_DocumentWidget_(iDocumentWidget *d) { "document.request.started doc:%p url:%s", d, cstr_String(d->mod.url)); - clear_ObjectList(d->media); - d->certFlags = 0; setLinkNumberMode_DocumentWidget_(d, iFalse); d->flags &= ~drawDownloadCounter_DocumentWidgetFlag; - d->flags &= ~fromCache_DocumentWidgetFlag; d->state = fetching_RequestState; set_Atomic(&d->isRequestUpdated, iFalse); d->request = new_GmRequest(certs_App()); @@ -3043,6 +3042,14 @@ static const char *humanReadableStatusCode_(enum iGmStatusCode code) { return format_CStr("%d ", code); } +static void setUrl_DocumentWidget_(iDocumentWidget *d, const iString *url) { + url = canonicalUrl_String(url); + if (!equal_String(d->mod.url, url)) { + d->flags |= urlChanged_DocumentWidgetFlag; + set_String(d->mod.url, url); + } +} + static void checkResponse_DocumentWidget_(iDocumentWidget *d) { if (!d->request) { return; @@ -3053,8 +3060,35 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) { } iGmResponse *resp = lockResponse_GmRequest(d->request); if (d->state == fetching_RequestState) { + /* Under certain conditions, inline any image response into the current document. */ + if (d->requestLinkId && + isSuccess_GmStatusCode(d->sourceStatus) && + startsWithCase_String(&d->sourceMime, "text/gemini") && + isSuccess_GmStatusCode(statusCode) && + startsWithCase_String(&resp->meta, "image/")) { + /* This request is turned into a new media request in the current document. */ + iDisconnect(GmRequest, d->request, updated, d, requestUpdated_DocumentWidget_); + iDisconnect(GmRequest, d->request, finished, d, requestFinished_DocumentWidget_); + iMediaRequest *mr = newReused_MediaRequest(d, d->requestLinkId, d->request); + unlockResponse_GmRequest(d->request); + d->request = NULL; /* ownership moved */ + postCommand_Widget(d, "document.request.cancelled doc:%p", d); + pushBack_ObjectList(d->media, mr); + iRelease(mr); + /* Reset the fetch state, returning to the originating page. */ + d->state = ready_RequestState; + if (equal_String(&mostRecentUrl_History(d->mod.history)->url, url_GmRequest(mr->req))) { + undo_History(d->mod.history); + } + setUrl_DocumentWidget_(d, url_GmDocument(d->view.doc)); + updateFetchProgress_DocumentWidget_(d); + postCommand_Widget(d, "media.updated link:%u request:%p", d->requestLinkId, mr); + return; + } + /* Get ready for the incoming new document. */ d->state = receivedPartialResponse_RequestState; d->flags &= ~fromCache_DocumentWidgetFlag; + clear_ObjectList(d->media); updateTrust_DocumentWidget_(d, resp); if (isSuccess_GmStatusCode(statusCode)) { clear_Banner(d->banner); @@ -3441,14 +3475,6 @@ static iWidget *swipeParent_DocumentWidget_(iDocumentWidget *d) { return findChild_Widget(as_Widget(d)->root->widget, "doctabs"); } -static void setUrl_DocumentWidget_(iDocumentWidget *d, const iString *url) { - url = canonicalUrl_String(url); - if (!equal_String(d->mod.url, url)) { - d->flags |= urlChanged_DocumentWidgetFlag; - set_String(d->mod.url, url); - } -} - static void setupSwipeOverlay_DocumentWidget_(iDocumentWidget *d, iWidget *overlay) { iWidget *w = as_Widget(d); iWidget *swipeParent = swipeParent_DocumentWidget_(d); @@ -4598,6 +4624,7 @@ static void interactingWithLink_DocumentWidget_(iDocumentWidget *d, iGmLinkId id clear_String(&d->linePrecedingLink); return; } + d->requestLinkId = id; const char *start = range_String(source_GmDocument(d->view.doc)).start; /* Find the preceding line. This is offered as a prefill option for a possible input query. */ while (loc.start > start && *loc.start != '\n') { @@ -5526,6 +5553,7 @@ void init_DocumentWidget(iDocumentWidget *d) { d->state = blank_RequestState; d->titleUser = new_String(); d->request = NULL; + d->requestLinkId = 0; d->isRequestUpdated = iFalse; d->media = new_ObjectList(); d->banner = new_Banner(); -- cgit v1.2.3 From e36bed146728f93d5be8a9b84d8756f0b5ccaada Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 30 Dec 2021 13:27:03 +0200 Subject: Added "Edit Page with Titan" A convenient way to copy the current page's source and open the Titan upload dialog with it prefilled. IssueID #415 --- po/en.po | 10 ++++++++-- res/lang/cs.bin | Bin 31599 -> 31683 bytes res/lang/de.bin | Bin 30606 -> 30690 bytes res/lang/en.bin | Bin 26692 -> 26766 bytes res/lang/eo.bin | Bin 25656 -> 25730 bytes res/lang/es.bin | Bin 30430 -> 30514 bytes res/lang/es_MX.bin | Bin 27762 -> 27836 bytes res/lang/fi.bin | Bin 30263 -> 30347 bytes res/lang/fr.bin | Bin 31410 -> 31494 bytes res/lang/gl.bin | Bin 29615 -> 29699 bytes res/lang/hu.bin | Bin 31435 -> 31519 bytes res/lang/ia.bin | Bin 28762 -> 28836 bytes res/lang/ie.bin | Bin 29350 -> 29434 bytes res/lang/isv.bin | Bin 25413 -> 25487 bytes res/lang/pl.bin | Bin 30038 -> 30122 bytes res/lang/ru.bin | Bin 44798 -> 44882 bytes res/lang/sk.bin | Bin 25749 -> 25823 bytes res/lang/sr.bin | Bin 44224 -> 44308 bytes res/lang/tok.bin | Bin 27472 -> 27556 bytes res/lang/tr.bin | Bin 29656 -> 29740 bytes res/lang/uk.bin | Bin 44143 -> 44227 bytes res/lang/zh_Hans.bin | Bin 25657 -> 25731 bytes res/lang/zh_Hant.bin | Bin 25855 -> 25939 bytes src/ui/documentwidget.c | 7 +++++++ src/ui/keys.c | 1 + src/ui/root.c | 1 + src/ui/uploadwidget.c | 4 ++++ src/ui/uploadwidget.h | 1 + 28 files changed, 22 insertions(+), 2 deletions(-) diff --git a/po/en.po b/po/en.po index 425243cc..a4b83dcc 100644 --- a/po/en.po +++ b/po/en.po @@ -1011,7 +1011,10 @@ msgid "heading.lookup.other" msgstr "OTHER" msgid "menu.page.upload" -msgstr "Upload Page with Titan…" +msgstr "Upload with Titan…" + +msgid "menu.page.upload.edit" +msgstr "Edit Page with Titan…" msgid "heading.upload" msgstr "Upload with Titan" @@ -1768,7 +1771,10 @@ msgid "keys.hoverurl" msgstr "Toggle show URL on hover" msgid "keys.upload" -msgstr "Upload page with Titan" +msgstr "Upload with Titan" + +msgid "keys.upload.edit" +msgstr "Edit Page with Titan" msgid "error.badstatus" msgstr "Unknown Status Code" diff --git a/res/lang/cs.bin b/res/lang/cs.bin index 47a5496f..bc34d7e8 100644 Binary files a/res/lang/cs.bin and b/res/lang/cs.bin differ diff --git a/res/lang/de.bin b/res/lang/de.bin index b0967918..5fb9eb03 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 a918af83..114fa684 100644 Binary files a/res/lang/en.bin and b/res/lang/en.bin differ diff --git a/res/lang/eo.bin b/res/lang/eo.bin index 5f5a056a..3d16450c 100644 Binary files a/res/lang/eo.bin and b/res/lang/eo.bin differ diff --git a/res/lang/es.bin b/res/lang/es.bin index 91c1a9e6..fe6bdbb5 100644 Binary files a/res/lang/es.bin and b/res/lang/es.bin differ diff --git a/res/lang/es_MX.bin b/res/lang/es_MX.bin index baf7e294..c96da0b0 100644 Binary files a/res/lang/es_MX.bin and b/res/lang/es_MX.bin differ diff --git a/res/lang/fi.bin b/res/lang/fi.bin index 67ed5398..d06dfc9c 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 b4ecf798..4cda45d8 100644 Binary files a/res/lang/fr.bin and b/res/lang/fr.bin differ diff --git a/res/lang/gl.bin b/res/lang/gl.bin index 005feca3..7ffb40ec 100644 Binary files a/res/lang/gl.bin and b/res/lang/gl.bin differ diff --git a/res/lang/hu.bin b/res/lang/hu.bin index 4f9b6d8b..e698c7ac 100644 Binary files a/res/lang/hu.bin and b/res/lang/hu.bin differ diff --git a/res/lang/ia.bin b/res/lang/ia.bin index d29bcee8..f951f0c5 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 b648a56c..c4671e06 100644 Binary files a/res/lang/ie.bin and b/res/lang/ie.bin differ diff --git a/res/lang/isv.bin b/res/lang/isv.bin index 32b2cb39..2788dc3f 100644 Binary files a/res/lang/isv.bin and b/res/lang/isv.bin differ diff --git a/res/lang/pl.bin b/res/lang/pl.bin index 20fa1621..29260245 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 6c2be568..712d8a01 100644 Binary files a/res/lang/ru.bin and b/res/lang/ru.bin differ diff --git a/res/lang/sk.bin b/res/lang/sk.bin index 0604ebab..79b1208f 100644 Binary files a/res/lang/sk.bin and b/res/lang/sk.bin differ diff --git a/res/lang/sr.bin b/res/lang/sr.bin index 38b56ffa..79e76d20 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 c8097f29..31f9536e 100644 Binary files a/res/lang/tok.bin and b/res/lang/tok.bin differ diff --git a/res/lang/tr.bin b/res/lang/tr.bin index 587774cc..758dba68 100644 Binary files a/res/lang/tr.bin and b/res/lang/tr.bin differ diff --git a/res/lang/uk.bin b/res/lang/uk.bin index 18707608..6aedfe5f 100644 Binary files a/res/lang/uk.bin and b/res/lang/uk.bin differ diff --git a/res/lang/zh_Hans.bin b/res/lang/zh_Hans.bin index 39024cdc..036ecca3 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 59c4dcea..d2aa482d 100644 Binary files a/res/lang/zh_Hant.bin and b/res/lang/zh_Hant.bin differ diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index f9efdd28..e63e4636 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -4116,6 +4116,12 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) setResponseViewer_UploadWidget(upload, d); addChild_Widget(get_Root()->widget, iClob(upload)); setupSheetTransition_Mobile(as_Widget(upload), iTrue); + if (argLabel_Command(cmd, "copy") && isUtf8_Rangecc(range_Block(&d->sourceContent))) { + iString text; + initBlock_String(&text, &d->sourceContent); + setText_UploadWidget(upload, &text); + deinit_String(&text); + } postRefresh_App(); } return iTrue; @@ -5080,6 +5086,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e { book_Icon " ${menu.page.import}", 0, 0, "bookmark.links confirm:1" }, { globe_Icon " ${menu.page.translate}", 0, 0, "document.translate" }, { upload_Icon " ${menu.page.upload}", 0, 0, "document.upload" }, + { "${menu.page.upload.edit}", 0, 0, "document.upload copy:1" }, { "---" }, { "${menu.page.copyurl}", 0, 0, "document.copylink" } }, 16); diff --git a/src/ui/keys.c b/src/ui/keys.c index 30072572..d4d9320e 100644 --- a/src/ui/keys.c +++ b/src/ui/keys.c @@ -240,6 +240,7 @@ static const struct { int id; iMenuItem bind; int flags; } defaultBindings_[] = { 100,{ "${keys.hoverurl}", '/', KMOD_PRIMARY, "prefs.hoverlink.toggle" }, 0 }, { 110,{ "${menu.save.downloads}", SDLK_s, KMOD_PRIMARY, "document.save" }, 0 }, { 120,{ "${keys.upload}", SDLK_u, KMOD_PRIMARY, "document.upload" }, 0 }, + { 121,{ "${keys.upload.edit}", SDLK_e, KMOD_PRIMARY, "document.upload copy:1" }, 0 }, /* The following cannot currently be changed (built-in duplicates). */ #if defined (iPlatformApple) { 1002, { NULL, SDLK_LEFTBRACKET, KMOD_PRIMARY, "navigate.back" }, 0 }, diff --git a/src/ui/root.c b/src/ui/root.c index 3e0124d8..bc2bc0fb 100644 --- a/src/ui/root.c +++ b/src/ui/root.c @@ -1402,6 +1402,7 @@ void createUserInterface_Root(iRoot *d) { { book_Icon " ${menu.page.import}", 0, 0, "bookmark.links confirm:1" }, { globe_Icon " ${menu.page.translate}", 0, 0, "document.translate" }, { upload_Icon " ${menu.page.upload}", 0, 0, "document.upload" }, + { "${menu.page.upload.edit}", 0, 0, "document.upload copy:1" }, { "---" }, { "${menu.page.copyurl}", 0, 0, "document.copylink" }, { "${menu.page.copysource}", 'c', KMOD_PRIMARY, "copy" }, diff --git a/src/ui/uploadwidget.c b/src/ui/uploadwidget.c index 28a35668..ae777a68 100644 --- a/src/ui/uploadwidget.c +++ b/src/ui/uploadwidget.c @@ -406,6 +406,10 @@ void setResponseViewer_UploadWidget(iUploadWidget *d, iDocumentWidget *doc) { d->viewer = doc; } +void setText_UploadWidget(iUploadWidget *d, const iString *text) { + setText_InputWidget(findChild_Widget(as_Widget(d), "upload.text"), text); +} + static iWidget *acceptButton_UploadWidget_(iUploadWidget *d) { return lastChild_Widget(findChild_Widget(as_Widget(d), "dialogbuttons")); } diff --git a/src/ui/uploadwidget.h b/src/ui/uploadwidget.h index 5a7de45e..1cc1f193 100644 --- a/src/ui/uploadwidget.h +++ b/src/ui/uploadwidget.h @@ -31,3 +31,4 @@ iDeclareType(DocumentWidget) void setUrl_UploadWidget (iUploadWidget *, const iString *url); void setResponseViewer_UploadWidget (iUploadWidget *, iDocumentWidget *doc); +void setText_UploadWidget (iUploadWidget *, const iString *text); -- cgit v1.2.3 From 727637718be197a77bed35a907de75426f11cfa2 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 30 Dec 2021 16:51:38 +0200 Subject: Android: Updating for latest SDL and revised Gradle project --- CMakeLists.txt | 4 ++-- src/app.c | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 37afe9d2..e5de32f7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -315,7 +315,7 @@ if (NOT ANDROID) add_executable (app ${SOURCES} ${RESOURCES} ${EMB_FONTS}) else () # The whole app becomes one shared library, based on this static one. - add_library (app STATIC ${SOURCES}) + add_library (app SHARED ${SOURCES}) endif () set_property (TARGET app PROPERTY C_STANDARD 11) if (TARGET ext-deps) @@ -478,7 +478,7 @@ elseif (HAIKU) LAGRANGE_EMB_BIN="${CMAKE_INSTALL_PREFIX}/resources.lgr") install (FILES ${EMB_BIN} DESTINATION .) elseif (ANDROID) - file (COPY ${EMB_BIN} DESTINATION ${CMAKE_SOURCE_DIR}/../app/src/main/assets) + file (COPY ${EMB_BIN} DESTINATION ${CMAKE_SOURCE_DIR}/../src/main/assets) elseif (UNIX AND NOT APPLE) include (GNUInstallDirs) set_target_properties (app PROPERTIES diff --git a/src/app.c b/src/app.c index f10a4768..0ed6f687 100644 --- a/src/app.c +++ b/src/app.c @@ -124,7 +124,7 @@ struct Impl_App { iMimeHooks * mimehooks; iGmCerts * certs; iVisited * visited; - iBookmarks * bookmarks; + iBookmarks * bookmarks; iMainWindow *window; iPtrArray popupWindows; iSortedArray tickers; /* per-frame callbacks, used for animations */ @@ -739,7 +739,7 @@ static void init_App_(iApp *d, int argc, char **argv) { d->isSuspended = iFalse; d->tempFilesPendingDeletion = new_StringSet(); init_CommandLine(&d->args, argc, argv); - /* Where was the app started from? We ask SDL first because the command line alone + /* Where was the app started from? We ask SDL first because the command line alone cannot be relied on (behavior differs depending on OS). */ { char *exec = SDL_GetBasePath(); if (exec) { -- cgit v1.2.3 From 315414689631c797449145b95f9d0f6136d46586 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 31 Dec 2021 15:06:11 +0200 Subject: Android: Disable SSE check, enable HarfBuzz and FriBidi --- Depends-Android.cmake | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Depends-Android.cmake b/Depends-Android.cmake index 74635620..3bc57f32 100644 --- a/Depends-Android.cmake +++ b/Depends-Android.cmake @@ -21,11 +21,20 @@ set (TFDN_STATIC_LIBRARY ON CACHE BOOL "") set (TFDN_ENABLE_INSTALL OFF CACHE BOOL "") set (TFDN_ENABLE_TESTS OFF CACHE BOOL "") set (TFDN_ENABLE_WEBREQUEST OFF CACHE BOOL "") -add_subdirectory (lib/the_Foundation) +set (TFDN_ENABLE_SSE41 OFF CACHE BOOL "") +add_subdirectory (lib/the_Foundation) add_library (the_Foundation::the_Foundation ALIAS the_Foundation) if (NOT OPENSSL_FOUND) message (FATAL_ERROR "Lagrange requires OpenSSL for TLS. Please check if pkg-config can find 'openssl'.") endif () if (NOT ZLIB_FOUND) message (FATAL_ERROR "Lagrange requires zlib for reading compressed archives. Please check if pkg-config can find 'zlib'.") -endif () \ No newline at end of file +endif () + +set (FRIBIDI_FOUND YES) +set (FRIBIDI_LIBRARIES ${ANDROID_DIR}/fribidi-android/${ANDROID_ABI}/lib/libfribidi.a) +set (FRIBIDI_INCLUDE_DIRS ${ANDROID_DIR}/fribidi-android/${ANDROID_ABI}/include) + +set (HARFBUZZ_FOUND YES) +set (HARFBUZZ_LIBRARIES ${ANDROID_DIR}/harfbuzz-android/${ANDROID_ABI}/lib/libharfbuzz.a) +set (HARFBUZZ_INCLUDE_DIRS ${ANDROID_DIR}/harfbuzz-android/${ANDROID_ABI}/include/harfbuzz) -- cgit v1.2.3 From 6b73b1fb83754badc37e1306871dde6eb293585c Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 31 Dec 2021 15:09:16 +0200 Subject: Updated the_Foundation --- lib/the_Foundation | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/the_Foundation b/lib/the_Foundation index cd0a22f5..b780063a 160000 --- a/lib/the_Foundation +++ b/lib/the_Foundation @@ -1 +1 @@ -Subproject commit cd0a22f5f003b723ecc038f287a320fb9b7c1d88 +Subproject commit b780063a686a5ef207a9ef940a0200c8a420c5df -- cgit v1.2.3 From 618d9eab84f191d0f7cd32537c87e50043850afb Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 31 Dec 2021 17:09:03 +0200 Subject: Android: JNI method for posting command events --- src/app.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.c b/src/app.c index 0ed6f687..71869f44 100644 --- a/src/app.c +++ b/src/app.c @@ -3474,7 +3474,7 @@ float displayDensity_Android(void) { #include -JNIEXPORT void JNICALL Java_fi_skyjake_lagrange_SDLActivity_postAppCommand( +JNIEXPORT void JNICALL Java_fi_skyjake_lagrange_LagrangeActivity_postAppCommand( JNIEnv* env, jclass jcls, jstring command) { -- cgit v1.2.3 From 2a36d8cd0376333d908742c1a8045c19a598c7c5 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 31 Dec 2021 18:56:53 +0200 Subject: Mobile: Fixed crash when creating bookmark Use of uninitialized memory was possible related to the folder selection dropdown. --- src/ui/util.c | 97 ++++++++++++++++++++++++++++++----------------------------- 1 file changed, 50 insertions(+), 47 deletions(-) diff --git a/src/ui/util.c b/src/ui/util.c index 39cfce13..ab7e9ebf 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -2951,6 +2951,7 @@ iWidget *makeBookmarkEditor_Widget(void) { { "${cancel}", 0, 0, "bmed.cancel" }, { uiTextCaution_ColorEscape "${dlg.bookmark.save}", SDLK_RETURN, KMOD_PRIMARY, "bmed.accept" } }; + iWidget *dlg = NULL; if (isUsingPanelLayout_Mobile()) { const iArray *folderItems = makeBookmarkFolderItems_(iTrue); const iMenuItem items[] = { @@ -2969,56 +2970,58 @@ iWidget *makeBookmarkEditor_Widget(void) { { "toggle id:bmed.tag.linksplit text:${bookmark.tag.linksplit}" }, { NULL } }; - iWidget *dlg = makePanels_Mobile("bmed", items, actions, iElemCount(actions)); + dlg = makePanels_Mobile("bmed", items, actions, iElemCount(actions)); setupSheetTransition_Mobile(dlg, iTrue); - return dlg; } - iWidget *dlg = makeSheet_Widget("bmed"); - setId_Widget(addChildFlags_Widget( - dlg, - iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.bookmark.edit}", NULL)), - frameless_WidgetFlag), - "bmed.heading"); - iWidget *headings, *values; - addChild_Widget(dlg, iClob(makeTwoColumns_Widget(&headings, &values))); - iInputWidget *inputs[4]; - /* Folder to add to. */ { - addChild_Widget(headings, iClob(makeHeading_Widget("${dlg.bookmark.folder}"))); - const iArray *folderItems = makeBookmarkFolderItems_(iFalse); - iLabelWidget *folderButton; - setId_Widget(addChildFlags_Widget(values, - iClob(folderButton = makeMenuButton_LabelWidget( - widestLabel_MenuItemArray(folderItems), - constData_Array(folderItems), - size_Array(folderItems))), alignLeft_WidgetFlag), - "bmed.folder"); - const uint32_t recentFolderId = recentFolder_Bookmarks(bookmarks_App()); - updateDropdownSelection_LabelWidget( - folderButton, format_CStr(" arg:%u", recentFolderId)); - setUserData_Object(folderButton, get_Bookmarks(bookmarks_App(), recentFolderId)); - } - addDialogInputWithHeading_(headings, values, "${dlg.bookmark.title}", "bmed.title", iClob(inputs[0] = new_InputWidget(0))); - addDialogInputWithHeading_(headings, values, "${dlg.bookmark.url}", "bmed.url", iClob(inputs[1] = new_InputWidget(0))); - setUrlContent_InputWidget(inputs[1], iTrue); - addDialogInputWithHeading_(headings, values, "${dlg.bookmark.tags}", "bmed.tags", iClob(inputs[2] = new_InputWidget(0))); - addDialogInputWithHeading_(headings, values, "${dlg.bookmark.icon}", "bmed.icon", iClob(inputs[3] = new_InputWidget(1))); - /* Buttons for special tags. */ - addChild_Widget(dlg, iClob(makePadding_Widget(gap_UI))); - iWidget *special = addChild_Widget(dlg, iClob(makeTwoColumns_Widget(&headings, &values))); - setFlags_Widget(special, collapse_WidgetFlag, iTrue); - setId_Widget(special, "bmed.special"); - makeTwoColumnHeading_("${heading.bookmark.tags}", headings, values); - addDialogToggle_(headings, values, "${bookmark.tag.home}", "bmed.tag.home"); - addDialogToggle_(headings, values, "${bookmark.tag.remote}", "bmed.tag.remote"); - addDialogToggle_(headings, values, "${bookmark.tag.linksplit}", "bmed.tag.linksplit"); - arrange_Widget(dlg); - for (int i = 0; i < 3; ++i) { - as_Widget(inputs[i])->rect.size.x = 100 * gap_UI - headings->rect.size.x; + else { + dlg = makeSheet_Widget("bmed"); + setId_Widget(addChildFlags_Widget( + dlg, + iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.bookmark.edit}", NULL)), + frameless_WidgetFlag), + "bmed.heading"); + iWidget *headings, *values; + addChild_Widget(dlg, iClob(makeTwoColumns_Widget(&headings, &values))); + iInputWidget *inputs[4]; + /* Folder to add to. */ { + addChild_Widget(headings, iClob(makeHeading_Widget("${dlg.bookmark.folder}"))); + const iArray *folderItems = makeBookmarkFolderItems_(iFalse); + iLabelWidget *folderButton; + setId_Widget(addChildFlags_Widget(values, + iClob(folderButton = makeMenuButton_LabelWidget( + widestLabel_MenuItemArray(folderItems), + constData_Array(folderItems), + size_Array(folderItems))), alignLeft_WidgetFlag), + "bmed.folder"); + } + addDialogInputWithHeading_(headings, values, "${dlg.bookmark.title}", "bmed.title", iClob(inputs[0] = new_InputWidget(0))); + addDialogInputWithHeading_(headings, values, "${dlg.bookmark.url}", "bmed.url", iClob(inputs[1] = new_InputWidget(0))); + setUrlContent_InputWidget(inputs[1], iTrue); + addDialogInputWithHeading_(headings, values, "${dlg.bookmark.tags}", "bmed.tags", iClob(inputs[2] = new_InputWidget(0))); + addDialogInputWithHeading_(headings, values, "${dlg.bookmark.icon}", "bmed.icon", iClob(inputs[3] = new_InputWidget(1))); + /* Buttons for special tags. */ + addChild_Widget(dlg, iClob(makePadding_Widget(gap_UI))); + iWidget *special = addChild_Widget(dlg, iClob(makeTwoColumns_Widget(&headings, &values))); + setFlags_Widget(special, collapse_WidgetFlag, iTrue); + setId_Widget(special, "bmed.special"); + makeTwoColumnHeading_("${heading.bookmark.tags}", headings, values); + addDialogToggle_(headings, values, "${bookmark.tag.home}", "bmed.tag.home"); + addDialogToggle_(headings, values, "${bookmark.tag.remote}", "bmed.tag.remote"); + addDialogToggle_(headings, values, "${bookmark.tag.linksplit}", "bmed.tag.linksplit"); + arrange_Widget(dlg); + for (int i = 0; i < 3; ++i) { + as_Widget(inputs[i])->rect.size.x = 100 * gap_UI - headings->rect.size.x; + } + addChild_Widget(dlg, iClob(makePadding_Widget(gap_UI))); + addChild_Widget(dlg, iClob(makeDialogButtons_Widget(actions, iElemCount(actions)))); + addChild_Widget(get_Root()->widget, iClob(dlg)); + setupSheetTransition_Mobile(dlg, iTrue); } - addChild_Widget(dlg, iClob(makePadding_Widget(gap_UI))); - addChild_Widget(dlg, iClob(makeDialogButtons_Widget(actions, iElemCount(actions)))); - addChild_Widget(get_Root()->widget, iClob(dlg)); - setupSheetTransition_Mobile(dlg, iTrue); + /* Use a recently accessed folder as the default. */ + const uint32_t recentFolderId = recentFolder_Bookmarks(bookmarks_App()); + iLabelWidget *folderDrop = findChild_Widget(dlg, "bmed.folder"); + updateDropdownSelection_LabelWidget(folderDrop, format_CStr(" arg:%u", recentFolderId)); + setUserData_Object(folderDrop, get_Bookmarks(bookmarks_App(), recentFolderId)); return dlg; } -- cgit v1.2.3 From 0e099b29c941e2cd2286e0bb9a7bff86721ac09d Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 31 Dec 2021 20:15:24 +0200 Subject: Android: Download directory --- src/app.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app.c b/src/app.c index 71869f44..7cd9e9e2 100644 --- a/src/app.c +++ b/src/app.c @@ -346,6 +346,9 @@ static const char *downloadDir_App_(void) { makeDirs_Path(dlDir); } return cstr_String(dlDir); +#endif +#if defined (iPlatformAndroidMobile) + return SDL_AndroidGetInternalStoragePath(); #endif return defaultDownloadDir_App_; } -- cgit v1.2.3 From 5d50db93f30e22592187e0dab9a8466946b65587 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 31 Dec 2021 20:22:00 +0200 Subject: Android: Better downloads directory --- src/app.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app.c b/src/app.c index 7cd9e9e2..58eecf51 100644 --- a/src/app.c +++ b/src/app.c @@ -325,6 +325,9 @@ static const char *dataDir_App_(void) { } static const char *downloadDir_App_(void) { +#if defined (iPlatformAndroidMobile) + return concatPath_CStr(SDL_AndroidGetInternalStoragePath(), "Downloads"); +#endif #if defined (iPlatformLinux) || defined (iPlatformOther) /* Parse user-dirs.dirs using the `xdg-user-dir` tool. */ iProcess *proc = iClob(new_Process()); @@ -346,9 +349,6 @@ static const char *downloadDir_App_(void) { makeDirs_Path(dlDir); } return cstr_String(dlDir); -#endif -#if defined (iPlatformAndroidMobile) - return SDL_AndroidGetInternalStoragePath(); #endif return defaultDownloadDir_App_; } -- cgit v1.2.3 From e58fa57545937f4dbfb52b4966623481f733c74f Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 31 Dec 2021 22:54:15 +0200 Subject: iOS: Bumped version, updated release notes --- CMakeLists.txt | 4 ++-- res/about/ios-version.gmi | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e5de32f7..596e7cd1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,8 +25,8 @@ project (Lagrange set (COPYRIGHT_YEAR 2021) if (IOS) set (PROJECT_VERSION 1.10) - set (IOS_BUNDLE_VERSION 7) - set (IOS_BUILD_DATE "2021-12-26") + set (IOS_BUNDLE_VERSION 8) + set (IOS_BUILD_DATE "2021-12-31") endif () # Defaults that depend on environment. diff --git a/res/about/ios-version.gmi b/res/about/ios-version.gmi index f7d88066..f09de24d 100644 --- a/res/about/ios-version.gmi +++ b/res/about/ios-version.gmi @@ -6,6 +6,13 @@ ``` # Release notes +## 1.10 (8) +* Added "Share" actions for downloaded files (via "Download Linked File") and selected text on page. +* Added "Edit Page with Titan": opens the Upload dialog with the page contents prefilled (previous contents are lost!). +* Inlining `image/*` responses into the current document regardless of file extension in URL. +* Fixed glitch with banner not being visible until page has finished loading. +* Fixed possible crash when creating a bookmark. + ## 1.10 (7) * Link context menu shows used identity and date of last visit in addition to the URL. * Removed the "Show URL on hover" setting. URLs are shown in the link context menu. -- cgit v1.2.3 From ce833b32c770475a2668067df878cdd094e2a4a8 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 1 Jan 2022 01:00:56 +0200 Subject: iOS: Showing activity views on iPad On iPad, activity views are popovers and require information about the source. The UI doesn't provide this, though, so just use the long-press tap position for now. --- src/ios.m | 14 +++++++++++++- src/ui/documentwidget.c | 5 +++-- src/ui/touch.c | 6 ++++++ src/ui/touch.h | 1 + 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/ios.m b/src/ios.m index 69e09419..82596ffd 100644 --- a/src/ios.m +++ b/src/ios.m @@ -25,6 +25,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "audio/player.h" #include "ui/command.h" #include "ui/window.h" +#include "ui/touch.h" #include #include @@ -541,7 +542,18 @@ static void openActivityView_(NSArray *activityItems) { [[UIActivityViewController alloc] initWithActivityItems:activityItems applicationActivities:nil]; - [viewController_(get_Window()) presentViewController:actView animated:YES completion:nil]; + iWindow *win = get_Window(); + UIViewController *viewCtl = viewController_(win); + UIPopoverPresentationController *popover = [actView popoverPresentationController]; + if (popover) { + [popover setSourceView:[viewCtl view]]; + iInt2 tapPos = latestTapPosition_Touch(); + tapPos.x /= win->pixelRatio; + tapPos.y /= win->pixelRatio; + [popover setSourceRect:(CGRect){{tapPos.x - 10, tapPos.y - 10}, {20, 20}}]; + [popover setCanOverlapSourceViewRect:YES]; + } + [viewCtl presentViewController:actView animated:YES completion:nil]; } void openTextActivityView_iOS(const iString *text) { diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index e63e4636..2bce16e8 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -5267,14 +5267,15 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e destroy_Widget(d->copyMenu); d->copyMenu = NULL; } - d->copyMenu = makeMenu_Widget(w, (iMenuItem[]){ + const iMenuItem items[] = { { clipCopy_Icon " ${menu.copy}", 0, 0, "copy" }, #if defined (iPlatformAppleMobile) { export_Icon " ${menu.share}", 0, 0, "copy share:1" }, #endif { "---" }, { close_Icon " ${menu.select.clear}", 0, 0, "document.select arg:0" }, - }, 3); + }; + d->copyMenu = makeMenu_Widget(w, items, iElemCount(items)); setFlags_Widget(d->copyMenu, noFadeBackground_WidgetFlag, iTrue); openMenu_Widget(d->copyMenu, pos_Click(&d->click)); return iTrue; diff --git a/src/ui/touch.c b/src/ui/touch.c index 3d318dfb..20ccf7b8 100644 --- a/src/ui/touch.c +++ b/src/ui/touch.c @@ -111,6 +111,7 @@ struct Impl_TouchState { double momFrictionPerStep; double lastMomTime; iInt2 currentTouchPos; /* for emulating SDL_GetMouseState() */ + iInt2 latestLongPressStartPos; }; static iTouchState *touchState_(void) { @@ -313,6 +314,7 @@ static void update_TouchState_(void *ptr) { } if (!touch->isTapAndHold && nowTime - touch->startTime >= longPressSpanMs_ && touch->affinity) { + touchState_()->latestLongPressStartPos = initF3_I2(touch->pos[0]); dispatchClick_Touch_(touch, SDL_BUTTON_RIGHT); touch->isTapAndHold = iTrue; touch->hasMoved = iFalse; @@ -838,6 +840,10 @@ iInt2 latestPosition_Touch(void) { return touchState_()->currentTouchPos; } +iInt2 latestTapPosition_Touch(void) { + return touchState_()->latestLongPressStartPos; +} + iBool isHovering_Touch(void) { iTouchState *d = touchState_(); if (numFingers_Touch() == 1) { diff --git a/src/ui/touch.h b/src/ui/touch.h index 9c45fcb1..15c6da1f 100644 --- a/src/ui/touch.h +++ b/src/ui/touch.h @@ -42,5 +42,6 @@ void widgetDestroyed_Touch (iWidget *widget); void transferAffinity_Touch (iWidget *src, iWidget *dst); iInt2 latestPosition_Touch (void); /* valid during processing of current event */ +iInt2 latestTapPosition_Touch (void); iBool isHovering_Touch (void); /* stationary touch or a long-press drag ongoing */ size_t numFingers_Touch (void); -- cgit v1.2.3 From baf2b40642da7510525da204a8950485665d9729 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 1 Jan 2022 01:02:06 +0200 Subject: iOS: Bumped version number --- CMakeLists.txt | 4 ++-- res/about/ios-version.gmi | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 596e7cd1..8e001f20 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,8 +25,8 @@ project (Lagrange set (COPYRIGHT_YEAR 2021) if (IOS) set (PROJECT_VERSION 1.10) - set (IOS_BUNDLE_VERSION 8) - set (IOS_BUILD_DATE "2021-12-31") + set (IOS_BUNDLE_VERSION 9) + set (IOS_BUILD_DATE "2022-01-01") endif () # Defaults that depend on environment. diff --git a/res/about/ios-version.gmi b/res/about/ios-version.gmi index f09de24d..f3122dbe 100644 --- a/res/about/ios-version.gmi +++ b/res/about/ios-version.gmi @@ -6,7 +6,7 @@ ``` # Release notes -## 1.10 (8) +## 1.10 (9) * Added "Share" actions for downloaded files (via "Download Linked File") and selected text on page. * Added "Edit Page with Titan": opens the Upload dialog with the page contents prefilled (previous contents are lost!). * Inlining `image/*` responses into the current document regardless of file extension in URL. -- cgit v1.2.3 From 6b3d31a3043d99890355c42dd3248411fd571347 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 1 Jan 2022 07:43:11 +0200 Subject: Updated copyright year --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8e001f20..42927c99 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,7 +22,7 @@ project (Lagrange DESCRIPTION "A Beautiful Gemini Client" LANGUAGES C ) -set (COPYRIGHT_YEAR 2021) +set (COPYRIGHT_YEAR 2022) if (IOS) set (PROJECT_VERSION 1.10) set (IOS_BUNDLE_VERSION 9) -- cgit v1.2.3 From 5d247c7fafddcb46d83572af788a04f835d48d42 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 1 Jan 2022 10:04:06 +0200 Subject: Mobile: User-configurable phone toolbar buttons Actions for the first two mobile portrait phone toolbar buttons can be chosen in Settings. --- po/en.po | 12 ++++++++ res/lang/cs.bin | Bin 31683 -> 31806 bytes res/lang/de.bin | Bin 30690 -> 30813 bytes res/lang/en.bin | Bin 26766 -> 26889 bytes res/lang/eo.bin | Bin 25730 -> 25853 bytes res/lang/es.bin | Bin 30514 -> 30637 bytes res/lang/es_MX.bin | Bin 27836 -> 27959 bytes res/lang/fi.bin | Bin 30347 -> 30470 bytes res/lang/fr.bin | Bin 31494 -> 31617 bytes res/lang/gl.bin | Bin 29699 -> 29822 bytes res/lang/hu.bin | Bin 31519 -> 31642 bytes res/lang/ia.bin | Bin 28836 -> 28959 bytes res/lang/ie.bin | Bin 29434 -> 29557 bytes res/lang/isv.bin | Bin 25487 -> 25610 bytes res/lang/pl.bin | Bin 30122 -> 30245 bytes res/lang/ru.bin | Bin 44882 -> 45005 bytes res/lang/sk.bin | Bin 25823 -> 25946 bytes res/lang/sr.bin | Bin 44308 -> 44431 bytes res/lang/tok.bin | Bin 27556 -> 27679 bytes res/lang/tr.bin | Bin 29740 -> 29863 bytes res/lang/uk.bin | Bin 44227 -> 44350 bytes res/lang/zh_Hans.bin | Bin 25731 -> 25854 bytes res/lang/zh_Hant.bin | Bin 25939 -> 26062 bytes src/app.c | 22 ++++++++++++++ src/defs.h | 17 +++++++++++ src/prefs.c | 2 ++ src/prefs.h | 1 + src/ui/mobile.c | 16 ++++++++++ src/ui/mobile.h | 11 +++++++ src/ui/root.c | 83 +++++++++++++++++++++++---------------------------- src/ui/util.c | 13 +++++++- 31 files changed, 130 insertions(+), 47 deletions(-) diff --git a/po/en.po b/po/en.po index a4b83dcc..cf9643a0 100644 --- a/po/en.po +++ b/po/en.po @@ -344,6 +344,9 @@ msgstr "Go to Parent" msgid "menu.root" msgstr "Go to Root" +msgid "menu.home" +msgstr "Go Home" + msgid "menu.reload" msgstr "Reload Page" @@ -1419,6 +1422,15 @@ msgstr "Load image on scroll:" msgid "prefs.hidetoolbarscroll" msgstr "Hide toolbar on scroll:" +msgid "heading.prefs.toolbaractions" +msgstr "Toolbar Actions" + +msgid "prefs.toolbaraction1" +msgstr "Button 1" + +msgid "prefs.toolbaraction2" +msgstr "Button 2" + msgid "prefs.ostheme" msgstr "Use system theme:" diff --git a/res/lang/cs.bin b/res/lang/cs.bin index bc34d7e8..9069f4b0 100644 Binary files a/res/lang/cs.bin and b/res/lang/cs.bin differ diff --git a/res/lang/de.bin b/res/lang/de.bin index 5fb9eb03..62893c64 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 114fa684..12cde1d0 100644 Binary files a/res/lang/en.bin and b/res/lang/en.bin differ diff --git a/res/lang/eo.bin b/res/lang/eo.bin index 3d16450c..be6c9cef 100644 Binary files a/res/lang/eo.bin and b/res/lang/eo.bin differ diff --git a/res/lang/es.bin b/res/lang/es.bin index fe6bdbb5..feb0b8ea 100644 Binary files a/res/lang/es.bin and b/res/lang/es.bin differ diff --git a/res/lang/es_MX.bin b/res/lang/es_MX.bin index c96da0b0..26188c5b 100644 Binary files a/res/lang/es_MX.bin and b/res/lang/es_MX.bin differ diff --git a/res/lang/fi.bin b/res/lang/fi.bin index d06dfc9c..86efb862 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 4cda45d8..e6977fd9 100644 Binary files a/res/lang/fr.bin and b/res/lang/fr.bin differ diff --git a/res/lang/gl.bin b/res/lang/gl.bin index 7ffb40ec..f282366d 100644 Binary files a/res/lang/gl.bin and b/res/lang/gl.bin differ diff --git a/res/lang/hu.bin b/res/lang/hu.bin index e698c7ac..3ca7ee30 100644 Binary files a/res/lang/hu.bin and b/res/lang/hu.bin differ diff --git a/res/lang/ia.bin b/res/lang/ia.bin index f951f0c5..29faea06 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 c4671e06..9c8bc9bd 100644 Binary files a/res/lang/ie.bin and b/res/lang/ie.bin differ diff --git a/res/lang/isv.bin b/res/lang/isv.bin index 2788dc3f..4414fdb3 100644 Binary files a/res/lang/isv.bin and b/res/lang/isv.bin differ diff --git a/res/lang/pl.bin b/res/lang/pl.bin index 29260245..aae2b705 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 712d8a01..1166b064 100644 Binary files a/res/lang/ru.bin and b/res/lang/ru.bin differ diff --git a/res/lang/sk.bin b/res/lang/sk.bin index 79b1208f..b43f885c 100644 Binary files a/res/lang/sk.bin and b/res/lang/sk.bin differ diff --git a/res/lang/sr.bin b/res/lang/sr.bin index 79e76d20..eec114aa 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 31f9536e..0202545c 100644 Binary files a/res/lang/tok.bin and b/res/lang/tok.bin differ diff --git a/res/lang/tr.bin b/res/lang/tr.bin index 758dba68..95bdffdc 100644 Binary files a/res/lang/tr.bin and b/res/lang/tr.bin differ diff --git a/res/lang/uk.bin b/res/lang/uk.bin index 6aedfe5f..e222a609 100644 Binary files a/res/lang/uk.bin and b/res/lang/uk.bin differ diff --git a/res/lang/zh_Hans.bin b/res/lang/zh_Hans.bin index 036ecca3..648016a8 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 d2aa482d..daa31717 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 58eecf51..1e66c6cf 100644 --- a/src/app.c +++ b/src/app.c @@ -248,6 +248,10 @@ static iString *serializePrefs_App_(const iApp *d) { appendFormat_String(str, "linewidth.set arg:%d\n", d->prefs.lineWidth); appendFormat_String(str, "linespacing.set arg:%f\n", d->prefs.lineSpacing); appendFormat_String(str, "returnkey.set arg:%d\n", d->prefs.returnKey); +#if defined (iPlatformMobile) + appendFormat_String(str, "toolbar.action.set arg:%d button:0\n", d->prefs.toolbarActions[0]); + appendFormat_String(str, "toolbar.action.set arg:%d button:1\n", d->prefs.toolbarActions[1]); +#endif iConstForEach(StringSet, fp, d->prefs.disabledFontPacks) { appendFormat_String(str, "fontpack.disable id:%s\n", cstr_String(fp.value)); } @@ -1870,6 +1874,12 @@ static void updatePrefsPinSplitButtons_(iWidget *d, int value) { } } +static void updatePrefsToolBarActionButton_(iWidget *prefs, int buttonIndex, int action) { + updateDropdownSelection_LabelWidget( + findChild_Widget(prefs, format_CStr("prefs.toolbaraction%d", buttonIndex + 1)), + format_CStr(" arg:%d button:%d", action, buttonIndex)); +} + static void updateScrollSpeedButtons_(iWidget *d, enum iScrollType type, const int value) { const char *typeStr = (type == mouse_ScrollType ? "mouse" : "keyboard"); for (int i = 0; i <= 40; i++) { @@ -1960,6 +1970,10 @@ static iBool handlePrefsCommands_(iWidget *d, const char *cmd) { format_CStr("returnkey.set arg:%d", arg_Command(cmd))); return iFalse; } + else if (equal_Command(cmd, "toolbar.action.set")) { + updatePrefsToolBarActionButton_(d, argLabel_Command(cmd, "button"), arg_Command(cmd)); + return iFalse; + } else if (equal_Command(cmd, "pinsplit.set")) { updatePrefsPinSplitButtons_(d, arg_Command(cmd)); return iFalse; @@ -2268,6 +2282,12 @@ iBool handleCommand_App(const char *cmd) { } return iTrue; } + else if (equal_Command(cmd, "toolbar.action.set")) { + d->prefs.toolbarActions[iClamp(argLabel_Command(cmd, "button"), 0, 1)] = + iClamp(arg_Command(cmd), 0, max_ToolbarAction - 1); + postCommand_App("~toolbar.actions.changed"); + return iTrue; + } else if (equal_Command(cmd, "translation.languages")) { d->prefs.langFrom = argLabel_Command(cmd, "from"); d->prefs.langTo = argLabel_Command(cmd, "to"); @@ -3006,6 +3026,8 @@ iBool handleCommand_App(const char *cmd) { updateDropdownSelection_LabelWidget( findChild_Widget(dlg, "prefs.returnkey"), format_CStr("returnkey.set arg:%d", d->prefs.returnKey)); + updatePrefsToolBarActionButton_(dlg, 0, d->prefs.toolbarActions[0]); + updatePrefsToolBarActionButton_(dlg, 1, d->prefs.toolbarActions[1]); setToggle_Widget(findChild_Widget(dlg, "prefs.retainwindow"), d->prefs.retainWindowSize); setText_InputWidget(findChild_Widget(dlg, "prefs.uiscale"), collectNewFormat_String("%g", uiScale_Window(as_Window(d->window)))); diff --git a/src/defs.h b/src/defs.h index 9a466674..25c0ceeb 100644 --- a/src/defs.h +++ b/src/defs.h @@ -66,6 +66,23 @@ enum iReturnKeyFlag { accept_ReturnKeyFlag = 4, /* shift */ }; +enum iToolbarAction { + back_ToolbarAction = 0, + forward_ToolbarAction = 1, + home_ToolbarAction = 2, + parent_ToolbarAction = 3, + reload_ToolbarAction = 4, + newTab_ToolbarAction = 5, + closeTab_ToolbarAction = 6, + addBookmark_ToolbarAction = 7, + translate_ToolbarAction = 8, + upload_ToolbarAction = 9, + editPage_ToolbarAction = 10, + findText_ToolbarAction = 11, + settings_ToolbarAction = 12, + max_ToolbarAction +}; + /* Return key behavior is not handled via normal bindings because only certain combinations are valid. */ enum iReturnKeyBehavior { diff --git a/src/prefs.c b/src/prefs.c index 6b0164b6..426b7212 100644 --- a/src/prefs.c +++ b/src/prefs.c @@ -44,6 +44,8 @@ void init_Prefs(iPrefs *d) { d->uiAnimations = iTrue; d->uiScale = 1.0f; /* default set elsewhere */ d->zoomPercent = 100; + d->toolbarActions[0] = back_ToolbarAction; + d->toolbarActions[1] = forward_ToolbarAction; d->sideIcon = iTrue; d->hideToolbarOnScroll = iTrue; d->blinkingCursor = iTrue; diff --git a/src/prefs.h b/src/prefs.h index 6c79a3e1..43f7fc0e 100644 --- a/src/prefs.h +++ b/src/prefs.h @@ -159,6 +159,7 @@ struct Impl_Prefs { enum iColorAccent accent; /* Window and User Interface */ float uiScale; + enum iToolbarAction toolbarActions[2]; /* Document presentation */ int zoomPercent; /* Behavior */ diff --git a/src/ui/mobile.c b/src/ui/mobile.c index 08d8dba2..e34cad3a 100644 --- a/src/ui/mobile.c +++ b/src/ui/mobile.c @@ -37,6 +37,22 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ # include "ios.h" #endif +const iToolbarActionSpec toolbarActions_Mobile[max_ToolbarAction] = { + { backArrow_Icon, "${menu.back}", "navigate.back" }, + { forwardArrow_Icon, "${menu.forward}", "navigate.forward" }, + { home_Icon, "${menu.home}", "navigate.home" }, + { upArrow_Icon, "${menu.parent}", "navigate.parent" }, + { reload_Icon, "${menu.reload}", "navigate.reload" }, + { openTab_Icon, "${menu.newtab}", "tabs.new" }, + { close_Icon, "${menu.closetab}", "tabs.close" }, + { bookmark_Icon, "${menu.page.bookmark}", "bookmark.add" }, + { globe_Icon, "${menu.page.translate}", "document.translate" }, + { upload_Icon, "${menu.page.upload}", "document.upload" }, + { edit_Icon, "${menu.page.upload.edit}", "document.upload copy:1" }, + { magnifyingGlass_Icon, "${menu.find}", "focus.set id:find.input" }, + { gear_Icon, "${menu.settings}", "preferences" }, +}; + iBool isUsingPanelLayout_Mobile(void) { return deviceType_App() != desktop_AppDeviceType; } diff --git a/src/ui/mobile.h b/src/ui/mobile.h index 54f55fd2..c19623f9 100644 --- a/src/ui/mobile.h +++ b/src/ui/mobile.h @@ -22,8 +22,19 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #pragma once +#include "defs.h" #include +iDeclareType(ToolbarActionSpec) + +struct Impl_ToolbarActionSpec { + const char *icon; + const char *label; + const char *command; +}; + +const iToolbarActionSpec toolbarActions_Mobile[max_ToolbarAction]; + iDeclareType(Widget) iDeclareType(MenuItem) diff --git a/src/ui/root.c b/src/ui/root.c index bc2bc0fb..31176115 100644 --- a/src/ui/root.c +++ b/src/ui/root.c @@ -592,14 +592,21 @@ static void updateNavBarIdentity_(iWidget *navBar) { static void updateNavDirButtons_(iWidget *navBar) { const iHistory *history = history_DocumentWidget(document_App()); - const iBool atOldest = atOldest_History(history); - const iBool atNewest = atNewest_History(history); + iBool atOldest = atOldest_History(history); + iBool atNewest = atNewest_History(history); setFlags_Widget(findChild_Widget(navBar, "navbar.back"), disabled_WidgetFlag, atOldest); setFlags_Widget(findChild_Widget(navBar, "navbar.forward"), disabled_WidgetFlag, atNewest); iWidget *toolBar = findWidget_App("toolbar"); if (toolBar) { - iLabelWidget *back = findChild_Widget(toolBar, "toolbar.back"); - iLabelWidget *fwd = findChild_Widget(toolBar, "toolbar.forward"); + /* Reset the state. */ + for (int i = 0; i < 2; i++) { + const char *id = (i == 0 ? "toolbar.action1" : "toolbar.action2"); + setFlags_Widget(findChild_Widget(toolBar, id), disabled_WidgetFlag, iFalse); + setOutline_LabelWidget(findChild_Widget(toolBar, id), iFalse); + } + /* Disable certain actions. */ + iLabelWidget *back = findMenuItem_Widget(toolBar, "navigate.back"); + iLabelWidget *fwd = findMenuItem_Widget(toolBar, "navigate.forward"); setFlags_Widget(as_Widget(back), disabled_WidgetFlag, atOldest); setOutline_LabelWidget(back, atOldest); setFlags_Widget(as_Widget(fwd), disabled_WidgetFlag, atNewest); @@ -1062,14 +1069,23 @@ static iBool handleSearchBarCommands_(iWidget *searchBar, const char *cmd) { } #if defined (iPlatformMobile) -static void dismissSidebar_(iWidget *sidebar, const char *toolButtonId) { - if (isVisible_Widget(sidebar)) { - postCommandf_App("%s.toggle", cstr_String(id_Widget(sidebar))); -// if (toolButtonId) { - // setFlags_Widget(findWidget_App(toolButtonId), noBackground_WidgetFlag, iTrue); -// } - setVisualOffset_Widget(sidebar, height_Widget(sidebar), 250, easeIn_AnimFlag); + +static void updateToolBarActions_(iWidget *toolBar) { + const iPrefs *prefs = prefs_App(); + for (int i = 0; i < 2; i++) { + int action = prefs->toolbarActions[i] + ? prefs->toolbarActions[i] + : (i == 0 ? back_ToolbarAction : forward_ToolbarAction); + iLabelWidget *button = + findChild_Widget(toolBar, i == 0 ? "toolbar.action1" : "toolbar.action2"); + if (button) { + setFlags_Widget(as_Widget(button), disabled_WidgetFlag, iFalse); + setOutline_LabelWidget(button, iFalse); + updateTextCStr_LabelWidget(button, toolbarActions_Mobile[action].icon); + setCommand_LabelWidget(button, collectNewCStr_String(toolbarActions_Mobile[action].command)); + } } + refresh_Widget(toolBar); } static iBool handleToolBarCommands_(iWidget *toolBar, const char *cmd) { @@ -1081,13 +1097,6 @@ static iBool handleToolBarCommands_(iWidget *toolBar, const char *cmd) { return iTrue; } else if (equal_Command(cmd, "toolbar.showview")) { - /* TODO: Clean this up. */ -// iWidget *sidebar = findWidget_App("sidebar"); -// iWidget *sidebar2 = findWidget_App("sidebar2"); -// dismissSidebar_(sidebar2, "toolbar.ident"); -// const iBool isVisible = isVisible_Widget(sidebar); - /* If a sidebar hasn't been shown yet, it's height is zero. */ -// const int viewHeight = size_Root(get_Root()).y; if (arg_Command(cmd) >= 0) { postCommandf_App("sidebar.mode arg:%d show:1", arg_Command(cmd)); } @@ -1102,29 +1111,6 @@ static iBool handleToolBarCommands_(iWidget *toolBar, const char *cmd) { postCommandf_App("sidebar.toggle"); } postCommand_App("preferences idents:1"); -#if 0 - /* TODO: Clean this up. */ - iWidget *sidebar2 = findWidget_App("sidebar2"); - //dismissSidebar_(sidebar, "toolbar.view"); - if (isVisible_Widget(sidebar)) { - postCommandf_App("sidebar.toggle"); - } - const iBool isVisible = isVisible_Widget(sidebar2); - // setFlags_Widget(findChild_Widget(toolBar, "toolbar.ident"), noBackground_WidgetFlag, - // isVisible); - /* If a sidebar hasn't been shown yet, it's height is zero. */ - const int viewHeight = size_Root(get_Root()).y; - if (isVisible) { - dismissSidebar_(sidebar2, NULL); - } - else { - postCommand_App("sidebar2.mode arg:3 show:1"); - int offset = height_Widget(sidebar2); - if (offset == 0) offset = size_Root(get_Root()).y; - setVisualOffset_Widget(sidebar2, offset, 0, 0); - setVisualOffset_Widget(sidebar2, 0, 400, easeOut_AnimFlag | softer_AnimFlag); - } -#endif return iTrue; } else if (equal_Command(cmd, "sidebar.mode.changed")) { @@ -1132,8 +1118,13 @@ static iBool handleToolBarCommands_(iWidget *toolBar, const char *cmd) { updateTextCStr_LabelWidget(viewTool, icon_SidebarMode(arg_Command(cmd))); return iFalse; } + else if (equal_Command(cmd, "toolbar.actions.changed")) { + updateToolBarActions_(toolBar); + return iFalse; + } return iFalse; } + #endif /* defined (iPlatformMobile) */ static iLabelWidget *newLargeIcon_LabelWidget(const char *text, const char *cmd) { @@ -1552,14 +1543,14 @@ void createUserInterface_Root(iRoot *d) { "toolbar.close"); #else setId_Widget(addChildFlags_Widget(toolBar, - iClob(newLargeIcon_LabelWidget(backArrow_Icon, "navigate.back")), + iClob(newLargeIcon_LabelWidget("", "...")), frameless_WidgetFlag), - "toolbar.back"); + "toolbar.action1"); #endif setId_Widget(addChildFlags_Widget(toolBar, - iClob(newLargeIcon_LabelWidget(forwardArrow_Icon, "navigate.forward")), + iClob(newLargeIcon_LabelWidget("", "...")), frameless_WidgetFlag), - "toolbar.forward"); + "toolbar.action2"); iWidget *identButton; setId_Widget(identButton = addChildFlags_Widget( toolBar, @@ -1589,10 +1580,10 @@ void createUserInterface_Root(iRoot *d) { setId_Widget(as_Widget(menuButton), "toolbar.navmenu"); addChildFlags_Widget(toolBar, iClob(menuButton), frameless_WidgetFlag); iForEach(ObjectList, i, children_Widget(toolBar)) { - iLabelWidget *btn = i.object; setFlags_Widget(i.object, noBackground_WidgetFlag, iTrue); } updateToolbarColors_Root(d); + updateToolBarActions_(toolBar); const iMenuItem items[] = { { book_Icon " ${sidebar.bookmarks}", 0, 0, "toolbar.showview arg:0" }, { star_Icon " ${sidebar.feeds}", 0, 0, "toolbar.showview arg:1" }, diff --git a/src/ui/util.c b/src/ui/util.c index ab7e9ebf..6add5c89 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -2383,6 +2383,15 @@ iWidget *makePreferences_Widget(void) { format_CStr("returnkey.set arg:%d", acceptWithPrimaryMod_ReturnKeyBehavior) }, { NULL } }; + iMenuItem toolbarActionItems[2][max_ToolbarAction + 1]; + iZap(toolbarActionItems); + for (int j = 0; j < 2; j++) { + for (int i = 0; i < max_ToolbarAction; i++) { + toolbarActionItems[j][i].label = toolbarActions_Mobile[i].label; + toolbarActionItems[j][i].command = + format_CStr("toolbar.action.set arg:%d button:%d", i, j); + } + } iMenuItem docThemes[2][max_GmDocumentTheme + 1]; for (int i = 0; i < 2; ++i) { const iBool isDark = (i == 0); @@ -2476,8 +2485,10 @@ iWidget *makePreferences_Widget(void) { { "title id:heading.prefs.interface" }, { "dropdown device:0 id:prefs.returnkey", 0, 0, (const void *) returnKeyBehaviors }, { "padding device:1" }, - //{ "toggle id:prefs.hoverlink" }, { "toggle device:2 id:prefs.hidetoolbarscroll" }, + { "heading device:2 id:heading.prefs.toolbaractions" }, + { "dropdown device:2 id:prefs.toolbaraction1", 0, 0, (const void *) toolbarActionItems[0] }, + { "dropdown device:2 id:prefs.toolbaraction2", 0, 0, (const void *) toolbarActionItems[1] }, { "heading id:heading.prefs.sizing" }, { "input id:prefs.uiscale maxlen:8" }, { NULL } -- cgit v1.2.3 From 3e371f7ea70b1c2e06e1af50c3b41efdecbd072b Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 1 Jan 2022 15:22:43 +0200 Subject: Android: Fixed root sizing; clip menu not showing; toolbar buttons The keyboard height was miscalculated if there were system keys under the window. --- src/prefs.c | 5 +++++ src/ui/root.c | 15 ++++++--------- src/ui/util.c | 6 +++++- src/ui/window.c | 20 ++++++++++++++++++-- src/ui/window.h | 7 ++++--- 5 files changed, 38 insertions(+), 15 deletions(-) diff --git a/src/prefs.c b/src/prefs.c index 426b7212..3aa05f01 100644 --- a/src/prefs.c +++ b/src/prefs.c @@ -44,7 +44,12 @@ void init_Prefs(iPrefs *d) { d->uiAnimations = iTrue; d->uiScale = 1.0f; /* default set elsewhere */ d->zoomPercent = 100; +#if defined (iPlatformAndroidMobile) + /* Android has a system-wide back button so no need to have a duplicate. */ + d->toolbarActions[0] = closeTab_ToolbarAction; +#else d->toolbarActions[0] = back_ToolbarAction; +#endif d->toolbarActions[1] = forward_ToolbarAction; d->sideIcon = iTrue; d->hideToolbarOnScroll = iTrue; diff --git a/src/ui/root.c b/src/ui/root.c index 31176115..5ec63e91 100644 --- a/src/ui/root.c +++ b/src/ui/root.c @@ -1534,19 +1534,10 @@ void createUserInterface_Root(iRoot *d) { arrangeHeight_WidgetFlag | arrangeHorizontal_WidgetFlag | commandOnClick_WidgetFlag | drawBackgroundToBottom_WidgetFlag, iTrue); -#if defined (iPlatformAndroidMobile) - /* Android has a system-provided back button (or gesture?), or in the toolbar we can have - a different in the place of Back. */ - setId_Widget(addChildFlags_Widget(toolBar, - iClob(newLargeIcon_LabelWidget(close_Icon, "tabs.close")), - frameless_WidgetFlag), - "toolbar.close"); -#else setId_Widget(addChildFlags_Widget(toolBar, iClob(newLargeIcon_LabelWidget("", "...")), frameless_WidgetFlag), "toolbar.action1"); -#endif setId_Widget(addChildFlags_Widget(toolBar, iClob(newLargeIcon_LabelWidget("", "...")), frameless_WidgetFlag), @@ -1638,6 +1629,12 @@ void createUserInterface_Root(iRoot *d) { { select_Icon " ${menu.selectall}", 0, 0, "input.selectall" }, }, 8); #endif + if (deviceType_App() == phone_AppDeviceType) { + /* Small screen; conserve space by removing the Cancel item. */ + iRelease(removeChild_Widget(clipMenu, lastChild_Widget(clipMenu))); + iRelease(removeChild_Widget(clipMenu, lastChild_Widget(clipMenu))); + iRelease(removeChild_Widget(clipMenu, lastChild_Widget(clipMenu))); + } iWidget *splitMenu = makeMenu_Widget(root, (iMenuItem[]){ { "${menu.split.merge}", '1', 0, "ui.split arg:0" }, { "${menu.split.swap}", SDLK_x, 0, "ui.split swap:1" }, diff --git a/src/ui/util.c b/src/ui/util.c index 6add5c89..c590caa9 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -1204,6 +1204,9 @@ void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, int menuOpenFlags) { leftExcess += l; rightExcess += r; } +#elif defined (iPlatformMobile) + /* Reserve space for the keyboard. */ + bottomExcess += get_MainWindow()->keyboardHeight; #endif if (!allowOverflow) { if (bottomExcess > 0 && (!isPortraitPhone || !isSlidePanel)) { @@ -2495,8 +2498,9 @@ iWidget *makePreferences_Widget(void) { }; const iMenuItem colorPanelItems[] = { { "title id:heading.prefs.colors" }, - //{ "heading id:heading.prefs.uitheme" }, +#if !defined (iPlatformAndroidMobile) { "toggle id:prefs.ostheme" }, +#endif { "radio id:prefs.theme", 0, 0, (const void *) themeItems }, { "radio id:prefs.accent", 0, 0, (const void *) accentItems }, { "heading id:heading.prefs.pagecontent" }, diff --git a/src/ui/window.c b/src/ui/window.c index 953d5ea4..ef941798 100644 --- a/src/ui/window.c +++ b/src/ui/window.c @@ -606,6 +606,7 @@ void init_MainWindow(iMainWindow *d, iRect rect) { #endif setCurrent_Text(d->base.text); SDL_GetRendererOutputSize(d->base.render, &d->base.size.x, &d->base.size.y); + d->maxDrawableHeight = d->base.size.y; setupUserInterface_MainWindow(d); postCommand_App("~bindings.changed"); /* update from bindings */ /* Load the border shadow texture. */ { @@ -1011,8 +1012,19 @@ iBool processEvent_Window(iWindow *d, const SDL_Event *ev) { if (event.type == SDL_USEREVENT && isCommand_UserEvent(ev, "window.sysframe") && mw) { /* This command is sent on Android to update the keyboard height. */ const char *cmd = command_UserEvent(ev); - setKeyboardHeight_MainWindow(mw, argLabel_Command(cmd, "fullheight") - - argLabel_Command(cmd, "bottom")); + /* + 0 + | + top + | | + | bottom (top of keyboard) : + | | : keyboardHeight + maxDrawableHeight : + | + fullheight + */ + setKeyboardHeight_MainWindow(mw, argLabel_Command(cmd, "top") + + mw->maxDrawableHeight - argLabel_Command(cmd, "bottom")); return iTrue; } if (processEvent_Touch(&event)) { @@ -1245,11 +1257,15 @@ void draw_MainWindow(iMainWindow *d) { isDrawing_ = iTrue; setCurrent_Text(d->base.text); /* Check if root needs resizing. */ { + const iBool wasPortrait = isPortrait_App(); iInt2 renderSize; SDL_GetRendererOutputSize(w->render, &renderSize.x, &renderSize.y); if (!isEqual_I2(renderSize, w->size)) { updateSize_MainWindow_(d, iTrue); processEvents_App(postedEventsOnly_AppEventMode); + if (isPortrait_App() != wasPortrait) { + d->maxDrawableHeight = renderSize.y; + } } } const int winFlags = SDL_GetWindowFlags(d->base.win); diff --git a/src/ui/window.h b/src/ui/window.h index ae111f4c..b4e348d2 100644 --- a/src/ui/window.h +++ b/src/ui/window.h @@ -116,7 +116,8 @@ struct Impl_MainWindow { iString * pendingSplitUrl; /* URL to open in a newly opened split */ iString * pendingSplitOrigin; /* tab from where split was initiated, if any */ SDL_Texture * appIcon; - int keyboardHeight; /* mobile software keyboards */ + int keyboardHeight; /* mobile software keyboards */ + int maxDrawableHeight; }; iLocalDef enum iWindowType type_Window(const iAnyWindow *d) { @@ -187,10 +188,10 @@ void setKeyboardHeight_MainWindow (iMainWindow *, int height); void setSplitMode_MainWindow (iMainWindow *, int splitMode); void checkPendingSplit_MainWindow (iMainWindow *); void swapRoots_MainWindow (iMainWindow *); -void showToolbars_MainWindow (iMainWindow *, iBool show); +//void showToolbars_MainWindow (iMainWindow *, iBool show); void resize_MainWindow (iMainWindow *, int w, int h); -iBool processEvent_MainWindow (iMainWindow *, const SDL_Event *); +//iBool processEvent_MainWindow (iMainWindow *, const SDL_Event *); void draw_MainWindow (iMainWindow *); void drawWhileResizing_MainWindow (iMainWindow *, int w, int h); /* workaround for SDL bug */ -- cgit v1.2.3 From ffdbd9444fc436c6f85412da45fa76d6147044f3 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 1 Jan 2022 15:37:32 +0200 Subject: Android: Show build version and date in Settings > About --- CMakeLists.txt | 11 +++++++++++ src/ui/util.c | 4 ++++ 2 files changed, 15 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 42927c99..0312e115 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,6 +28,11 @@ if (IOS) set (IOS_BUNDLE_VERSION 9) set (IOS_BUILD_DATE "2022-01-01") endif () +if (ANDROID) + set (PROJECT_VERSION 1.10) + set (ANDROID_BUILD_VERSION a3) # remember to update Gradle, AndroidManifest.xml + set (ANDROID_BUILD_DATE "2022-01-01") +endif () # Defaults that depend on environment. set (DEFAULT_RESIZE_DRAW ON) @@ -444,6 +449,12 @@ if (APPLE) ) endif () endif () +if (ANDROID) + target_compile_definitions (app PUBLIC + LAGRANGE_ANDROID_VERSION="${ANDROID_BUILD_VERSION}" + LAGRANGE_ANDROID_BUILD_DATE="${ANDROID_BUILD_DATE}" + ) +endif () if (MSYS) target_link_libraries (app PUBLIC d2d1 uuid dwmapi) # querying DPI if (ENABLE_WINSPARKLE) diff --git a/src/ui/util.c b/src/ui/util.c index c590caa9..e631a092 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -2571,6 +2571,10 @@ iWidget *makePreferences_Widget(void) { #if defined (iPlatformAppleMobile) appendFormat_String(aboutText, " (" LAGRANGE_IOS_VERSION ") %s" LAGRANGE_IOS_BUILD_DATE, escape_Color(uiTextDim_ColorId)); +#endif +#if defined (iPlatformAndroidMobile) + appendFormat_String(aboutText, " (" LAGRANGE_ANDROID_VERSION ") %s" LAGRANGE_ANDROID_BUILD_DATE, + escape_Color(uiTextDim_ColorId)); #endif } const iMenuItem aboutPanelItems[] = { -- cgit v1.2.3 From edd6555da303a6eb6fc6dc4132a5094625ba27e9 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 2 Jan 2022 07:42:51 +0200 Subject: Mobile: Taller portrait phone navbar The navbar is an important tap target, so it should be the same height as most menu items and the toolbar. --- src/ui/inputwidget.c | 9 ++++----- src/ui/root.c | 26 +++++++++----------------- src/ui/widget.h | 18 ++++++++++++++++++ 3 files changed, 31 insertions(+), 22 deletions(-) diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index 2de16e6e..5d74f855 100644 --- a/src/ui/inputwidget.c +++ b/src/ui/inputwidget.c @@ -59,6 +59,9 @@ static const int unlimitedWidth_InputWidget_ = 1000000; /* TODO: WrapText di static const iChar sensitiveChar_ = 0x25cf; /* black circle */ static const char * sensitive_ = "\u25cf"; +#define extraPaddingHeight_ ((isPortraitPhone_App() ? 3.0f : 1.25f) * gap_UI) /* phone: proper tap target */ +#define minWidth_InputWidget_ (3 * gap_UI) + static void enableEditorKeysInMenus_(iBool enable) { #if defined (iPlatformAppleDesktop) enableMenuItemsByKey_MacOS(SDLK_LEFT, KMOD_PRIMARY, enable); @@ -228,7 +231,7 @@ struct Impl_InputWidget { size_t maxLen; /* characters */ iString srcHint; iString hint; - int leftPadding; + int leftPadding; /* additional padding between frame and content */ int rightPadding; int minWrapLines, maxWrapLines; /* min/max number of visible lines allowed */ iRangei visWrapLines; /* which wrap lines are current visible */ @@ -359,8 +362,6 @@ static const iInputLine *line_InputWidget_(const iInputWidget *d, size_t index) #endif /* !LAGRANGE_USE_SYSTEM_TEXT_INPUT */ -#define extraPaddingHeight_ (1.25f * gap_UI) - static iRect contentBounds_InputWidget_(const iInputWidget *d) { const iWidget *w = constAs_Widget(d); iRect bounds = adjusted_Rect(bounds_Widget(w), @@ -374,8 +375,6 @@ static iRect contentBounds_InputWidget_(const iInputWidget *d) { return bounds; } -#define minWidth_InputWidget_ (3 * gap_UI) - static iWrapText wrap_InputWidget_(const iInputWidget *d, int y) { #if LAGRANGE_USE_SYSTEM_TEXT_INPUT iUnused(y); /* full text is wrapped always */ diff --git a/src/ui/root.c b/src/ui/root.c index 5ec63e91..d5148056 100644 --- a/src/ui/root.c +++ b/src/ui/root.c @@ -650,10 +650,10 @@ static void checkLoadAnimation_Root_(iRoot *d) { void updatePadding_Root(iRoot *d) { if (d == NULL) return; - iWidget *toolBar = findChild_Widget(d->widget, "toolbar"); - float bottom = 0.0f; #if defined (iPlatformAppleMobile) + iWidget *toolBar = findChild_Widget(d->widget, "toolbar"); float left, top, right; + float bottom = 0.0f; safeAreaInsets_iOS(&left, &top, &right, &bottom); /* Respect the safe area insets. */ { setPadding_Widget(findChild_Widget(d->widget, "navdiv"), left, top, right, 0); @@ -662,15 +662,6 @@ void updatePadding_Root(iRoot *d) { } } #endif -// if (toolBar) { - /* TODO: get this from toolBar height, but it's buggy for some reason */ -// const int sidebarBottomPad = isPortrait_App() ? 11 * gap_UI + bottom : 0; -// setPadding_Widget(findChild_Widget(d->widget, "sidebar"), 0, 0, 0, sidebarBottomPad); - //setPadding_Widget(findChild_Widget(d->widget, "sidebar2"), 0, 0, 0, sidebarBottomPad); - /* TODO: There seems to be unrelated layout glitch in the sidebar where its children - are not arranged correctly until it's hidden and reshown. */ -// } - /* Note that `handleNavBarCommands_` also adjusts padding and spacing. */ } void updateToolbarColors_Root(iRoot *d) { @@ -773,9 +764,8 @@ static void updateNavBarSize_(iWidget *navBar) { const iBool isPhone = deviceType_App() == phone_AppDeviceType; const iBool isNarrow = !isPhone && isNarrow_Root(navBar->root); /* Adjust navbar padding. */ { - int hPad = isPhone && isPortrait_App() ? 0 : (isPhone || isNarrow) ? gap_UI / 2 - : gap_UI * 3 / 2; - int vPad = gap_UI * 3 / 2; + int hPad = isPortraitPhone_App() ? 0 : isPhone || isNarrow ? gap_UI / 2 : (gap_UI * 3 / 2); + int vPad = gap_UI * 3 / 2; int topPad = !findWidget_Root("winbar") ? gap_UI / 2 : 0; setPadding_Widget(navBar, hPad, vPad / 3 + topPad, hPad, vPad / 2); } @@ -1162,7 +1152,7 @@ void updateMetrics_Root(iRoot *d) { iWidget *urlButtons = findChild_Widget(navBar, "url.buttons"); iLabelWidget *idName = findChild_Widget(d->widget, "toolbar.name"); setPadding_Widget(as_Widget(url), 0, gap_UI, 0, gap_UI); - navBar->rect.size.y = 0; /* recalculate height based on children (FIXME: shouldn't be needed) */ +// navBar->rect.size.y = 0; /* recalculate height based on children (FIXME: shouldn't be needed) */ setFixedSize_Widget(embedPad, init_I2(width_Widget(urlButtons) + gap_UI / 2, 1)); rightEmbed->rect.pos.y = gap_UI; updatePadding_Root(d); @@ -1403,13 +1393,15 @@ void createUserInterface_Root(iRoot *d) { setFont_LabelWidget(pageMenuButton, uiContentBold_FontId); setAlignVisually_LabelWidget(pageMenuButton, iTrue); addChildFlags_Widget(urlButtons, iClob(pageMenuButton), - embedFlags | tight_WidgetFlag | collapse_WidgetFlag); + embedFlags | tight_WidgetFlag | collapse_WidgetFlag | + resizeToParentHeight_WidgetFlag); updateSize_LabelWidget(pageMenuButton); } /* Reload button. */ { iLabelWidget *reload = newIcon_LabelWidget(reloadCStr_, 0, 0, "navigate.reload"); setId_Widget(as_Widget(reload), "reload"); - addChildFlags_Widget(urlButtons, iClob(reload), embedFlags | collapse_WidgetFlag); + addChildFlags_Widget(urlButtons, iClob(reload), embedFlags | collapse_WidgetFlag | + resizeToParentHeight_WidgetFlag); updateSize_LabelWidget(reload); } addChildFlags_Widget(as_Widget(url), iClob(urlButtons), moveToParentRightEdge_WidgetFlag); diff --git a/src/ui/widget.h b/src/ui/widget.h index 4ab8d602..eb3004dc 100644 --- a/src/ui/widget.h +++ b/src/ui/widget.h @@ -237,6 +237,24 @@ iLocalDef int height_Widget(const iAnyObject *d) { } return 0; } +iLocalDef int leftPad_Widget(const iWidget *d) { + return d->padding[0]; +} +iLocalDef int topPad_Widget(const iWidget *d) { + return d->padding[1]; +} +iLocalDef int rightPad_Widget(const iWidget *d) { + return d->padding[2]; +} +iLocalDef int bottomPad_Widget(const iWidget *d) { + return d->padding[3]; +} +iLocalDef iInt2 tlPad_Widget(const iWidget *d) { + return init_I2(leftPad_Widget(d), topPad_Widget(d)); +} +iLocalDef iInt2 brPad_Widget(const iWidget *d) { + return init_I2(rightPad_Widget(d), bottomPad_Widget(d)); +} iLocalDef iObjectList *children_Widget(iAnyObject *d) { if (d == NULL) return NULL; iAssert(isInstance_Object(d, &Class_Widget)); -- cgit v1.2.3 From 5e535fa7f3f39d8c948338e1aa152b3efb11b727 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 2 Jan 2022 07:53:06 +0200 Subject: Lang: Headings are title-cased The uppercasing of headings will occur programmatically if appropriate. --- po/en.po | 60 +++++++++++++++++++++++++-------------------------- res/lang/en.bin | Bin 26889 -> 26889 bytes res/lang/eo.bin | Bin 25853 -> 25853 bytes res/lang/es_MX.bin | Bin 27959 -> 27959 bytes res/lang/ia.bin | Bin 28959 -> 28959 bytes res/lang/ie.bin | Bin 29557 -> 29557 bytes res/lang/isv.bin | Bin 25610 -> 25610 bytes res/lang/pl.bin | Bin 30245 -> 30245 bytes res/lang/sk.bin | Bin 25946 -> 25946 bytes res/lang/tok.bin | Bin 27679 -> 27679 bytes res/lang/zh_Hans.bin | Bin 25854 -> 25854 bytes res/lang/zh_Hant.bin | Bin 26062 -> 26062 bytes 12 files changed, 30 insertions(+), 30 deletions(-) diff --git a/po/en.po b/po/en.po index cf9643a0..687c4321 100644 --- a/po/en.po +++ b/po/en.po @@ -573,7 +573,7 @@ msgid "history.clear" msgstr "Clear History…" msgid "heading.history.clear" -msgstr "CLEAR HISTORY" +msgstr "Clear History" msgid "dlg.confirm.history.clear" msgstr "Do you really want to erase the history of all visited pages?" @@ -582,7 +582,7 @@ msgid "dlg.history.clear" msgstr "Clear History" msgid "heading.confirm.bookmarks.delete" -msgstr "DELETE BOOKMARKS" +msgstr "Delete Bookmarks" #, c-format msgid "dlg.confirm.bookmarks.delete" @@ -660,13 +660,13 @@ msgid "ident.switch" msgstr "Use %s" msgid "heading.ident.use" -msgstr "IDENTITY USAGE" +msgstr "Identity Usage" msgid "menu.edit.notes" msgstr "Edit Notes…" msgid "heading.ident.notes" -msgstr "IDENTITY NOTES" +msgstr "Identity Notes" # %s refers to name of an identity. #, c-format @@ -680,7 +680,7 @@ msgid "ident.delete" msgstr "Delete Identity…" msgid "heading.ident.delete" -msgstr "DELETE IDENTITY" +msgstr "Delete Identity" #, c-format msgid "dlg.confirm.ident.delete" @@ -701,7 +701,7 @@ msgid "ident.gotohelp" msgstr "See %sHelp%s for more information about TLS client certificates." msgid "heading.unsub" -msgstr "UNSUBSCRIBE" +msgstr "Unsubscribe" #, c-format msgid "dlg.confirm.unsub" @@ -718,7 +718,7 @@ msgid "error.server.msg" msgstr "Server responded with the message:" msgid "heading.pageinfo" -msgstr "PAGE INFORMATION" +msgstr "Page Information" msgid "pageinfo.header.cached" msgstr "(cached content)" @@ -771,10 +771,10 @@ msgid "menu.input.precedingline" msgstr "Paste Preceding Line" msgid "heading.save" -msgstr "FILE SAVED" +msgstr "File Saved" msgid "heading.save.incomplete" -msgstr "PAGE INCOMPLETE" +msgstr "Page Incomplete" msgid "dlg.save.incomplete" msgstr "The page contents are still being downloaded." @@ -786,10 +786,10 @@ msgid "dlg.save.opendownload" msgstr "Open Downloaded File" msgid "heading.save.error" -msgstr "ERROR SAVING FILE" +msgstr "Error Saving File" msgid "heading.import.bookmarks" -msgstr "IMPORT BOOKMARKS" +msgstr "Import Bookmarks" #, c-format msgid "dlg.import.found" @@ -807,7 +807,7 @@ msgid "dlg.import.notnew" msgstr "All links on this page are already bookmarked." msgid "heading.autoreload" -msgstr "AUTO-RELOAD" +msgstr "Auto-Reload" msgid "dlg.autoreload" msgstr "Select the auto-reload interval for this tab." @@ -868,7 +868,7 @@ msgid "link.file.delete" msgstr "Delete File" msgid "heading.file.delete" -msgstr "DELETE FILE" +msgstr "Delete File" msgid "dlg.file.delete.confirm" msgstr "Are you sure you want to delete this file?" @@ -877,7 +877,7 @@ msgid "dlg.file.delete" msgstr "Delete" msgid "heading.openlink" -msgstr "OPEN LINK" +msgstr "Open Link" #, c-format msgid "dlg.openlink.confirm" @@ -909,7 +909,7 @@ msgid "dlg.certwarn.domain.expired" msgstr "The received certificate is expired AND for the wrong domain." msgid "heading.certimport" -msgstr "IMPORT IDENTITY" +msgstr "Import Identity" msgid "dlg.certimport.help" msgstr "Paste a PEM-encoded certificate and/or private key,\nor drop a .crt/.key file on the window." @@ -921,10 +921,10 @@ msgid "dlg.certimport.notfound.page" msgstr "No certificate/key found on the current page." msgid "heading.certimport.pasted" -msgstr "PASTED FROM CLIPBOARD" +msgstr "Pasted from Clipboard" msgid "heading.certimport.dropped" -msgstr "DROPPED FILE" +msgstr "Dropped File" # button in the mobile New Identity dialog msgid "dlg.certimport.pickfile" @@ -1091,7 +1091,7 @@ msgid "upload.port" msgstr "Port…" msgid "heading.uploadport" -msgstr "TITAN UPLOAD PORT" +msgstr "Titan Upload Port" msgid "dlg.uploadport.msg" msgstr "Set the Titan server port to use for this URL. The port is saved in the site-specific configuration." @@ -1112,7 +1112,7 @@ msgid "dlg.upload.pickfile" msgstr "Select File" msgid "heading.translate" -msgstr "TRANSLATE PAGE" +msgstr "Translate Page" msgid "dlg.translate.unavail" msgstr "Service Unavailable" @@ -1174,7 +1174,7 @@ msgid "lang.es" msgstr "Spanish" msgid "heading.newident" -msgstr "NEW IDENTITY" +msgstr "New Identity" msgid "dlg.newident.rsa.selfsign" msgstr "Creating a self-signed 2048-bit RSA certificate." @@ -1231,13 +1231,13 @@ msgid "dlg.newident.create" msgstr "Create Identity" msgid "heading.newident.missing" -msgstr "MISSING INFO" +msgstr "Missing Info" msgid "dlg.newindent.missing.commonname" msgstr "A \"Common name\" must be specified." msgid "heading.newident.date.bad" -msgstr "INVALID DATE" +msgstr "Invalid Date" msgid "dlg.newident.date.past" msgstr "Expiration date must be in the future." @@ -1246,10 +1246,10 @@ msgid "dlg.newident.date.example" msgstr "Please check the \"Valid until\" date. Examples:\n• 2030\n• 2025-06-30\n• 2021-12-31 23:59:59" msgid "heading.feedcfg" -msgstr "FEED SETTINGS" +msgstr "Feed Settings" msgid "heading.subscribe" -msgstr "SUBSCRIBE TO PAGE" +msgstr "Subscribe to Page" msgid "dlg.feed.title" msgstr "Title:" @@ -1273,10 +1273,10 @@ msgid "dlg.feed.sub" msgstr "Subscribe" msgid "heading.bookmark.add" -msgstr "ADD BOOKMARK" +msgstr "Add Bookmark" msgid "heading.bookmark.edit" -msgstr "EDIT BOOKMARK" +msgstr "Edit Bookmark" msgid "dlg.bookmark.save" msgstr "Save Bookmark" @@ -1300,7 +1300,7 @@ msgid "heading.bookmark.tags" msgstr "Special Tags" msgid "heading.addfolder" -msgstr "ADD FOLDER" +msgstr "Add Folder" msgid "dlg.addfolder.defaulttitle" msgstr "New Folder" @@ -2036,7 +2036,7 @@ msgid "fontpack.delete" msgstr "Permanently delete \"%s\"" msgid "heading.fontpack.delete" -msgstr "DELETE FONTPACK" +msgstr "Delete Fontpack" #, c-format msgid "dlg.fontpack.delete.confirm" @@ -2064,7 +2064,7 @@ msgid "truetype.help.installed" msgstr "This font is installed in the user fonts directory." msgid "heading.dismiss.warning" -msgstr "DISMISS WARNING?" +msgstr "Dismiss Warning?" #, c-format msgid "dlg.dismiss.ansi" @@ -2074,7 +2074,7 @@ msgid "dlg.dismiss.warning" msgstr "Dismiss Warning" msgid "heading.fontpack.classic" -msgstr "DOWNLOAD FONTPACK" +msgstr "Download Fontpack" msgid "dlg.fontpack.classic.msg" msgstr "The fonts previously bundled with the app are now available as a separate download. Would you like to download the \"Classic set\" fontpack now?" diff --git a/res/lang/en.bin b/res/lang/en.bin index 12cde1d0..1b26e61e 100644 Binary files a/res/lang/en.bin and b/res/lang/en.bin differ diff --git a/res/lang/eo.bin b/res/lang/eo.bin index be6c9cef..3b734982 100644 Binary files a/res/lang/eo.bin and b/res/lang/eo.bin differ diff --git a/res/lang/es_MX.bin b/res/lang/es_MX.bin index 26188c5b..3de1f106 100644 Binary files a/res/lang/es_MX.bin and b/res/lang/es_MX.bin differ diff --git a/res/lang/ia.bin b/res/lang/ia.bin index 29faea06..7772f7d0 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 9c8bc9bd..648fb372 100644 Binary files a/res/lang/ie.bin and b/res/lang/ie.bin differ diff --git a/res/lang/isv.bin b/res/lang/isv.bin index 4414fdb3..adeeb8cd 100644 Binary files a/res/lang/isv.bin and b/res/lang/isv.bin differ diff --git a/res/lang/pl.bin b/res/lang/pl.bin index aae2b705..fab560a5 100644 Binary files a/res/lang/pl.bin and b/res/lang/pl.bin differ diff --git a/res/lang/sk.bin b/res/lang/sk.bin index b43f885c..ec171de2 100644 Binary files a/res/lang/sk.bin and b/res/lang/sk.bin differ diff --git a/res/lang/tok.bin b/res/lang/tok.bin index 0202545c..95532b2a 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 648016a8..0a12f0dd 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 daa31717..ba1ff8c9 100644 Binary files a/res/lang/zh_Hant.bin and b/res/lang/zh_Hant.bin differ -- cgit v1.2.3 From 42911f7598676cceba7f2d4590c7bbe99d11e173 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 2 Jan 2022 12:25:07 +0200 Subject: Tell SDL to allow screensaver --- src/main.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.c b/src/main.c index f765f82f..e18ab065 100644 --- a/src/main.c +++ b/src/main.c @@ -67,6 +67,7 @@ int main(int argc, char **argv) { "ECDHE-RSA-AES128-GCM-SHA256:" "DHE-RSA-AES256-GCM-SHA384"); SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1"); + SDL_EnableScreenSaver(); SDL_SetHint(SDL_HINT_MAC_BACKGROUND_APP, "1"); SDL_SetHint(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, "1"); #if SDL_VERSION_ATLEAST(2, 0, 8) -- cgit v1.2.3 From 45f08515cbe6741ccb31add70575a3207e2c3e83 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 2 Jan 2022 15:20:48 +0200 Subject: Attempt to ensure invalidation of cached theme data --- src/ui/documentwidget.c | 1 + src/ui/root.c | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 2bce16e8..2fd5f148 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -3763,6 +3763,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) return iFalse; } else if (equal_Command(cmd, "theme.changed")) { + invalidatePalette_GmDocument(d->view.doc); invalidateTheme_History(d->mod.history); /* forget cached color palettes */ if (document_App() == d) { updateTheme_DocumentWidget_(d); diff --git a/src/ui/root.c b/src/ui/root.c index d5148056..af759427 100644 --- a/src/ui/root.c +++ b/src/ui/root.c @@ -549,6 +549,11 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) { root->root->pendingArrange = iFalse; return iTrue; } + else if (equal_Command(cmd, "theme.changed")) { + /* The phone toolbar is draw-buffered so it needs refreshing. */ + refresh_Widget(findWidget_App("toolbar")); + return iFalse; + } else if (handleCommand_App(cmd)) { return iTrue; } -- cgit v1.2.3 From 6ec19264f26d79009d582bd35d82e108632a326e Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 2 Jan 2022 15:21:47 +0200 Subject: Mobile: Tweaking URL input widget padding Portrait phone URL input field has some special padding rules. This should be done elsewhere (updateMetrics_Root?), but InputWidget does not check the Widget paddings currently. --- src/ui/inputwidget.c | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index 5d74f855..7ceb59c1 100644 --- a/src/ui/inputwidget.c +++ b/src/ui/inputwidget.c @@ -59,7 +59,6 @@ static const int unlimitedWidth_InputWidget_ = 1000000; /* TODO: WrapText di static const iChar sensitiveChar_ = 0x25cf; /* black circle */ static const char * sensitive_ = "\u25cf"; -#define extraPaddingHeight_ ((isPortraitPhone_App() ? 3.0f : 1.25f) * gap_UI) /* phone: proper tap target */ #define minWidth_InputWidget_ (3 * gap_UI) static void enableEditorKeysInMenus_(iBool enable) { @@ -266,6 +265,15 @@ struct Impl_InputWidget { iDefineObjectConstructionArgs(InputWidget, (size_t maxLen), maxLen) +static int extraPaddingHeight_InputWidget_(const iInputWidget *d) { + if (isPortraitPhone_App() && !cmp_String(id_Widget(&d->widget), "url")) { + /* Special case: the URL input field gets taller in portrait phone mode to make + the tap target more generous. */ + return 2.5f * gap_UI; + } + return 1.25f * gap_UI; +} + static void restoreBackup_InputWidget_(iInputWidget *d) { if (!d->backupPath) return; iFile *f = new_File(d->backupPath); @@ -370,7 +378,15 @@ static iRect contentBounds_InputWidget_(const iInputWidget *d) { shrink_Rect(&bounds, init_I2(gap_UI * (flags_Widget(w) & tight_WidgetFlag ? 1 : 2), 0)); bounds.pos.y += padding_().y / 2; if (flags_Widget(w) & extraPadding_WidgetFlag) { - bounds.pos.y += extraPaddingHeight_ / 2; + if (d->sysCtrl && !cmp_String(id_Widget(w), "url")) { + /* TODO: This is super hacky: the native UI control would be offset incorrectly. + These paddings/offsets are getting a bit ridiculous, should rethink the whole thing. + Use the Widget paddings! */ + bounds.pos.y += 1.25f * gap_UI / 2; + } + else { + bounds.pos.y += extraPaddingHeight_InputWidget_(d) / 2; + } } return bounds; } @@ -627,7 +643,7 @@ static void updateSizeForFixedLength_InputWidget_(iInputWidget *d) { /* Set a fixed size based on maximum possible width of the text. */ iBlock *content = new_Block(d->maxLen); fill_Block(content, 'M'); - int extraHeight = (flags_Widget(as_Widget(d)) & extraPadding_WidgetFlag ? extraPaddingHeight_ : 0); + int extraHeight = (flags_Widget(as_Widget(d)) & extraPadding_WidgetFlag ? extraPaddingHeight_InputWidget_(d) : 0); setFixedSize_Widget( as_Widget(d), add_I2(measure_Text(d->font, cstr_Block(content)).bounds.size, @@ -774,7 +790,7 @@ static void updateMetrics_InputWidget_(iInputWidget *d) { const int oldHeight = height_Rect(w->rect); w->rect.size.y = contentHeight_InputWidget_(d) + 3.0f * padding_().y; /* TODO: Why 3x? */ if (flags_Widget(w) & extraPadding_WidgetFlag) { - w->rect.size.y += extraPaddingHeight_; + w->rect.size.y += extraPaddingHeight_InputWidget_(d); } invalidateBuffered_InputWidget_(d); if (height_Rect(w->rect) != oldHeight) { @@ -1679,7 +1695,7 @@ static iRect bounds_InputWidget_(const iInputWidget *d) { /* There may be more visible lines than fits in the widget bounds. */ bounds.size.y = contentHeight_InputWidget_(d) + 3 * padding_().y; if (w->flags & extraPadding_WidgetFlag) { - bounds.size.y += extraPaddingHeight_; + bounds.size.y += extraPaddingHeight_InputWidget_(d); } return bounds; } -- cgit v1.2.3 From 64f4ef11e433e2400a00e2fb7fe3bc3402889a9a Mon Sep 17 00:00:00 2001 From: Olga Smirnova Date: Sat, 1 Jan 2022 15:20:33 +0000 Subject: Translated using Weblate (Interlingua) Currently translated at 92.7% (572 of 617 strings) Translation: Lagrange/User Interface Translate-URL: http://weblate.skyjake.fi/projects/lagrange/ui/ia/ --- po/ia.po | 394 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 371 insertions(+), 23 deletions(-) diff --git a/po/ia.po b/po/ia.po index a983ca71..c8ddc5ad 100644 --- a/po/ia.po +++ b/po/ia.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" -"PO-Revision-Date: 2021-07-19 19:06+0000\n" +"PO-Revision-Date: 2022-01-02 15:50+0000\n" "Last-Translator: Olga Smirnova \n" "Language-Team: Interlingua \n" @@ -133,7 +133,7 @@ msgid "toolbar.outline" msgstr "Structura del pagina" msgid "hint.findtext" -msgstr "Trovar texto in le pagina" +msgstr "trovar texto in le pagina" msgid "status.feeds" msgstr "Actualisation del syndicationes" @@ -760,8 +760,8 @@ msgstr "Certificato non valide" #, c-format msgid "ident.usedonurls" msgid_plural "ident.usedonurls.n" -msgstr[0] "Usate por %zu URL" -msgstr[1] "Usate por %zu URLs" +msgstr[0] "Usate por %u URL" +msgstr[1] "Usate por %u URLs" #, c-format msgid "dlg.import.add" @@ -926,9 +926,7 @@ msgstr "Vide %sAdjuta%s por plus de information re certificatos de cliente TLS." #, c-format msgid "dlg.confirm.unsub" -msgstr "" -"Disabonar le syndication\n" -"«%s»?" +msgstr "Disabonar le syndication «%s»?" msgid "heading.pageinfo" msgstr "INFORMATION RE LE PAGINA" @@ -1001,7 +999,7 @@ msgid "error.tls" msgstr "Fallimento del rete o TLS" msgid "error.tls.msg" -msgstr "Fallite a communicar con le servitor. Ecce le message de error:" +msgstr "Fallite a communicar con le servitor." msgid "error.slowdown" msgstr "Relentamento" @@ -1020,8 +1018,8 @@ msgstr "Monstrar tote" msgid "num.bytes" msgid_plural "num.bytes.n" -msgstr[0] "%zu byte" -msgstr[1] "%zu bytes" +msgstr[0] "%u byte" +msgstr[1] "%u bytes" msgid "prefs.doctheme.name.colorfuldark" msgstr "Colorate obscur" @@ -1211,9 +1209,8 @@ msgstr "Actualisar marcapaginas remote" #, c-format msgid "dlg.confirm.ident.delete" msgstr "" -"Vole tu vermente deler le identitate\n" -"%s%s%s\n" -"con su certificato e clave private?" +"Vole tu vermente deler le identitate %s«%s»%s con su certificato e clave " +"private?" msgid "lang.ie" msgstr "Interlingue" @@ -1241,14 +1238,14 @@ msgstr "Le contento del pagina se carga ancora." #, c-format msgid "feeds.list.counts" msgid_plural "feeds.list.counts.n" -msgstr[0] "%zu abonate syndication que contine %%s.\n" -msgstr[1] "%zu abonate syndicationes que contine %%s.\n" +msgstr[0] "%u abonate syndication que contine %%s.\n" +msgstr[1] "%u abonate syndicationes que contine %%s.\n" #, c-format msgid "feeds.list.entrycount" msgid_plural "feeds.list.entrycount.n" -msgstr[0] "%zu elemento in toto" -msgstr[1] "%zu elementos in toto" +msgstr[0] "%u elemento in toto" +msgstr[1] "%u elementos in toto" msgid "dlg.autoreload" msgstr "Interstitio de recarga automatic por iste scheda." @@ -1458,7 +1455,7 @@ msgid "menu.split.vertical" msgstr "Vertical" msgid "menu.view.split" -msgstr "Finder le vista..." +msgstr "Finder le vista…" msgid "bookmark.tag.linksplit" msgstr "Ligamines que aperi al latere" @@ -1507,9 +1504,8 @@ msgstr "Sortir le archivo" #, c-format msgid "archive.summary" msgid_plural "archive.summary.n" -msgstr[0] "Iste archivo contine %zu elemento e su dimension comprimite es %.1f MB." -msgstr[1] "" -"Iste archivo contine %zu elementos e su dimension comprimite es %.1f MB." +msgstr[0] "Iste archivo contine %u elemento e su dimension comprimite es %.1f MB." +msgstr[1] "Iste archivo contine %u elementos e su dimension comprimite es %.1f MB." msgid "dir.empty" msgstr "Iste directorio es vacue." @@ -1517,8 +1513,8 @@ msgstr "Iste directorio es vacue." #, c-format msgid "dir.summary" msgid_plural "dir.summary.n" -msgstr[0] "Iste directorio contine %zu elemento." -msgstr[1] "Iste directorio contine %zu elementos." +msgstr[0] "Iste directorio contine %u elemento." +msgstr[1] "Iste directorio contine %u elementos." msgid "keys.tab.close.other" msgstr "Clauder le altere schedas" @@ -1579,3 +1575,355 @@ msgstr "DELER LE FILE" msgid "dlg.file.delete" msgstr "Deler" + +msgid "upload.file.name" +msgstr "Nomine de file:" + +msgid "upload.file.drophere" +msgstr "(trahe e depone un file al fenestra)" + +msgid "upload.mime" +msgstr "Typo MIME:" + +msgid "upload.token" +msgstr "Token:" + +msgid "prefs.scrollspeed.mouse" +msgstr "Velocitate del mus:" + +msgid "dlg.newident.more" +msgstr "Plus…" + +msgid "dlg.fontpack.classic" +msgstr "Discargar le fontpack (25 MB)" + +msgid "prefs.returnkey.linebreak" +msgstr "Ruptura de linea" + +msgid "error.certverify" +msgstr "Servitor non fidabile" + +msgid "prefs.imagestyle" +msgstr "Colorisar imagines:" + +msgid "prefs.font.mono" +msgstr "Preformattate:" + +# Font to use for headings and body when Monospace body is enabled. +msgid "prefs.font.monodoc" +msgstr "Typo de characteres mono-spatio:" + +msgid "upload.port" +msgstr "Porta…" + +msgid "heading.uploadport" +msgstr "PORTA DE INCARGAMENTO DE TITAN" + +msgid "prefs.returnkey.accept" +msgstr "Acceptar" + +msgid "prefs.returnkey" +msgstr "Comportamento del clave Enter:" + +msgid "prefs.time.24h" +msgstr "Tempore a 24-horas" + +msgid "prefs.imagestyle.original" +msgstr "Nulle" + +msgid "prefs.imagestyle.text" +msgstr "Color del texto" + +msgid "prefs.gemtext.ansi" +msgstr "Escappamentos ANSI:" + +# Color of text background. +msgid "prefs.gemtext.ansi.bg" +msgstr "Color de fundo" + +msgid "prefs.gemtext.ansi.fontstyle" +msgstr "Stilo del characteres" + +msgid "prefs.memorysize" +msgstr "Dimension de memoria:" + +msgid "keys.upload" +msgstr "Incargar un pagina via Titan" + +msgid "error.server.msg" +msgstr "Le servitor ha respondite:" + +# Action label +msgid "fontpack.meta.viewfile" +msgstr "Vider le file" + +msgid "dlg.newident.scope" +msgstr "Usar pro:" + +msgid "menu.pageinfo" +msgstr "Monstrar information re le pagina" + +msgid "menu.save.downloads.open" +msgstr "Salvar a Discargas e aperir le file" + +msgid "prefs.scrollspeed.keyboard" +msgstr "Velocitate de claviero:" + +msgid "num.files" +msgid_plural "num.files.n" +msgstr[0] "%u dossier" +msgstr[1] "%u dossiers" + +msgid "prefs.font.ui" +msgstr "IdU:" + +msgid "fontpack.meta.disabled" +msgstr ", inactive" + +#, c-format +msgid "fontpack.enable" +msgstr "Activar «%s»" + +#, c-format +msgid "fontpack.disable" +msgstr "Disactivar «%s»" + +msgid "fontpack.export" +msgstr "Vider un patrono de fontpack.ini" + +#, c-format +msgid "fontpack.install" +msgstr "Installar «%s»" + +#, c-format +msgid "fontpack.upgrade" +msgstr "Promover «%s» al version %d" + +msgid "media.untitled.image" +msgstr "Imagine" + +msgid "media.untitled.audio" +msgstr "Audio" + +msgid "dlg.fontpack.delete" +msgstr "Deler le fontpack" + +msgid "prefs.font.body" +msgstr "Corpore:" + +msgid "heading.fontpack.meta.enabled" +msgstr "Fontpacks active" + +msgid "heading.fontpack.meta.disabled" +msgstr "Fontpacks inactive" + +msgid "menu.newfolder" +msgstr "Nove dossier…" + +# used for Preferences on mobile +msgid "menu.settings" +msgstr "Parametros" + +# keep this short (3x1 horiz layout) +msgid "menu.selectall" +msgstr "Seliger toto" + +# keep this short (3x1 horiz layout) +msgid "menu.delete" +msgstr "Deler" + +#, c-format +msgid "dlg.bookmarks.delete" +msgid_plural "dlg.bookmarks.delete.n" +msgstr[0] "Deler le marcapagina" +msgstr[1] "Deler %u marcapaginas" + +msgid "bookmark.export.count" +msgid_plural "bookmark.export.count.n" +msgstr[0] "Vos have %d marcapagina." +msgstr[1] "Vos have %d marcapaginas." + +msgid "sidebar.action.show" +msgstr "Monstrar:" + +msgid "heading.confirm.bookmarks.delete" +msgstr "DELER MARCAPAGINAS" + +msgid "menu.website" +msgstr "Sito web del projecto…" + +# keep this short (3x1 horiz layout) +msgid "menu.undo" +msgstr "Disfacer" + +msgid "fontpack.open.fontsdir" +msgstr "Aperir le directorio del typos del usator" + +msgid "fontpack.open.aboutfonts" +msgstr "Monstrar le typos de character installate" + +msgid "menu.fonts" +msgstr "Gerer typos de litteras…" + +# This label should be fairly short so it fits in a button in the sidebar. +msgid "sidebar.action.feeds.markallread" +msgstr "Toto legite" + +msgid "num.fonts" +msgid_plural "num.fonts.n" +msgstr[0] "%u typo" +msgstr[1] "%u typos" + +msgid "dlg.file.delete.confirm" +msgstr "Desira tu vermente deler iste file?" + +msgid "bookmark.export.format.linklines" +msgstr "Cata ligamine representa un marcapagina." + +msgid "menu.page.upload" +msgstr "Incargar un pagina via Titan…" + +msgid "heading.upload" +msgstr "INCARGAR VIA TITAN" + +msgid "upload.id" +msgstr "Identitate:" + +msgid "dlg.upload.id.none" +msgstr "Nulle" + +msgid "heading.upload.file" +msgstr "File" + +msgid "upload.file.size" +msgstr "Dimension de file:" + +msgid "dlg.upload.send" +msgstr "Incargar" + +# used on mobile +msgid "dlg.upload.text" +msgstr "Incargar un texto simple" + +# used on mobile +msgid "dlg.upload.file" +msgstr "Incargar un file" + +# used on mobile +msgid "dlg.upload.pickfile" +msgstr "Seliger un file" + +msgid "dlg.newident.scope.domain" +msgstr "Dominio actual" + +msgid "dlg.newident.scope.page" +msgstr "Pagina actual" + +msgid "dlg.newident.scope.none" +msgstr "Non usate" + +msgid "dlg.feed.ignoreweb" +msgstr "Ignorar ligamines HTTP(S):" + +msgid "dlg.bookmark.folder" +msgstr "Dossier:" + +msgid "heading.bookmark.tags" +msgstr "ETIQUETTAS SPECIAL" + +msgid "heading.addfolder" +msgstr "ADDER UN DOSSIER" + +msgid "dlg.addfolder.defaulttitle" +msgstr "Nove dossier" + +msgid "menu.update" +msgstr "Controlar pro le actualisationes…" + +msgid "status.query.tight" +msgstr "Requesta" + +msgid "dlg.addfolder" +msgstr "Adder un dossier" + +# used on mobile +msgid "heading.settings" +msgstr "PARAMETROS" + +msgid "prefs.font.heading" +msgstr "Capites:" + +msgid "prefs.boldlink.visited" +msgstr "Visitate" + +msgid "prefs.font.warnmissing" +msgstr "Advertimentos de glyphos:" + +msgid "prefs.linespacing" +msgstr "Spatiamento inter lineas:" + +msgid "error.certexpired" +msgstr "Certificato perimite" + +msgid "error.glyphs" +msgstr "Glyphos mancante" + +msgid "heading.fontpack.meta" +msgstr "Typos de character" + +#, c-format +msgid "fontpack.meta.version" +msgstr "Version %d" + +msgid "fontpack.meta.installed" +msgstr "Installate" + +msgid "fontpack.meta.notinstalled" +msgstr "Non installate" + +msgid "heading.fontpack.delete" +msgstr "DELER UN FONTPACK" + +msgid "fontpack.install.ttf" +msgstr "Installar un typo de litteras TrueType" + +msgid "dlg.dismiss.warning" +msgstr "Dimitter le advertimento" + +msgid "heading.fontpack.classic" +msgstr "DISCARGAR UN FONTPACK" + +msgid "prefs.imagestyle.preformat" +msgstr "Color de preformattate" + +#, c-format +msgid "fontpack.delete" +msgstr "Deler permanentemente «%s»" + +msgid "dlg.certwarn.title" +msgstr "Problema de securitate" + +# button in the mobile New Identity dialog +msgid "dlg.certimport.pickfile" +msgstr "Importar un certificato o un file clave" + +msgid "bookmark.export.title.folder" +msgstr "Marcapaginas" + +msgid "bookmark.export.title.tag" +msgstr "Etiquettas de marcapaginas" + +msgid "prefs.imagestyle.grayscale" +msgstr "Scala de gris" + +msgid "error.ansi" +msgstr "Emulation de terminal" + +msgid "dlg.upload.id.default" +msgstr "Predefinite" + +msgid "heading.upload.text" +msgstr "Texto" + +msgid "hint.upload.text" +msgstr "Insere texto a incargar" -- cgit v1.2.3 From f975714ca43f668d72eed26ea1a90de3991430b8 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 2 Jan 2022 18:40:21 +0200 Subject: Android: Added release notes and Help page --- CMakeLists.txt | 5 +++++ res/about/android-help.gmi | 47 +++++++++++++++++++++++++++++++++++++++++++ res/about/android-version.gmi | 24 ++++++++++++++++++++++ src/resources.c | 3 +++ 4 files changed, 79 insertions(+) create mode 100644 res/about/android-help.gmi create mode 100644 res/about/android-version.gmi diff --git a/CMakeLists.txt b/CMakeLists.txt index 0312e115..70c76505 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -109,6 +109,11 @@ if (IOS) res/about/ios-help.gmi res/about/ios-version.gmi ) +elseif (ANDROID) + list (APPEND RESOURCES + res/about/android-help.gmi + res/about/android-version.gmi + ) else () list (APPEND RESOURCES res/about/help.gmi diff --git a/res/about/android-help.gmi b/res/about/android-help.gmi new file mode 100644 index 00000000..1425b6bc --- /dev/null +++ b/res/about/android-help.gmi @@ -0,0 +1,47 @@ +```LAGRANGE + __ __ __ ___ +| /\ / _` |__) /\ |\ | / _` |__ +|___ /~~\ \__> | \ /~~\ | \| \__> |___ + +``` +# Help + +## What is Gemini + +Gemini is a simple protocol for serving content over the internet. It specifies a Markdown inspired format allowing basic plain text document markup. Compared to HTTP and HTML, Gemini is vastly simpler and easier to work with. + +=> gemini://gemini.circumlunar.space/docs/faq.gmi Project Gemini FAQ +=> gemini://gemini.circumlunar.space/docs/specification.gmi Protocol and 'text/gemini' specification + +## What is Lagrange + +Lagrange is a GUI client for browsing Geminispace. It offers modern conveniences familiar from web browsers, such as smooth scrolling, inline image viewing, multiple tabs, visual themes, Unicode fonts, bookmarks, history, and page outlines. + +Like Gemini, Lagrange has been designed with minimalism in mind. It depends on a small number of essential libraries. It is written in C and uses SDL for hardware-accelerated graphics. OpenSSL is used for secure communications. + +=> about:lagrange About Lagrange +=> https://www.libsdl.org SDL: Simple DirectMedia Layer +=> https://www.openssl.org OpenSSL: Cryptography and SSL/TLS Toolkit + +### Features + +* Beautiful typography with full Unicode support +* Autogenerated page style and symbol for each Gemini domain +* Light and dark color themes +* Scaling page content (50%...200%) +* Freely adjustable scaling factor for the UI +* Sidebar for managing bookmarks, subscribed feeds, and viewing browsing history and the page outline +* Multiple tabs +* Split view for browsing two pages at once (iPad only) +* Find text on the page +* Open image and audio links inline on the same page +* Smart suggestions when typing an URL — search bookmarks, history, identities +* Search engine integration +* Identity management — create and use TLS client certificates +* Subscribe to Gemini and Atom feeds +* Use Gemini pages as a source of bookmarks +* Audio playback: MP3, Ogg Vorbis, WAV +* Read Gempub books and view ZIP archive contents +* Built-in support for Gopher +* Built-in support for uploading data using the Titan protocol +* Use proxy servers for HTTP, Gopher, or Gemini content diff --git a/res/about/android-version.gmi b/res/about/android-version.gmi new file mode 100644 index 00000000..a7b87d47 --- /dev/null +++ b/res/about/android-version.gmi @@ -0,0 +1,24 @@ +```LAGRANGE + __ __ __ ___ +| /\ / _` |__) /\ |\ | / _` |__ +|___ /~~\ \__> | \ /~~\ | \| \__> |___ + +``` +# Release notes + +## 1.10 (Alpha 3) +* Added Android-specific release notes. +* Added Settings > UI > Toolbar Actions: customize the two leftmost phone toolbar buttons. +* Show build version in Settings > About. +* Fixed sizing of the UI when the device has on-screen system keys. +* Fixed the copy/paste menu staying hidden behind the keyboard. +* Fixed system Auto-Rotate setting not locking the screen orientation. + +## 1.10 (Alpha 2) +* Upgraded SDL to 2.0.8, which fixed a lot of stability issues. +* Added native binaries for arm64, x86, and x86_64. +* Enabled bidirectional text and complex scripts (with HarfBuzz and FriBidi). +* Enabled switching to landscape orientation. + +## 1.10 (Alpha 1) +Initial test build with SDL 2.0.5 and 32-bit ARM binaries. It works, but barely. \ No newline at end of file diff --git a/src/resources.c b/src/resources.c index 80af4b4b..fb234f6a 100644 --- a/src/resources.c +++ b/src/resources.c @@ -70,6 +70,9 @@ static struct { #if defined (iPlatformAppleMobile) { &blobHelp_Resources, "about/ios-help.gmi" }, { &blobVersion_Resources, "about/ios-version.gmi" }, +#elif defined (iPlatformAndroidMobile) + { &blobHelp_Resources, "about/android-help.gmi" }, + { &blobVersion_Resources, "about/android-version.gmi" }, #else { &blobHelp_Resources, "about/help.gmi" }, { &blobVersion_Resources, "about/version.gmi" }, -- cgit v1.2.3 From 0d1bc0e469e8ca4b2cd3d269974c964e0f2916b0 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Mon, 3 Jan 2022 07:34:59 +0200 Subject: Mobile: Updated Help --- res/about/android-help.gmi | 128 +++++++++++++++++++++++++++++++++- res/about/android-version.gmi | 2 +- res/about/help.gmi | 4 +- res/about/ios-help.gmi | 157 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 287 insertions(+), 4 deletions(-) diff --git a/res/about/android-help.gmi b/res/about/android-help.gmi index 1425b6bc..48835967 100644 --- a/res/about/android-help.gmi +++ b/res/about/android-help.gmi @@ -32,7 +32,6 @@ Like Gemini, Lagrange has been designed with minimalism in mind. It depends on a * Freely adjustable scaling factor for the UI * Sidebar for managing bookmarks, subscribed feeds, and viewing browsing history and the page outline * Multiple tabs -* Split view for browsing two pages at once (iPad only) * Find text on the page * Open image and audio links inline on the same page * Smart suggestions when typing an URL — search bookmarks, history, identities @@ -45,3 +44,130 @@ Like Gemini, Lagrange has been designed with minimalism in mind. It depends on a * Built-in support for Gopher * Built-in support for uploading data using the Titan protocol * Use proxy servers for HTTP, Gopher, or Gemini content + +# Work in progress! + +This documentation is a work in progress. You may find it useful to alse see the desktop Help page: +=> gemini://git.skyjake.fi:1968/lagrange/release/res/about/help.gmi Help page (desktop version, latest release) + +# 1 User interface + +## 1.6 Identities (client certificates) + +Gemini uses TLS client certificates for user/session identification purposes. Unlike on the web where user identity tracking is covert and automatic, client certificates must be manually taken into use, and you are able to define how long each certificate remains valid. The term "Identity" is used in Lagrange to refer to client certificates. + +The Identities sidebar tab shows all identities known to Lagrange. This includes identities created in Lagrange and any identities based on imported X.509 certificates. + +### 1.6.1 Creating a new identity + +Click on the 👤 button in the toolbar and select "New Identity...". + +Consider any information you enter in the certificate as public — only the Common Name is required to be non-empty. The generated certificate will use the Common Name as the issuer and subject of the certificate, making it clear that the certificate is self-signed. The other required field is the expiration date in "Valid until". Entering a year is sufficient, and means that the certificate is valid until the end of that year. + +### 1.6.2 Using an identity + +You will need to select an identity when you encounter this error message: + +> 🔑 Certificate Required + +Go to Settings > Identities and tap on an identity to toggle it on/off for the currently open URL. On subsequent page loads, the certificate will then be sent to the server when the URL or any URL under it is fetched. You can click on the 👤 button in the navigation bar to see which identity is being used for the current page. + +As the sidebar is not keyboard-navigable, note that identities can also be accessed via lookup results. Identities matching the search terms are shown as the last category in the lookup results list. From there, one can toggle an identity for the current page, or stop using it on all pages. + +### 1.6.3 Importing existing certificates + +To import an existing X.509 certificate as an identity, click on 👤 and select "Import...". + +When the Import Identity dialog opens, it checks the currently open page and the system clipboard for any certificates and private keys in PEM format. If found, these are automatically loaded in. + +## 1.8 Uploads (with Titan) + +Titan is a sister protocol to Gemini that enables sending arbitrary amounts of data from a client to a server. The Gemini protocol itself only enables sending up to 1024 bytes of data in a request. Furthermore, the request URL also counts against that limit, and the sent data must be percent-encoded so it can be parsed as a valid URL. Consequently, Gemini clients can only send very limited amounts of data to a server. Titan solves this by expanding the request so that the request URL is followed by a payload field. When it comes to TLS, Titan is equivalent to Gemini, so the same server and client certificates can be used with both. + +=> gemini://transjovian.org/titan Titan Protocol (by Alex Schroeder) + +While Titan and Gemini are related, Titan is a separate protocol and regular Gemini servers are not expected to support it. Whether it makes sense to allow clients to upload large amounts of data is a service-specific question. For example, a server that hosts a gemlog could enable Titan uploads for submitting new posts, or editing existing posts by uploading a revised version. + +As far as Lagrange is concerned, Titan is just one of the supported URL schemes. Whenever you try to open a "titan://" URL, no matter if it is manually entered into the URL field, or by clicking on a link, opening a bookmark, feed entry, or via a redirect, a dialog will open where you can specify the data to upload. + +The Titan upload dialog supports two ways to enter the data: + +* You can type text in the input field on the "Text" tab. It will be sent to the server using 'text/plain' as the media type. +* You can drag and drop a file on the dialog. The details of the file to be uploaded are visible on the "File" tab. The media type can be specified manually if Lagrange does not correctly detect it. + +The upload token is a feature of Titan where servers can require a certain token text/passphrase for uploads. It is up to the server how this is interpreted. It could be used as a simple password, or even a command to further instruct the server about what to do with the uploaded data. Please refer to the server's instructions about what to enter here. The token may also be left empty. + +The text entered into the upload dialog's main text field is protected against accidental closing of the dialog or the application, or a crash. The previous text is restored when the dialog is reopened. The text field contents are only cleared when the submitted Titan request has been successfully completed. + +# 2 Customization + +## 2.1 Browsing behavior + +The "Open archive indices" option controls whether index.gmi pages are automatically opened while browsing the contents of a ZIP archive. The purpose is to simulate the behavior of a Gemini server where opening a directory will by default show its index page. Enabling this option makes navigating an archived copy of a capsule a more streamlined experience. + +## 2.4 Fonts + +This version of Lagrange supports TrueType fonts. To use a new font, simply view a .ttf (or .fontpack) file in the app and a page footer action is available for performing the installation. + +The "Fonts" page of Settings contains options that affect the appearance of text in page content, the user interface, and text rendering in general. + +To be precise, on this tab you can select which _typefaces_ to use for certain elements of the page and the UI; picking a particular font (including its size, weight, style, etc.) for specific elements is not possible. Lagrange controls the size and styling of text by choosing fonts according to your theme and typeface preferences. The term "font" is often used to mean "typeface" in this document and in the app UI. + +The fonts for document headings, body text, preformatted blocks, and the user interface can be chosen separately. There is also a separate font for monospaced body text, used when the protocol-specific "Monospace body" option is enabled. + +There are two built-in fonts available: + +* "Source Sans" is the default UI font that is also used for page content. +* "Iosevka" is the monospaced font. + +There is also an "Iosevka (compact)" variant that actually uses the font's original line spacing. It is the default for preformatted blocks to avoid gaps between lines of ASCII art, but not for body text for better legibility. + +See sections 2.4.4 and 5 for more details about font management, compatibility, and configuration. + +### 2.4.1 Monospace body + +The "Monospace body" option causes all pages to be displayed in a monospace font. For example, most Gopher content has been written with the assumption of a monospace font, so it may provide a better reading experience to enable this for Gopher pages. The selected "Monospace font" is applied to the entire document. + +Handling of whitespace in page content also changes when this option is enabled: spaces are no longer normalized so if the source uses multiple spaces somewhere, those are shown as-is. + +### 2.4.2 ANSI escapes + +=> https://en.wikipedia.org/wiki/ANSI_escape_code ANSI escape code (Wikipedia): +> ANSI escape sequences are a standard for in-band signaling to control cursor location, color, font styling, and other options on video text terminals and terminal emulators. + +Sometimes these codes are used for things like colored ASCII art. However, Lagrange is not a terminal emulator so only a very minimal set of codes have any effect; the rest are ignored. + +The "ANSI escapes" setting controls which ANSI escape codes are enabled: + +* "FG Color" enables changes to text foreground color. +* "BG Color" enables changes to text background color. +* "Font Style" enables changing the font style: bold, italic, or monospace. (These correspond to codes 1, 3, and 11.) + +A warning banner is displayed if any codes are detected on a page. This helps you be aware of potential visual artifacts, like text color that is being forced to black regardless of the page background color. Click on the warning to dismiss it on a per-site basis. + +If you serve content via Gemini, please be aware that ANSI escapes may not be supported by clients, and may in fact disrupt the behavior of a client in unexpected ways. For instance, consider a screen reader or a web proxy that doesn't filter out the codes. Only employ ANSI escapes when the user has somehow indicated that is their preference. + +### 2.4.3 Other font options + +The "Glyph warnings" option controls whether a warning will be shown when a page is missing one or more characters. This is typically due to the page being in a language that is not covered by any of the installed fonts. Clicking on the glyph warning banner opens the font management page. + +"Smoothing" enables or disables glyph antialiasing. In this version, it is recommended that smoothing is always enabled. This is because the text renderer does not support hinting of any kind, which means that without smoothing, glyph shapes will be severely distorted. You may still want to try disabling this option if antialiasing causes eye strain for you. + +### 2.4.4 Managing fonts + +Open "about:fonts" to see all the installed fonts, and enable, disable or uninstall individual fontpacks. At the top of the page, there is a link to the skyjake.fi Font Library: a curated collection of fontpacks that can be freely distributed. From there you can conveniently install more fonts specifically tuned for Lagrange. + +The rest of the page is divided to two sections: enabled and disabled fontpacks. Individual packs can be disabled, so they remain installed but not in use. (Tip: Use the Outline sidebar to see a list of all packs, if there are many.) + +Click on the "View file" links to see what each fontpack contains. The fontpack format is described in section 5: it is simply a ZIP archive with some metadata and a bunch of font files. When you install a TrueType font (i.e., copy it to the user's fonts directory), it is treated as a fontpack containing a single file, although no ZIP archive is created for it. Use the "View fontpack.ini template" link to see the generated metadata that can be used as a basis for creating an actual fontpack. + +About compatibility: fonts are complex, and while the TrueType format is not the most advanced one, it still has features that are not supported in Lagrange. The technical reason is that glyphs are rasterized using stb_truetype, a rather simple TrueType font library. (It's nice and small, though.) You may find that some fonts simply fail to load correctly, or look wrong in the app. + +* Hinting is not supported. +* Bitmap glyphs are not supported. +* Multi-color glyphs are not supported. + +TrueType Collections (.ttc) can be used, with the restriction that each declared font uses a single index from the collection. The index must be appended to the file name: +```.ttc example +regular = "NotoSansCJK-Regular.ttc:2" +``` diff --git a/res/about/android-version.gmi b/res/about/android-version.gmi index a7b87d47..610e35b0 100644 --- a/res/about/android-version.gmi +++ b/res/about/android-version.gmi @@ -15,7 +15,7 @@ * Fixed system Auto-Rotate setting not locking the screen orientation. ## 1.10 (Alpha 2) -* Upgraded SDL to 2.0.8, which fixed a lot of stability issues. +* Upgraded SDL to 2.0.18, which fixed a lot of stability issues. * Added native binaries for arm64, x86, and x86_64. * Enabled bidirectional text and complex scripts (with HarfBuzz and FriBidi). * Enabled switching to landscape orientation. diff --git a/res/about/help.gmi b/res/about/help.gmi index 7c904bd9..b753cb39 100644 --- a/res/about/help.gmi +++ b/res/about/help.gmi @@ -253,7 +253,7 @@ You will need to select an identity when you encounter this error message: > 🔑 Certificate Required -Clicking on an identity in the sidebar will toggle it on/off for the currently open URL. On subsequent page loads, the certificate will then be sent to the server when the URL or any URL under it is fetched. You can click on the 👤 button in the navigation bar to see which identity is being used for the current page. +Clicking on an identity in the sidebar shows a menu where you can control usage of the identity for the currently open URL. On subsequent page loads, the certificate will then be sent to the server when the URL or any URL under it is fetched. You can click on the 👤 button in the navigation bar to see which identity is being used for the current page. As the sidebar is not keyboard-navigable, note that identities can also be accessed via lookup results. Identities matching the search terms are shown as the last category in the lookup results list. From there, one can toggle an identity for the current page, or stop using it on all pages. @@ -390,7 +390,7 @@ Page content color themes are selected on the "Colors" tab of Preferences. The " ## 2.4 Fonts -This version of Lagrange supports TrueType fonts. To use a new font in the app, simply view the .ttf file in the app and a page footer action is available for performing the installation. For example, try drag-and-dropping a .ttf file on the window. Alternatively, you can manually copy the font to the "fonts" subdirectory of the user-specific configuration directory (see section 3.5). +This version of Lagrange supports TrueType fonts. To use a new font, simply view a .ttf file in the app and a page footer action is available for performing the installation. For example, try drag-and-dropping a .ttf file on the window. Alternatively, you can manually copy the font to the "fonts" subdirectory of the user-specific configuration directory (see section 3.5). The "Fonts" tab of the Preferences dialog contains options that affect the appearance of text in page content, the user interface, and text rendering in general. diff --git a/res/about/ios-help.gmi b/res/about/ios-help.gmi index 1425b6bc..41a8445b 100644 --- a/res/about/ios-help.gmi +++ b/res/about/ios-help.gmi @@ -45,3 +45,160 @@ Like Gemini, Lagrange has been designed with minimalism in mind. It depends on a * Built-in support for Gopher * Built-in support for uploading data using the Titan protocol * Use proxy servers for HTTP, Gopher, or Gemini content + +# Work in progress! + +This documentation is a work in progress. You may find it useful to alse see the desktop Help page: +=> gemini://git.skyjake.fi:1968/lagrange/release/res/about/help.gmi Help page (desktop version, latest release) + +# 1 User interface + +## 1.6 Identities (client certificates) + +Gemini uses TLS client certificates for user/session identification purposes. Unlike on the web where user identity tracking is covert and automatic, client certificates must be manually taken into use, and you are able to define how long each certificate remains valid. The term "Identity" is used in Lagrange to refer to client certificates. + +The Identities sidebar tab shows all identities known to Lagrange. This includes identities created in Lagrange and any identities based on imported X.509 certificates. + +### 1.6.1 Creating a new identity + +Click on the 👤 button in the toolbar and select "New Identity...". + +Consider any information you enter in the certificate as public — only the Common Name is required to be non-empty. The generated certificate will use the Common Name as the issuer and subject of the certificate, making it clear that the certificate is self-signed. The other required field is the expiration date in "Valid until". Entering a year is sufficient, and means that the certificate is valid until the end of that year. + +### 1.6.2 Using an identity + +You will need to select an identity when you encounter this error message: + +> 🔑 Certificate Required + +Go to Settings > Identities and tap on an identity to toggle it on/off for the currently open URL. On subsequent page loads, the certificate will then be sent to the server when the URL or any URL under it is fetched. You can click on the 👤 button in the navigation bar to see which identity is being used for the current page. + +As the sidebar is not keyboard-navigable, note that identities can also be accessed via lookup results. Identities matching the search terms are shown as the last category in the lookup results list. From there, one can toggle an identity for the current page, or stop using it on all pages. + +### 1.6.3 Importing existing certificates + +To import an existing X.509 certificate as an identity, click on 👤 and select "Import...". + +When the Import Identity dialog opens, it checks the currently open page and the system clipboard for any certificates and private keys in PEM format. If found, these are automatically loaded in. + +## 1.8 Uploads (with Titan) + +Titan is a sister protocol to Gemini that enables sending arbitrary amounts of data from a client to a server. The Gemini protocol itself only enables sending up to 1024 bytes of data in a request. Furthermore, the request URL also counts against that limit, and the sent data must be percent-encoded so it can be parsed as a valid URL. Consequently, Gemini clients can only send very limited amounts of data to a server. Titan solves this by expanding the request so that the request URL is followed by a payload field. When it comes to TLS, Titan is equivalent to Gemini, so the same server and client certificates can be used with both. + +=> gemini://transjovian.org/titan Titan Protocol (by Alex Schroeder) + +While Titan and Gemini are related, Titan is a separate protocol and regular Gemini servers are not expected to support it. Whether it makes sense to allow clients to upload large amounts of data is a service-specific question. For example, a server that hosts a gemlog could enable Titan uploads for submitting new posts, or editing existing posts by uploading a revised version. + +As far as Lagrange is concerned, Titan is just one of the supported URL schemes. Whenever you try to open a "titan://" URL, no matter if it is manually entered into the URL field, or by clicking on a link, opening a bookmark, feed entry, or via a redirect, a dialog will open where you can specify the data to upload. + +The Titan upload dialog supports two ways to enter the data: + +* You can type text in the input field on the "Text" tab. It will be sent to the server using 'text/plain' as the media type. +* You can drag and drop a file on the dialog. The details of the file to be uploaded are visible on the "File" tab. The media type can be specified manually if Lagrange does not correctly detect it. + +The upload token is a feature of Titan where servers can require a certain token text/passphrase for uploads. It is up to the server how this is interpreted. It could be used as a simple password, or even a command to further instruct the server about what to do with the uploaded data. Please refer to the server's instructions about what to enter here. The token may also be left empty. + +The text entered into the upload dialog's main text field is protected against accidental closing of the dialog or the application, or a crash. The previous text is restored when the dialog is reopened. The text field contents are only cleared when the submitted Titan request has been successfully completed. + +## 1.9 Split view mode (iPad only) + +By default, only one tab is visible at a time in the application window. However, sometimes it is beneficial to see two pages at once. For example, many capsules have top-level menus or lists of articles, and keeping the menu/index visible on the side makes navigation less cumbersome. + +Split view mode divides the UI into two equivalent parts. You can have multiple tabs in each split. Closing all tabs on one side will remove the split and return back to the normal unsplit mode. + +Each split has its own sidebars, which means that in split view mode you can have a total of four sidebars open at the same time. + +### 1.9.1 Switching focus + +At any given time, one of the splits has input focus. This is indicated by a colored line at the top of the section, and some UI elements will be dimmed out on the unfocused side. + +If a keyboard is connected, you can use the Next/Previous Tab keybindings or Ctrl+Tab to switch keyboard focus between the sections. Next/Previous Tab is particularly convenient as it cycles through all open tabs, jumping to the other side of the split when appropriate. You may also press Tab to cycle input focus between all the URL input fields. + +### 1.9.2 Pinning + +While it is sometimes useful to simply have two independent browsers open side by side, by default view splitting is meant to assist in navigating hierarchies and lists. In the typical use case, you'll have a menu or an index page on the left, and a content page open on the right. Links clicked on the left will automatically open on the right. + +This is called "pinning" and the behavior can be configured in Settings. The "Split view pinning" setting on the "General" page of Settings controls where links get opened in a split view. There are three modes available: + +* "None" causes links to open in the tab where the link is clicked. In this mode, both sides of the split can be navigated independently. +* "Left Tab" causes links clicked on the left tab to open on the right side. The page open in the left tab is therefore "pinned" and does not change unless you enter a new URL or navigate to the parent or root. +* "Right Tab" is the same but works the other way around. The page open in the right tab is pinned and clicked links open on the left. + +The default pinning mode is "Left Tab". + +The ◧ indicator is shown in the URL input field when the current tab is pinned. + +# 2 Customization + +## 2.1 Browsing behavior + +The "Open archive indices" option controls whether index.gmi pages are automatically opened while browsing the contents of a ZIP archive. The purpose is to simulate the behavior of a Gemini server where opening a directory will by default show its index page. Enabling this option makes navigating an archived copy of a capsule a more streamlined experience. + +"Split view pinning" controls which tab links will be opened on when browsing in split view mode. The default mode is "Left Tab", which means that the page in the left tab is pinned (remains unchanged) when clicking on a link. For more information, see section 1.9. + +## 2.4 Fonts + +This version of Lagrange supports TrueType fonts. To use a new font, simply view a .ttf (or .fontpack) file in the app and a page footer action is available for performing the installation. + +The "Fonts" page of Settings contains options that affect the appearance of text in page content, the user interface, and text rendering in general. + +To be precise, on this tab you can select which _typefaces_ to use for certain elements of the page and the UI; picking a particular font (including its size, weight, style, etc.) for specific elements is not possible. Lagrange controls the size and styling of text by choosing fonts according to your theme and typeface preferences. The term "font" is often used to mean "typeface" in this document and in the app UI. + +The fonts for document headings, body text, preformatted blocks, and the user interface can be chosen separately. There is also a separate font for monospaced body text, used when the protocol-specific "Monospace body" option is enabled. + +There are two built-in fonts available: + +* "Source Sans" is the default UI font that is also used for page content. +* "Iosevka" is the monospaced font. + +There is also an "Iosevka (compact)" variant that actually uses the font's original line spacing. It is the default for preformatted blocks to avoid gaps between lines of ASCII art, but not for body text for better legibility. + +See sections 2.4.4 and 5 for more details about font management, compatibility, and configuration. + +### 2.4.1 Monospace body + +The "Monospace body" option causes all pages to be displayed in a monospace font. For example, most Gopher content has been written with the assumption of a monospace font, so it may provide a better reading experience to enable this for Gopher pages. The selected "Monospace font" is applied to the entire document. + +Handling of whitespace in page content also changes when this option is enabled: spaces are no longer normalized so if the source uses multiple spaces somewhere, those are shown as-is. + +### 2.4.2 ANSI escapes + +=> https://en.wikipedia.org/wiki/ANSI_escape_code ANSI escape code (Wikipedia): +> ANSI escape sequences are a standard for in-band signaling to control cursor location, color, font styling, and other options on video text terminals and terminal emulators. + +Sometimes these codes are used for things like colored ASCII art. However, Lagrange is not a terminal emulator so only a very minimal set of codes have any effect; the rest are ignored. + +The "ANSI escapes" setting controls which ANSI escape codes are enabled: + +* "FG Color" enables changes to text foreground color. +* "BG Color" enables changes to text background color. +* "Font Style" enables changing the font style: bold, italic, or monospace. (These correspond to codes 1, 3, and 11.) + +A warning banner is displayed if any codes are detected on a page. This helps you be aware of potential visual artifacts, like text color that is being forced to black regardless of the page background color. Click on the warning to dismiss it on a per-site basis. + +If you serve content via Gemini, please be aware that ANSI escapes may not be supported by clients, and may in fact disrupt the behavior of a client in unexpected ways. For instance, consider a screen reader or a web proxy that doesn't filter out the codes. Only employ ANSI escapes when the user has somehow indicated that is their preference. + +### 2.4.3 Other font options + +The "Glyph warnings" option controls whether a warning will be shown when a page is missing one or more characters. This is typically due to the page being in a language that is not covered by any of the installed fonts. Clicking on the glyph warning banner opens the font management page. + +"Smoothing" enables or disables glyph antialiasing. In this version, it is recommended that smoothing is always enabled. This is because the text renderer does not support hinting of any kind, which means that without smoothing, glyph shapes will be severely distorted. You may still want to try disabling this option if antialiasing causes eye strain for you. + +### 2.4.4 Managing fonts + +Open "about:fonts" to see all the installed fonts, and enable, disable or uninstall individual fontpacks. At the top of the page, there is a link to the skyjake.fi Font Library: a curated collection of fontpacks that can be freely distributed. From there you can conveniently install more fonts specifically tuned for Lagrange. + +The rest of the page is divided to two sections: enabled and disabled fontpacks. Individual packs can be disabled, so they remain installed but not in use. (Tip: Use the Outline sidebar to see a list of all packs, if there are many.) + +Click on the "View file" links to see what each fontpack contains. The fontpack format is described in section 5: it is simply a ZIP archive with some metadata and a bunch of font files. When you install a TrueType font (i.e., copy it to the user's fonts directory), it is treated as a fontpack containing a single file, although no ZIP archive is created for it. Use the "View fontpack.ini template" link to see the generated metadata that can be used as a basis for creating an actual fontpack. + +About compatibility: fonts are complex, and while the TrueType format is not the most advanced one, it still has features that are not supported in Lagrange. The technical reason is that glyphs are rasterized using stb_truetype, a rather simple TrueType font library. (It's nice and small, though.) You may find that some fonts simply fail to load correctly, or look wrong in the app. + +* Hinting is not supported. +* Bitmap glyphs are not supported. +* Multi-color glyphs are not supported. + +TrueType Collections (.ttc) can be used, with the restriction that each declared font uses a single index from the collection. The index must be appended to the file name: +```.ttc example +regular = "NotoSansCJK-Regular.ttc:2" +``` -- cgit v1.2.3 From e3d8f675f76e86738b25a344b1c087f64bbc8ac3 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Mon, 3 Jan 2022 07:43:56 +0200 Subject: Android: Default return key behavior is to insert a newline --- src/app.c | 6 ++++++ src/defs.h | 7 ++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/app.c b/src/app.c index 1e66c6cf..7497d6f8 100644 --- a/src/app.c +++ b/src/app.c @@ -404,6 +404,12 @@ static void loadPrefs_App_(iApp *d) { insert_StringSet(d->prefs.disabledFontPacks, collect_String(suffix_Command(cmd, "id"))); } +#if defined (iPlatformAndroidMobile) + else if (equal_Command(cmd, "returnkey.set")) { + /* Hardcoded to avoid accidental presses of the virtual Return key. */ + d->prefs.returnKey = default_ReturnKeyBehavior; + } +#endif #if !defined (LAGRANGE_ENABLE_DOWNLOAD_EDIT) else if (equal_Command(cmd, "downloads")) { continue; /* can't change downloads directory */ diff --git a/src/defs.h b/src/defs.h index 25c0ceeb..d2e13294 100644 --- a/src/defs.h +++ b/src/defs.h @@ -86,7 +86,7 @@ enum iToolbarAction { /* Return key behavior is not handled via normal bindings because only certain combinations are valid. */ enum iReturnKeyBehavior { - default_ReturnKeyBehavior = + acceptWithoutMod_ReturnKeyBehavior = shiftReturn_ReturnKeyFlag | (return_ReturnKeyFlag << accept_ReturnKeyFlag), acceptWithShift_ReturnKeyBehavior = return_ReturnKeyFlag | (shiftReturn_ReturnKeyFlag << accept_ReturnKeyFlag), @@ -96,6 +96,11 @@ enum iReturnKeyBehavior { #else return_ReturnKeyFlag | (controlReturn_ReturnKeyFlag << accept_ReturnKeyFlag), #endif +#if defined (iPlatformAndroidMobile) + default_ReturnKeyBehavior = acceptWithShift_ReturnKeyBehavior, +#else + default_ReturnKeyBehavior = acceptWithoutMod_ReturnKeyBehavior, +#endif }; int keyMod_ReturnKeyFlag (int flag); -- cgit v1.2.3 From ff6bbf826196892418fad213ca1313e8b1134f18 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Mon, 3 Jan 2022 09:10:01 +0200 Subject: Android: Fixed glitches with updating keyboard height --- src/ui/window.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/ui/window.c b/src/ui/window.c index ef941798..af36bb22 100644 --- a/src/ui/window.c +++ b/src/ui/window.c @@ -1023,8 +1023,12 @@ iBool processEvent_Window(iWindow *d, const SDL_Event *ev) { | fullheight */ - setKeyboardHeight_MainWindow(mw, argLabel_Command(cmd, "top") + - mw->maxDrawableHeight - argLabel_Command(cmd, "bottom")); + const int top = argLabel_Command(cmd, "top"); + const int bottom = argLabel_Command(cmd, "bottom"); + if (!SDL_IsScreenKeyboardShown(mw->base.win)) { + mw->maxDrawableHeight = bottom - top; + } + setKeyboardHeight_MainWindow(mw, top + mw->maxDrawableHeight - bottom); return iTrue; } if (processEvent_Touch(&event)) { @@ -1475,6 +1479,7 @@ iBool isOpenGLRenderer_Window(void) { } void setKeyboardHeight_MainWindow(iMainWindow *d, int height) { + height = iMax(0, height); if (d->keyboardHeight != height) { d->keyboardHeight = height; postCommandf_App("keyboard.changed arg:%d", height); -- cgit v1.2.3 From 60776cc0385dd0cddeb2faa6d78e094cfa45bb73 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Mon, 3 Jan 2022 09:13:43 +0200 Subject: Android: Build date and release notes --- CMakeLists.txt | 2 +- res/about/android-version.gmi | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 70c76505..211da6db 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,7 +31,7 @@ endif () if (ANDROID) set (PROJECT_VERSION 1.10) set (ANDROID_BUILD_VERSION a3) # remember to update Gradle, AndroidManifest.xml - set (ANDROID_BUILD_DATE "2022-01-01") + set (ANDROID_BUILD_DATE "2022-01-03") endif () # Defaults that depend on environment. diff --git a/res/about/android-version.gmi b/res/about/android-version.gmi index 610e35b0..fc7b444b 100644 --- a/res/about/android-version.gmi +++ b/res/about/android-version.gmi @@ -10,6 +10,7 @@ * Added Android-specific release notes. * Added Settings > UI > Toolbar Actions: customize the two leftmost phone toolbar buttons. * Show build version in Settings > About. +* Changed return key behavior to insert newlines where possible. * Fixed sizing of the UI when the device has on-screen system keys. * Fixed the copy/paste menu staying hidden behind the keyboard. * Fixed system Auto-Rotate setting not locking the screen orientation. -- cgit v1.2.3 From 784ca2d73117c044cfb20ce75e51c4269c9cbfa8 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 4 Jan 2022 08:23:47 +0200 Subject: Text: Light and regular font weight via ANSI escapes SGR codes 2 and 10. --- src/gmdocument.c | 18 ++++++++++++------ src/ui/text.c | 25 ++++++++++++++++++++++++- src/ui/text.h | 3 +++ 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/gmdocument.c b/src/gmdocument.c index fd13bc82..60773e0b 100644 --- a/src/gmdocument.c +++ b/src/gmdocument.c @@ -556,16 +556,22 @@ static const int maxLedeLines_ = 10; static void applyAttributes_RunTypesetter_(iRunTypesetter *d, iTextAttrib attrib) { /* WARNING: This is duplicated in run_Font_(). Make sure they behave identically. */ - if (attrib.bold) { - d->run.font = fontWithStyle_Text(d->baseFont, bold_FontStyle); - d->run.color = tmFirstParagraph_ColorId; + if (attrib.monospace) { + d->run.font = fontWithFamily_Text(d->baseFont, monospace_FontId); + d->run.color = tmPreformatted_ColorId; } else if (attrib.italic) { d->run.font = fontWithStyle_Text(d->baseFont, italic_FontStyle); } - else if (attrib.monospace) { - d->run.font = fontWithFamily_Text(d->baseFont, monospace_FontId); - d->run.color = tmPreformatted_ColorId; + else if (attrib.regular) { + d->run.font = fontWithStyle_Text(d->baseFont, regular_FontStyle); + } + else if (attrib.bold) { + d->run.font = fontWithStyle_Text(d->baseFont, bold_FontStyle); + d->run.color = tmFirstParagraph_ColorId; + } + else if (attrib.light) { + d->run.font = fontWithStyle_Text(d->baseFont, light_FontStyle); } else { d->run.font = d->baseFont; diff --git a/src/ui/text.c b/src/ui/text.c index 116c5eba..4b43f872 100644 --- a/src/ui/text.c +++ b/src/ui/text.c @@ -453,6 +453,10 @@ void setAnsiFlags_Text(int ansiFlags) { activeText_->ansiFlags = ansiFlags; } +int ansiFlags_Text(void) { + return activeText_->ansiFlags; +} + void setDocumentFontSize_Text(iText *d, float fontSizeFactor) { fontSizeFactor *= contentScale_Text_; iAssert(fontSizeFactor > 0); @@ -834,7 +838,6 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh .font = d->font, }; const int *logToSource = constData_Array(&d->logicalToSourceOffset); - const int * logToVis = constData_Array(&d->logicalToVisual); const iChar * logicalText = constData_Array(&d->logical); iBool isRTL = d->isBaseRTL; int numNonSpace = 0; @@ -874,17 +877,34 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh /* Note: This styling is hardcoded to match `typesetOneLine_RunTypesetter_()`. */ if (ansi & allowFontStyle_AnsiFlag && equal_Rangecc(sequence, "1")) { run.attrib.bold = iTrue; + run.attrib.regular = iFalse; + run.attrib.light = iFalse; if (d->baseFgColorId == tmParagraph_ColorId) { setFgColor_AttributedRun_(&run, tmFirstParagraph_ColorId); } attribFont = font_Text_(fontWithStyle_Text(fontId_Text_(d->baseFont), bold_FontStyle)); } + else if (ansi & allowFontStyle_AnsiFlag && equal_Rangecc(sequence, "2")) { + run.attrib.light = iTrue; + run.attrib.regular = iFalse; + run.attrib.bold = iFalse; + attribFont = font_Text_(fontWithStyle_Text(fontId_Text_(d->baseFont), + light_FontStyle)); + } else if (ansi & allowFontStyle_AnsiFlag && equal_Rangecc(sequence, "3")) { run.attrib.italic = iTrue; attribFont = font_Text_(fontWithStyle_Text(fontId_Text_(d->baseFont), italic_FontStyle)); } + else if (ansi & allowFontStyle_AnsiFlag && equal_Rangecc(sequence, "10")) { + run.attrib.regular = iTrue; + run.attrib.bold = iFalse; + run.attrib.light = iFalse; + run.attrib.italic = iFalse; + attribFont = font_Text_(fontWithStyle_Text(fontId_Text_(d->baseFont), + regular_FontStyle)); + } else if (ansi & allowFontStyle_AnsiFlag && equal_Rangecc(sequence, "11")) { run.attrib.monospace = iTrue; setFgColor_AttributedRun_(&run, tmPreformatted_ColorId); @@ -892,7 +912,9 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh monospace_FontId)); } else if (equal_Rangecc(sequence, "0")) { + run.attrib.regular = iFalse; run.attrib.bold = iFalse; + run.attrib.light = iFalse; run.attrib.italic = iFalse; run.attrib.monospace = iFalse; attribFont = run.font = d->baseFont; @@ -973,6 +995,7 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh pushBack_Array(&d->runs, &run); } #if 0 + const int *logToVis = constData_Array(&d->logicalToVisual); printf("[AttributedText] %zu runs:\n", size_Array(&d->runs)); iConstForEach(Array, i, &d->runs) { const iAttributedRun *run = i.value; diff --git a/src/ui/text.h b/src/ui/text.h index cb29adad..c8bb6f85 100644 --- a/src/ui/text.h +++ b/src/ui/text.h @@ -161,6 +161,7 @@ enum iAnsiFlag { void setOpacity_Text (float opacity); void setBaseAttributes_Text (int fontId, int fgColorId); /* current "normal" text attributes */ void setAnsiFlags_Text (int ansiFlags); +int ansiFlags_Text (void); void cache_Text (int fontId, iRangecc text); /* pre-render glyphs */ @@ -192,7 +193,9 @@ struct Impl_TextAttrib { int16_t fgColorId; int16_t bgColorId; struct { + uint16_t regular : 1; uint16_t bold : 1; + uint16_t light : 1; uint16_t italic : 1; uint16_t monospace : 1; uint16_t isBaseRTL : 1; -- cgit v1.2.3 From 2bd476483a37662797b70d6577ddb0da64cdbaa6 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 4 Jan 2022 08:26:25 +0200 Subject: Date in Gemini feed links is de-emphasized Use a regular font weight and dimmer color for the entry date. --- res/about/version.gmi | 1 + src/gmdocument.c | 5 +++-- src/ui/color.h | 2 +- src/ui/documentwidget.c | 39 ++++++++++++++++++++++++++++++++++++--- 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/res/about/version.gmi b/res/about/version.gmi index 5a7b3c99..d363d195 100644 --- a/res/about/version.gmi +++ b/res/about/version.gmi @@ -16,6 +16,7 @@ New features: Changes and enhancements: * Inline image metadata goes under the image instead of possibly overlapping the image caption text. * Improved apperance of opened links highlighting. It no longer goes under the side elements on the page. +* Entry dates in feed links are de-emphasized for improved readability. * Revised layout of the Upload dialog. There is a new edit field that lets you modify the file path of the URL. Identity names are in bold. * Optimized UI layout procedure and memory use during UI event processing. * Audio subsystem is only initialized when actually needed. diff --git a/src/gmdocument.c b/src/gmdocument.c index 60773e0b..19230392 100644 --- a/src/gmdocument.c +++ b/src/gmdocument.c @@ -1274,8 +1274,8 @@ static void setDerivedThemeColors_(enum iGmDocumentTheme theme) { mix_Color(get_Color(tmQuoteIcon_ColorId), get_Color(tmBackground_ColorId), 0.4f)); set_Color(tmBackgroundOpenLink_ColorId, mix_Color(get_Color(tmLinkText_ColorId), get_Color(tmBackground_ColorId), 0.90f)); - set_Color(tmFrameOpenLink_ColorId, - mix_Color(get_Color(tmLinkText_ColorId), get_Color(tmBackground_ColorId), 0.75f)); + set_Color(tmLinkFeedEntryDate_ColorId, + mix_Color(get_Color(tmLinkText_ColorId), get_Color(tmBackground_ColorId), 0.25f)); if (theme == colorfulDark_GmDocumentTheme) { /* Ensure paragraph text and link text aren't too similarly colored. */ if (delta_Color(get_Color(tmLinkText_ColorId), get_Color(tmParagraph_ColorId)) < 100) { @@ -1803,6 +1803,7 @@ static void markLinkRunsVisited_GmDocument_(iGmDocument *d, const iIntSet *linkI iForEach(Array, r, &d->layout) { iGmRun *run = r.value; if (run->linkId && !run->mediaId && contains_IntSet(linkIds, run->linkId)) { + /* TODO: Does this even work? The font IDs may be different. */ if (run->font == bold_FontId) { run->font = paragraph_FontId; } diff --git a/src/ui/color.h b/src/ui/color.h index 0b3f8bed..24f9e713 100644 --- a/src/ui/color.h +++ b/src/ui/color.h @@ -136,7 +136,7 @@ enum iColorId { tmBackgroundAltText_ColorId, /* derived from other theme colors */ tmFrameAltText_ColorId, /* derived from other theme colors */ tmBackgroundOpenLink_ColorId, /* derived from other theme colors */ - tmFrameOpenLink_ColorId, /* derived from other theme colors */ + tmLinkFeedEntryDate_ColorId, /* derived from other theme colors */ tmLinkCustomIconVisited_ColorId, /* derived from other theme colors */ tmBadLink_ColorId, diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 2fd5f148..88e016f3 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -1274,7 +1274,7 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { if (isMobileHover && !isPartOfHover) { bg = tmBackground_ColorId; /* hover ended and was invalidated */ } - const int frame = tmFrameOpenLink_ColorId; +// const int frame = tmFrameOpenLink_ColorId; const int pad = gap_Text; iRect wideRect = { init_I2(origin.x - pad, visPos.y), init_I2(d->docBounds.size.x + 2 * pad, @@ -1364,13 +1364,46 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { runBaseAttributes_GmDocument(doc, run, &f, &c); setBaseAttributes_Text(f, c); } + /* Fancy date in Gemini feed links. */ { + if (run->linkId && run->flags & startOfLine_GmRunFlag && ~run->flags & decoration_GmRunFlag) { + static iRegExp *datePattern_; + if (!datePattern_) { + datePattern_ = new_RegExp("^[12][0-9][0-9][0-9]-[01][0-9]-[0-3][0-9]\\s", 0); + } + iRegExpMatch m; + init_RegExpMatch(&m); + if (matchRange_RegExp(datePattern_, run->text, &m)) { + /* The date uses regular weight and a dimmed color. */ + iString styled; + initRange_String(&styled, run->text); + insertData_Block(&styled.chars, 10, "\x1b[0m", 4); /* restore */ + iBlock buf; + init_Block(&buf, 0); + appendCStr_Block(&buf, "\x1b[10m"); /* regular font weight */ + appendCStr_Block(&buf, escape_Color(isHover ? fg : tmLinkFeedEntryDate_ColorId)); + insertData_Block(&styled.chars, 0, constData_Block(&buf), size_Block(&buf)); + deinit_Block(&buf); + const int oldAnsi = ansiFlags_Text(); + setAnsiFlags_Text(oldAnsi | allowFontStyle_AnsiFlag); + setBaseAttributes_Text(run->font, fg); + drawBoundRange_Text(run->font, + visPos, + (run->isRTL ? -1 : 1) * width_Rect(run->visBounds), + fg, + range_String(&styled)); + setAnsiFlags_Text(oldAnsi); + deinit_String(&styled); + goto runDrawn; + } + } + } drawBoundRange_Text(run->font, visPos, (run->isRTL ? -1 : 1) * width_Rect(run->visBounds), fg, run->text); - setBaseAttributes_Text(-1, -1); runDrawn:; + setBaseAttributes_Text(-1, -1); } /* Presentation of links. */ if (run->linkId && ~run->flags & decoration_GmRunFlag) { @@ -5472,7 +5505,7 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) { drawChildren_Widget(w); /* Information about the hovered link. */ if (deviceType_App() == desktop_AppDeviceType && prefs_App()->hoverLink && d->linkInfo) { - const int pad = gap_UI; + const int pad = 0; /*gap_UI;*/ update_LinkInfo(d->linkInfo, d->view.doc, d->view.hoverLink ? d->view.hoverLink->linkId : 0, -- cgit v1.2.3 From a3e6469fba3585ee46cc43f200f237afb1266711 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 4 Jan 2022 08:41:23 +0200 Subject: Updated release notes, language strings --- res/about/version.gmi | 1 + res/lang/ia.bin | Bin 28062 -> 28765 bytes res/lang/tr.bin | Bin 28928 -> 28911 bytes 3 files changed, 1 insertion(+) diff --git a/res/about/version.gmi b/res/about/version.gmi index b9ea37db..7344cd26 100644 --- a/res/about/version.gmi +++ b/res/about/version.gmi @@ -8,6 +8,7 @@ ## 1.9.4 * Fixed crash when a link is missing both URL and label (just a `=>`). +* Updated UI translations. ## 1.9.3 * Added UI language for Dutch. diff --git a/res/lang/ia.bin b/res/lang/ia.bin index 4750b545..c8e00d5e 100644 Binary files a/res/lang/ia.bin and b/res/lang/ia.bin differ diff --git a/res/lang/tr.bin b/res/lang/tr.bin index 1092938f..1dd9733b 100644 Binary files a/res/lang/tr.bin and b/res/lang/tr.bin differ -- cgit v1.2.3 From 4866ecbca5b71f52852cea6a63d119fe731c9b9a Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Mon, 27 Dec 2021 13:45:17 +0200 Subject: Text: Fixed color escapes in the simple text renderer --- src/ui/text_simple.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ui/text_simple.c b/src/ui/text_simple.c index 8560c138..71942cf1 100644 --- a/src/ui/text_simple.c +++ b/src/ui/text_simple.c @@ -118,8 +118,9 @@ static iRect runSimple_Font_(iFont *d, const iRunArgs *args) { if (match_RegExp(activeText_->ansiEscape, chPos, args->text.end - chPos, &m)) { if (mode & draw_RunMode && ~mode & permanentColorFlag_RunMode) { /* Change the color. */ - iColor clr; - ansiColors_Color(capturedRange_RegExpMatch(&m, 1), tmParagraph_ColorId, + iColor clr = get_Color(args->color); + ansiColors_Color(capturedRange_RegExpMatch(&m, 1), + activeText_->baseFgColorId, none_ColorId, &clr, NULL); SDL_SetTextureColorMod(activeText_->cache, clr.r, clr.g, clr.b); if (args->mode & fillBackground_RunMode) { -- cgit v1.2.3 From ec575faf05060617b7ade4d3787b6f0e88a116bf Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 4 Jan 2022 08:51:49 +0200 Subject: Updated release notes --- res/about/version.gmi | 2 ++ 1 file changed, 2 insertions(+) diff --git a/res/about/version.gmi b/res/about/version.gmi index 7344cd26..c333d15b 100644 --- a/res/about/version.gmi +++ b/res/about/version.gmi @@ -8,7 +8,9 @@ ## 1.9.4 * Fixed crash when a link is missing both URL and label (just a `=>`). +* Fixed handling of foreground color escapes in the simple text renderer. * Updated UI translations. +* Upgraded SDL to 2.0.18. ## 1.9.3 * Added UI language for Dutch. -- cgit v1.2.3 From 81610dc712d004e81f9bbf1feaf6433a09f56e3a Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 4 Jan 2022 08:58:15 +0200 Subject: SDL patch for macOS and iOS updated for 2.0.18 SDL has added multiple input sources for mice. --- sdl2.0.18-macos-ios.diff | 129 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 sdl2.0.18-macos-ios.diff diff --git a/sdl2.0.18-macos-ios.diff b/sdl2.0.18-macos-ios.diff new file mode 100644 index 00000000..8c143226 --- /dev/null +++ b/sdl2.0.18-macos-ios.diff @@ -0,0 +1,129 @@ +diff --git a/src/events/SDL_mouse.c b/src/events/SDL_mouse.c +index 539f17138..a5a5b00fd 100644 +--- a/src/events/SDL_mouse.c ++++ b/src/events/SDL_mouse.c +@@ -709,8 +709,8 @@ SDL_SendMouseWheel(SDL_Window * window, SDL_MouseID mouseID, float x, float y, S + event.type = SDL_MOUSEWHEEL; + event.wheel.windowID = mouse->focus ? mouse->focus->id : 0; + event.wheel.which = mouseID; +- event.wheel.x = integral_x; +- event.wheel.y = integral_y; ++ event.wheel.x = x; //integral_x; ++ event.wheel.y = y; //integral_y; + event.wheel.preciseX = x; + event.wheel.preciseY = y; + event.wheel.direction = (Uint32)direction; +diff --git a/src/video/cocoa/SDL_cocoamouse.m b/src/video/cocoa/SDL_cocoamouse.m +index e9d832d64..4cfa3624b 100644 +--- a/src/video/cocoa/SDL_cocoamouse.m ++++ b/src/video/cocoa/SDL_cocoamouse.m +@@ -463,10 +463,16 @@ + (NSCursor *)invisibleCursor + } + + SDL_MouseID mouseID = mouse->mouseID; +- CGFloat x = -[event deltaX]; +- CGFloat y = [event deltaY]; ++ CGFloat x = -[event scrollingDeltaX]; ++ CGFloat y = [event scrollingDeltaY]; + SDL_MouseWheelDirection direction = SDL_MOUSEWHEEL_NORMAL; + ++ /* HACK: Make a distinction between precise and imprecise scrolling. ++ Trackpad seems to be mouseID 0. */ ++ if (![event hasPreciseScrollingDeltas]) { ++ mouseID = 1; ++ } ++ + if ([event respondsToSelector:@selector(isDirectionInvertedFromDevice)]) { + if ([event isDirectionInvertedFromDevice] == YES) { + direction = SDL_MOUSEWHEEL_FLIPPED; +diff --git a/src/video/cocoa/SDL_cocoawindow.h b/src/video/cocoa/SDL_cocoawindow.h +index 489db169f..79a41dd50 100644 +--- a/src/video/cocoa/SDL_cocoawindow.h ++++ b/src/video/cocoa/SDL_cocoawindow.h +@@ -110,6 +110,8 @@ typedef enum + /* Touch event handling */ + -(void) handleTouches:(NSTouchPhase) phase withEvent:(NSEvent*) theEvent; + ++-(void) syncMouseButtonAndKeyboardModifierState; ++ + @end + /* *INDENT-ON* */ + +diff --git a/src/video/cocoa/SDL_cocoawindow.m b/src/video/cocoa/SDL_cocoawindow.m +index f09088c45..7379c2269 100644 +--- a/src/video/cocoa/SDL_cocoawindow.m ++++ b/src/video/cocoa/SDL_cocoawindow.m +@@ -1236,6 +1236,25 @@ - (void)otherMouseDown:(NSEvent *)theEvent + [self mouseDown:theEvent]; + } + ++- (void)syncMouseButtonAndKeyboardModifierState { ++ SDL_Mouse *mouse = SDL_GetMouse(); ++ if (mouse) { ++ for (int i = 0; i < mouse->num_sources; i++) { ++ if (mouse->sources[i].mouseID == mouse->mouseID) { ++ mouse->sources[i].buttonstate = 0; ++ } ++ } ++ } ++ SDL_SendKeyboardKey(SDL_RELEASED, SDL_SCANCODE_LGUI); ++ SDL_SendKeyboardKey(SDL_RELEASED, SDL_SCANCODE_RGUI); ++ SDL_SendKeyboardKey(SDL_RELEASED, SDL_SCANCODE_LSHIFT); ++ SDL_SendKeyboardKey(SDL_RELEASED, SDL_SCANCODE_RSHIFT); ++ SDL_SendKeyboardKey(SDL_RELEASED, SDL_SCANCODE_LCTRL); ++ SDL_SendKeyboardKey(SDL_RELEASED, SDL_SCANCODE_RCTRL); ++ SDL_SendKeyboardKey(SDL_RELEASED, SDL_SCANCODE_LALT); ++ SDL_SendKeyboardKey(SDL_RELEASED, SDL_SCANCODE_RALT); ++} ++ + - (void)mouseUp:(NSEvent *)theEvent + { + SDL_Mouse *mouse = SDL_GetMouse(); +diff --git a/src/video/uikit/SDL_uikitviewcontroller.h b/src/video/uikit/SDL_uikitviewcontroller.h +index f7f4c9de6..50c72aad0 100644 +--- a/src/video/uikit/SDL_uikitviewcontroller.h ++++ b/src/video/uikit/SDL_uikitviewcontroller.h +@@ -58,10 +58,13 @@ + #if !TARGET_OS_TV + - (NSUInteger)supportedInterfaceOrientations; + - (BOOL)prefersStatusBarHidden; ++- (void)setStatusStyle:(UIStatusBarStyle)style; ++- (UIStatusBarStyle)preferredStatusBarStyle; + - (BOOL)prefersHomeIndicatorAutoHidden; + - (UIRectEdge)preferredScreenEdgesDeferringSystemGestures; + + @property (nonatomic, assign) int homeIndicatorHidden; ++@property (nonatomic, assign) UIStatusBarStyle statusBarStyle; + #endif + + #if SDL_IPHONE_KEYBOARD +diff --git a/src/video/uikit/SDL_uikitviewcontroller.m b/src/video/uikit/SDL_uikitviewcontroller.m +index b2db4037d..d39f8085c 100644 +--- a/src/video/uikit/SDL_uikitviewcontroller.m ++++ b/src/video/uikit/SDL_uikitviewcontroller.m +@@ -105,6 +105,7 @@ - (instancetype)initWithSDLWindow:(SDL_Window *)_window + #endif + + #if !TARGET_OS_TV ++ self.statusBarStyle = UIStatusBarStyleDefault; + SDL_AddHintCallback(SDL_HINT_IOS_HIDE_HOME_INDICATOR, + SDL_HideHomeIndicatorHintChanged, + (__bridge void *) self); +@@ -230,6 +231,17 @@ - (BOOL)prefersHomeIndicatorAutoHidden + return hidden; + } + ++- (void)setStatusStyle:(UIStatusBarStyle)style ++{ ++ self.statusBarStyle = style; ++ [self setNeedsStatusBarAppearanceUpdate]; ++} ++ ++- (UIStatusBarStyle)preferredStatusBarStyle ++{ ++ return self.statusBarStyle; ++} ++ + - (UIRectEdge)preferredScreenEdgesDeferringSystemGestures + { + if (self.homeIndicatorHidden >= 0) { -- cgit v1.2.3 From 5e29b4d947494ac6617057b9186277a24945a225 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sat, 25 Dec 2021 06:42:56 +0200 Subject: SDL line drawing regression was fixed in 2.0.18 --- src/ui/labelwidget.c | 2 +- src/ui/paint.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui/labelwidget.c b/src/ui/labelwidget.c index 4dd66a28..1ff6d3b7 100644 --- a/src/ui/labelwidget.c +++ b/src/ui/labelwidget.c @@ -347,7 +347,7 @@ static void draw_LabelWidget_(const iLabelWidget *d) { bottomRight_Rect(frameRect), bottomLeft_Rect(frameRect) }; -#if SDL_VERSION_ATLEAST(2, 0, 16) +#if SDL_COMPILEDVERSION == SDL_VERSIONNUM(2, 0, 16) if (isOpenGLRenderer_Window()) { /* A very curious regression in SDL 2.0.16. */ points[3].x--; diff --git a/src/ui/paint.c b/src/ui/paint.c index b92be27e..a91c6f12 100644 --- a/src/ui/paint.c +++ b/src/ui/paint.c @@ -108,7 +108,7 @@ void drawRect_Paint(const iPaint *d, iRect rect, int color) { { left_Rect(rect), br.y }, { left_Rect(rect), top_Rect(rect) } }; -#if SDL_VERSION_ATLEAST(2, 0, 16) +#if SDL_COMPILEDVERSION == SDL_VERSIONNUM(2, 0, 16) if (isOpenGLRenderer_Window()) { /* A very curious regression in SDL 2.0.16. */ edges[3].y--; -- cgit v1.2.3 From 31312b7a2e51a87a41bf777b6af135329927460d Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Tue, 4 Jan 2022 11:03:46 +0200 Subject: Bumped version number; updated release notes --- CMakeLists.txt | 2 +- res/about/version.gmi | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b47f8537..2b6c0ca6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,7 +18,7 @@ cmake_minimum_required (VERSION 3.9) project (Lagrange - VERSION 1.9.4 + VERSION 1.9.5 DESCRIPTION "A Beautiful Gemini Client" LANGUAGES C ) diff --git a/res/about/version.gmi b/res/about/version.gmi index c333d15b..b45a8923 100644 --- a/res/about/version.gmi +++ b/res/about/version.gmi @@ -6,6 +6,9 @@ ``` # Release notes +## 1.9.5 +* Fixed misshapen button borders due to an SDL line drawing glitch. + ## 1.9.4 * Fixed crash when a link is missing both URL and label (just a `=>`). * Fixed handling of foreground color escapes in the simple text renderer. -- cgit v1.2.3 From 32cbf877ac137b28eb41a9084717707907c02d94 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Wed, 5 Jan 2022 08:40:53 +0200 Subject: Handling keys while navigating via home row When navigating via home row, ensure that other widgets will not trigger when keys are pressed. The document is given keyboard focus even though it is not flagged as focusable, to make sure it gets to handle all the keys first. On macOS, disable all native menu items that have a home row keyboard shortcut. Also on macOS, disable all native menus when changing bindings. IssueID #419 --- src/macos.h | 2 ++ src/macos.m | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ src/ui/bindingswidget.c | 6 +++++- src/ui/documentwidget.c | 20 +++++++++++++++----- src/ui/widget.c | 10 +++++++++- src/ui/widget.h | 3 ++- 6 files changed, 82 insertions(+), 8 deletions(-) diff --git a/src/macos.h b/src/macos.h index 22a6dfff..10cbba81 100644 --- a/src/macos.h +++ b/src/macos.h @@ -39,8 +39,10 @@ void hideTitleBar_MacOS (iWindow *window); void insertMenuItems_MacOS (const char *menuLabel, int atIndex, const iMenuItem *items, size_t count); void removeMenu_MacOS (int atIndex); void enableMenu_MacOS (const char *menuLabel, iBool enable); +void enableMenuIndex_MacOS (int index, iBool enable); void enableMenuItem_MacOS (const char *menuItemCommand, iBool enable); void enableMenuItemsByKey_MacOS (int key, int kmods, iBool enable); +void enableMenuItemsOnHomeRow_MacOS(iBool enable); void handleCommand_MacOS (const char *cmd); void showPopupMenu_MacOS (iWidget *source, iInt2 windowCoord, const iMenuItem *items, size_t n); diff --git a/src/macos.m b/src/macos.m index cfbca488..28e349ea 100644 --- a/src/macos.m +++ b/src/macos.m @@ -31,6 +31,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include #include +#include #import @@ -439,6 +440,13 @@ void enableMenu_MacOS(const char *menuLabel, iBool enable) { [menuItem setEnabled:enable]; } +void enableMenuIndex_MacOS(int index, iBool enable) { + NSApplication *app = [NSApplication sharedApplication]; + NSMenu *appMenu = [app mainMenu]; + NSMenuItem *menuItem = [appMenu itemAtIndex:index]; + [menuItem setEnabled:enable]; +} + void enableMenuItem_MacOS(const char *menuItemCommand, iBool enable) { NSApplication *app = [NSApplication sharedApplication]; NSMenu *appMenu = [app mainMenu]; @@ -513,6 +521,47 @@ void enableMenuItemsByKey_MacOS(int key, int kmods, iBool enable) { delete_String(keyEquiv); } +void enableMenuItemsOnHomeRow_MacOS(iBool enable) { + iStringSet *homeRowKeys = new_StringSet(); + const char *keys[] = { /* Note: another array in documentwidget.c */ + "f", "d", "s", "a", + "j", "k", "l", + "r", "e", "w", "q", + "u", "i", "o", "p", + "v", "c", "x", "z", + "m", "n", + "g", "h", + "b", + "t", "y" + }; + iForIndices(i, keys) { + iString str; + initCStr_String(&str, keys[i]); + insert_StringSet(homeRowKeys, &str); + deinit_String(&str); + } + NSApplication *app = [NSApplication sharedApplication]; + NSMenu *appMenu = [app mainMenu]; + for (NSMenuItem *mainMenuItem in appMenu.itemArray) { + NSMenu *menu = mainMenuItem.submenu; + if (menu) { + for (NSMenuItem *menuItem in menu.itemArray) { + if (menuItem.keyEquivalentModifierMask == 0) { + iString equiv; + initCStr_String(&equiv, [menuItem.keyEquivalent + cStringUsingEncoding:NSUTF8StringEncoding]); + if (contains_StringSet(homeRowKeys, &equiv)) { + [menuItem setEnabled:enable]; + [menu setAutoenablesItems:NO]; + } + deinit_String(&equiv); + } + } + } + } + iRelease(homeRowKeys); +} + static void setShortcut_NSMenuItem_(NSMenuItem *item, int key, int kmods) { NSEventModifierFlags modMask; iString *str = composeKeyEquivalent_(key, kmods, &modMask); diff --git a/src/ui/bindingswidget.c b/src/ui/bindingswidget.c index 4cf8df8e..13f9434e 100644 --- a/src/ui/bindingswidget.c +++ b/src/ui/bindingswidget.c @@ -143,12 +143,16 @@ static void setActiveItem_BindingsWidget_(iBindingsWidget *d, size_t pos) { item->isWaitingForEvent = iTrue; invalidateItem_ListWidget(d->list, d->activePos); } -#if defined (iPlatformAppleDesktop) +#if defined (iPlatformAppleDesktop) && defined (iHaveNativeContextMenus) /* Native menus must be disabled while grabbing keys so the shortcuts don't trigger. */ const iBool enableNativeMenus = (d->activePos == iInvalidPos); + enableMenu_MacOS("${menu.title.file}", enableNativeMenus); enableMenu_MacOS("${menu.title.edit}", enableNativeMenus); enableMenu_MacOS("${menu.title.view}", enableNativeMenus); + enableMenu_MacOS("${menu.title.bookmarks}", enableNativeMenus); enableMenu_MacOS("${menu.title.identity}", enableNativeMenus); + enableMenuIndex_MacOS(6, enableNativeMenus); + enableMenuIndex_MacOS(7, enableNativeMenus); #endif } diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 46af5fcd..f83539f7 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -55,6 +55,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "visbuf.h" #include "visited.h" +#if defined (iPlatformAppleDesktop) +# include "macos.h" +#endif #if defined (iPlatformAppleMobile) # include "ios.h" #endif @@ -469,11 +472,18 @@ static void enableActions_DocumentWidget_(iDocumentWidget *d, iBool enable) { } static void setLinkNumberMode_DocumentWidget_(iDocumentWidget *d, iBool set) { - iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, set); - /* Children have priority when handling events. */ - enableActions_DocumentWidget_(d, !set); - if (d->menu) { - setFlags_Widget(d->menu, disabled_WidgetFlag, set); + if (((d->flags & showLinkNumbers_DocumentWidgetFlag) != 0) != set) { + iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, set); + /* Children have priority when handling events. */ + enableActions_DocumentWidget_(d, !set); +#if defined (iPlatformAppleDesktop) + enableMenuItemsOnHomeRow_MacOS(!set); +#endif + /* Ensure all keyboard events come here first. */ + setKeyboardGrab_Widget(set ? as_Widget(d) : NULL); + if (d->menu) { + setFlags_Widget(d->menu, disabled_WidgetFlag, set); + } } } diff --git a/src/ui/widget.c b/src/ui/widget.c index cedda461..28c34ccf 100644 --- a/src/ui/widget.c +++ b/src/ui/widget.c @@ -1961,7 +1961,8 @@ iBool isAffectedByVisualOffset_Widget(const iWidget *d) { } void setFocus_Widget(iWidget *d) { - iWindow *win = get_Window(); + iWindow *win = d ? window_Widget(d) : get_Window(); + iAssert(win); if (win->focus != d) { if (win->focus) { iAssert(!contains_PtrSet(win->focus->root->pendingDestruction, win->focus)); @@ -1976,6 +1977,13 @@ void setFocus_Widget(iWidget *d) { } } +void setKeyboardGrab_Widget(iWidget *d) { + iWindow *win = d ? window_Widget(d) : get_Window(); + iAssert(win); + win->focus = d; + /* no notifications sent */ +} + iWidget *focus_Widget(void) { return get_Window()->focus; } diff --git a/src/ui/widget.h b/src/ui/widget.h index 4025f5c5..57088c07 100644 --- a/src/ui/widget.h +++ b/src/ui/widget.h @@ -302,7 +302,8 @@ void scrollInfo_Widget (const iWidget *, iWidgetScrollInfo *inf int backgroundFadeColor_Widget (void); -void setFocus_Widget (iWidget *); +void setFocus_Widget (iWidget *); /* widget must be flagged `focusable` */ +void setKeyboardGrab_Widget (iWidget *); /* sets focus on any widget */ iWidget * focus_Widget (void); void setHover_Widget (iWidget *); iWidget * hover_Widget (void); -- cgit v1.2.3 From d4cbe3e6d25f2c5275e58a991d7af0c5ec07f00e Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Wed, 5 Jan 2022 08:44:11 +0200 Subject: Updated release notes --- res/about/version.gmi | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/res/about/version.gmi b/res/about/version.gmi index b45a8923..575dbf70 100644 --- a/res/about/version.gmi +++ b/res/about/version.gmi @@ -7,7 +7,10 @@ # Release notes ## 1.9.5 -* Fixed misshapen button borders due to an SDL line drawing glitch. +* Fixed misshapen button borders (SDL 2.0.16 line drawing workaround). +* Fixed actions being triggered when navigating via home row keys. +* macOS: Fixed native menu items being triggered when navigating via home row keys. +* macOS: Fixed native menu items triggering when changing key bindings. ## 1.9.4 * Fixed crash when a link is missing both URL and label (just a `=>`). -- cgit v1.2.3 From 680db07eabd116cfde9d2894c69ff14e6d5e01ad Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Wed, 5 Jan 2022 08:51:21 +0200 Subject: Updated copyright year --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2b6c0ca6..9d421fe2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,7 +22,7 @@ project (Lagrange DESCRIPTION "A Beautiful Gemini Client" LANGUAGES C ) -set (COPYRIGHT_YEAR 2021) +set (COPYRIGHT_YEAR 2022) if (IOS) set (PROJECT_VERSION 1.7) set (IOS_BUNDLE_VERSION 23) -- cgit v1.2.3 From 5f7785b30f48a4d9c9784d332ba020d182b18f2c Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Wed, 5 Jan 2022 13:53:13 +0200 Subject: Updated language strings --- res/lang/cs.bin | Bin 30937 -> 31844 bytes res/lang/de.bin | Bin 29906 -> 30813 bytes res/lang/en.bin | Bin 26003 -> 26889 bytes res/lang/eo.bin | Bin 24963 -> 25859 bytes res/lang/es.bin | Bin 29735 -> 30642 bytes res/lang/es_MX.bin | Bin 27062 -> 27959 bytes res/lang/fi.bin | Bin 29573 -> 30480 bytes res/lang/fr.bin | Bin 30710 -> 31617 bytes res/lang/gl.bin | Bin 28913 -> 29820 bytes res/lang/hu.bin | Bin 30735 -> 31642 bytes res/lang/ia.bin | Bin 28765 -> 29672 bytes res/lang/ie.bin | Bin 28672 -> 29579 bytes res/lang/isv.bin | Bin 24723 -> 25610 bytes res/lang/nl.bin | Bin 28092 -> 28999 bytes res/lang/pl.bin | Bin 29338 -> 30245 bytes res/lang/ru.bin | Bin 44148 -> 45055 bytes res/lang/sk.bin | Bin 25059 -> 25946 bytes res/lang/sr.bin | Bin 43555 -> 44462 bytes res/lang/tok.bin | Bin 26777 -> 27684 bytes res/lang/tr.bin | Bin 28911 -> 29818 bytes res/lang/uk.bin | Bin 43480 -> 44387 bytes res/lang/zh_Hans.bin | Bin 24957 -> 25854 bytes res/lang/zh_Hant.bin | Bin 25345 -> 26252 bytes 23 files changed, 0 insertions(+), 0 deletions(-) diff --git a/res/lang/cs.bin b/res/lang/cs.bin index 22043934..abd4a735 100644 Binary files a/res/lang/cs.bin and b/res/lang/cs.bin differ diff --git a/res/lang/de.bin b/res/lang/de.bin index b2bb35a0..62893c64 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 fbf4c73c..1b26e61e 100644 Binary files a/res/lang/en.bin and b/res/lang/en.bin differ diff --git a/res/lang/eo.bin b/res/lang/eo.bin index d7f9cebf..318153d3 100644 Binary files a/res/lang/eo.bin and b/res/lang/eo.bin differ diff --git a/res/lang/es.bin b/res/lang/es.bin index 749645bf..71fa7161 100644 Binary files a/res/lang/es.bin and b/res/lang/es.bin differ diff --git a/res/lang/es_MX.bin b/res/lang/es_MX.bin index 078de89d..3de1f106 100644 Binary files a/res/lang/es_MX.bin and b/res/lang/es_MX.bin differ diff --git a/res/lang/fi.bin b/res/lang/fi.bin index dee3561f..ad009896 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 96c1148e..e6977fd9 100644 Binary files a/res/lang/fr.bin and b/res/lang/fr.bin differ diff --git a/res/lang/gl.bin b/res/lang/gl.bin index 02482f3a..2dfb1059 100644 Binary files a/res/lang/gl.bin and b/res/lang/gl.bin differ diff --git a/res/lang/hu.bin b/res/lang/hu.bin index 7b7edb50..3ca7ee30 100644 Binary files a/res/lang/hu.bin and b/res/lang/hu.bin differ diff --git a/res/lang/ia.bin b/res/lang/ia.bin index c8e00d5e..b2d71185 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 14f16e5d..a6d526de 100644 Binary files a/res/lang/ie.bin and b/res/lang/ie.bin differ diff --git a/res/lang/isv.bin b/res/lang/isv.bin index 80754fc5..adeeb8cd 100644 Binary files a/res/lang/isv.bin and b/res/lang/isv.bin differ diff --git a/res/lang/nl.bin b/res/lang/nl.bin index 6ae01202..6ed111cd 100644 Binary files a/res/lang/nl.bin and b/res/lang/nl.bin differ diff --git a/res/lang/pl.bin b/res/lang/pl.bin index c0affedf..fab560a5 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 f0762eea..ed7838bf 100644 Binary files a/res/lang/ru.bin and b/res/lang/ru.bin differ diff --git a/res/lang/sk.bin b/res/lang/sk.bin index deda3b69..ec171de2 100644 Binary files a/res/lang/sk.bin and b/res/lang/sk.bin differ diff --git a/res/lang/sr.bin b/res/lang/sr.bin index dc13d2e1..c710e929 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 6a93803c..196cc5be 100644 Binary files a/res/lang/tok.bin and b/res/lang/tok.bin differ diff --git a/res/lang/tr.bin b/res/lang/tr.bin index 1dd9733b..cf62799a 100644 Binary files a/res/lang/tr.bin and b/res/lang/tr.bin differ diff --git a/res/lang/uk.bin b/res/lang/uk.bin index 4154c354..bbb5c3cf 100644 Binary files a/res/lang/uk.bin and b/res/lang/uk.bin differ diff --git a/res/lang/zh_Hans.bin b/res/lang/zh_Hans.bin index 3a83dd40..0a12f0dd 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 ffad7072..2e7c8108 100644 Binary files a/res/lang/zh_Hant.bin and b/res/lang/zh_Hant.bin differ -- cgit v1.2.3 From 06245e4ec4058a4f968bbd27c5e3361425a5d1b4 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Wed, 5 Jan 2022 14:10:50 +0200 Subject: Cleanup Post merge. --- AUTHORS.md | 2 +- src/ui/labelwidget.c | 9 +++++++-- src/ui/sidebarwidget.c | 2 +- src/ui/util.c | 5 ++--- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index 293cf7e5..99e13875 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -5,7 +5,7 @@ Lagrange was created by Jaakko Keränen () in July 2020. Legend: `C` code, `T` translation ``` -C 2757 Jaakko Keränen +C 2975 Jaakko Keränen CT 43 Nikolay Korotkiy T 41 Olga Smirnova T 39 Alyssa Liddell diff --git a/src/ui/labelwidget.c b/src/ui/labelwidget.c index 89e95c85..3454014a 100644 --- a/src/ui/labelwidget.c +++ b/src/ui/labelwidget.c @@ -226,8 +226,13 @@ static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int *meta = uiTextDisabled_ColorId; } if (isSel) { - if (!d->flags.checkMark) { - *bg = uiBackgroundSelected_ColorId; + if (!d->flags.checkMark) { + if (isMenuItem) { + *bg = uiBackgroundUnfocusedSelection_ColorId; + } + else { + *bg = uiBackgroundSelected_ColorId; + } if (!isKeyRoot) { *bg = isDark_ColorTheme(colorTheme_App()) ? uiBackgroundUnfocusedSelection_ColorId : uiMarked_ColorId; diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index 409d749c..30e35b7c 100644 --- a/src/ui/sidebarwidget.c +++ b/src/ui/sidebarwidget.c @@ -1521,8 +1521,8 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) iBookmark *bm = get_Bookmarks(bookmarks_App(), item->id); if (flag == subscribed_BookmarkFlag && (bm->flags & flag)) { removeEntries_Feeds(item->id); /* get rid of unsubscribed entries */ + } bm->flags ^= flag; - } postCommand_App("bookmarks.changed"); } return iTrue; diff --git a/src/ui/util.c b/src/ui/util.c index 13bfd92f..e4e4f50b 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -2547,17 +2547,16 @@ iWidget *makePreferences_Widget(void) { { "buttons id:prefs.mono", 0, 0, (const void *) monoFontItems }, { "padding" }, { "dropdown id:prefs.font.monodoc", 0, 0, (const void *) constData_Array(makeFontItems_("monodoc")) }, - { "toggle id:prefs.font.warnmissing" }, { "padding" }, + { "toggle id:prefs.font.warnmissing" }, { "heading id:prefs.gemtext.ansi" }, { "toggle id:prefs.gemtext.ansi.fg" }, { "toggle id:prefs.gemtext.ansi.bg" }, { "toggle id:prefs.gemtext.ansi.fontstyle" }, -// { "toggle id:prefs.font.smooth" }, // { "padding" }, // { "dropdown id:prefs.font.ui", 0, 0, (const void *) constData_Array(makeFontItems_("ui")) }, - { "button text:" fontpack_Icon " " uiTextAction_ColorEscape "${menu.fonts}", 0, 0, "!open url:about:fonts" }, { "padding" }, + { "button text:" fontpack_Icon " " uiTextAction_ColorEscape "${menu.fonts}", 0, 0, "!open url:about:fonts" }, { NULL } }; const iMenuItem stylePanelItems[] = { -- cgit v1.2.3 From 3293d2fb2e4f92e3c0ebc3f2a6cf0acd2e9543cf Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Wed, 5 Jan 2022 15:08:47 +0200 Subject: Updated release notes --- res/about/help.gmi | 2 +- res/about/version.gmi | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/res/about/help.gmi b/res/about/help.gmi index 7949b775..93bc6a05 100644 --- a/res/about/help.gmi +++ b/res/about/help.gmi @@ -424,7 +424,7 @@ The "ANSI escapes" setting controls which ANSI escape codes are enabled: * "FG Color" enables changes to text foreground color. * "BG Color" enables changes to text background color. -* "Font Style" enables changing the font style: bold, italic, or monospace. (These correspond to codes 1, 3, and 11.) +* "Font Style" enables changing the font style: bold, light, italic, regular, or monospace. (These correspond to codes 1, 2, 3, 10, and 11.) A warning banner is displayed if any codes are detected on a page. This helps you be aware of potential visual artifacts, like text color that is being forced to black regardless of the page background color. Click on the warning to dismiss it on a per-site basis. diff --git a/res/about/version.gmi b/res/about/version.gmi index ff954fc6..351e1edc 100644 --- a/res/about/version.gmi +++ b/res/about/version.gmi @@ -8,15 +8,22 @@ ## 1.10 New features: -* Added "Paste Preceding Line" in the input prompt dialog. Potential use cases include user-editable sections on a page and suggested/example input values. +* macOS: Trackpad swipe navigation. * Identity toolbar menu can be used to switch between alternate identities. If you have used multiple identities on one site, this makes it more convenient to switch between them. +* Added "Edit Page with Titan": opens the upload dialog with current page's content prefilled. +* Added "Paste Preceding Line" in the input prompt dialog. Potential use cases include user-editable sections on a page and suggested/example input values. +* Added footer action to open file in another app when the media type is unsupported. * Added option to disable cursor blinking in input fields. -* UI language for Dutch. +* Added ANSI SGR codes for light and regular font weights (2, 10). +* macOS: Added "Show in Finder" in the Identities sidebar. Changes and enhancements: -* Inline image metadata goes under the image instead of possibly overlapping the image caption text. -* Improved apperance of opened links highlighting. It no longer goes under the side elements on the page. +* Improved image inlining: all responses with an image media type can get inlined, regardless of the file extension in the URL. +* Inline image metadata goes under the image instead of possibly overlapping the label text. +* Inline downloads have a context menu for relevant actions, and clicking on the download opens the file. +* Improved highlighting of open pages. The highlight no longer goes under the side elements on the page. * Entry dates in feed links are de-emphasized for improved readability. +* Revised link hover popup. None of the information appears on the same line any more (which was problematic if there wasn't enough space). Instead, everything is shown in a popup at the bottom/top of the view, including the identity that will be used when opening the link and the date of last visit. * Revised layout of the Upload dialog. There is a new edit field that lets you modify the file path of the URL. Identity names are in bold. * Optimized UI layout procedure and memory use during UI event processing. * Audio subsystem is only initialized when actually needed. @@ -26,9 +33,12 @@ Changes and enhancements: Fixes: * Fixed a history caching issue: if there were multiple instances of the same URL in history, only the latest one's content would be used when navigating back/forward. +* Fixed handling of reserved characters in URLs (cf. RFC 3986, section 2.2). * Fixed very narrow input fields causing the app to hang. * Fixed initial scroll position in multiline input fields. * Fixed lookup results list becoming too narrow. +* Fixed glitches when a widget has multiple simultanous animations. +* macOS: Line breaks inside menu items (e.g., info about current identity). ## 1.9.5 * Fixed misshapen button borders (SDL 2.0.16 line drawing workaround). -- cgit v1.2.3 From 6d1a21966a6819698f29b3fb0136af13ef929e6e Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Wed, 5 Jan 2022 15:09:06 +0200 Subject: Mobile: URL field padding on tablets --- src/ui/inputwidget.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index 7ceb59c1..9261da0c 100644 --- a/src/ui/inputwidget.c +++ b/src/ui/inputwidget.c @@ -266,9 +266,9 @@ struct Impl_InputWidget { iDefineObjectConstructionArgs(InputWidget, (size_t maxLen), maxLen) static int extraPaddingHeight_InputWidget_(const iInputWidget *d) { - if (isPortraitPhone_App() && !cmp_String(id_Widget(&d->widget), "url")) { - /* Special case: the URL input field gets taller in portrait phone mode to make - the tap target more generous. */ + if ((isPortraitPhone_App() || deviceType_App() == tablet_AppDeviceType) && + !cmp_String(id_Widget(&d->widget), "url")) { + /* Make the tap target more generous. */ return 2.5f * gap_UI; } return 1.25f * gap_UI; -- cgit v1.2.3 From eddaa31cd3799666ae7520db9cd2362c1cce22e8 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Wed, 5 Jan 2022 16:00:08 +0200 Subject: Mobile: Incorrect number of items in page menu --- src/ui/root.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/root.c b/src/ui/root.c index af759427..0a366996 100644 --- a/src/ui/root.c +++ b/src/ui/root.c @@ -1393,7 +1393,7 @@ void createUserInterface_Root(iRoot *d) { { "${menu.page.copyurl}", 0, 0, "document.copylink" }, { "${menu.page.copysource}", 'c', KMOD_PRIMARY, "copy" }, { download_Icon " " saveToDownloads_Label, SDLK_s, KMOD_PRIMARY, "document.save" } }, - 12); + 14); setId_Widget(as_Widget(pageMenuButton), "pagemenubutton"); setFont_LabelWidget(pageMenuButton, uiContentBold_FontId); setAlignVisually_LabelWidget(pageMenuButton, iTrue); -- cgit v1.2.3 From f45839c10ddf30e735354f66f1f4b3886e181437 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Wed, 5 Jan 2022 16:01:21 +0200 Subject: Consistent dialog titles Added a utility for setting up dialog titles consistently. They are now in all-caps and aligned appropriately. --- src/ui/certimportwidget.c | 5 +-- src/ui/documentwidget.c | 2 +- src/ui/uploadwidget.c | 7 +---- src/ui/util.c | 79 ++++++++++++++--------------------------------- src/ui/util.h | 1 + 5 files changed, 27 insertions(+), 67 deletions(-) diff --git a/src/ui/certimportwidget.c b/src/ui/certimportwidget.c index f4dfdefa..e4e461e0 100644 --- a/src/ui/certimportwidget.c +++ b/src/ui/certimportwidget.c @@ -145,10 +145,7 @@ void init_CertImportWidget(iCertImportWidget *d) { else { /* This should behave similar to sheets. */ useSheetStyle_Widget(w); - addChildFlags_Widget( - w, - iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.certimport}", NULL)), - frameless_WidgetFlag); + addDialogTitle_Widget(w, "${heading.certimport}", NULL); d->info = addChildFlags_Widget(w, iClob(new_LabelWidget(infoText_, NULL)), frameless_WidgetFlag); addChild_Widget(w, iClob(makePadding_Widget(gap_UI))); d->crtLabel = new_LabelWidget("", NULL); { diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 9b531957..2ea0a4b7 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -5129,7 +5129,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e { "${menu.page.upload.edit}", 0, 0, "document.upload copy:1" }, { "---" }, { "${menu.page.copyurl}", 0, 0, "document.copylink" } }, - 16); + 17); if (isEmpty_Range(&d->selectMark)) { pushBackN_Array( &items, diff --git a/src/ui/uploadwidget.c b/src/ui/uploadwidget.c index ae777a68..ed448768 100644 --- a/src/ui/uploadwidget.c +++ b/src/ui/uploadwidget.c @@ -224,12 +224,7 @@ void init_UploadWidget(iUploadWidget *d) { else { useSheetStyle_Widget(w); setFlags_Widget(w, overflowScrollable_WidgetFlag, iFalse); - setAllCaps_LabelWidget( - addChildFlags_Widget( - w, - iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.upload}", NULL)), - frameless_WidgetFlag), - iTrue); + addDialogTitle_Widget(w, "${heading.upload}", NULL); iWidget *headings, *values; /* URL path. */ { iWidget *page = makeTwoColumns_Widget(&headings, &values); diff --git a/src/ui/util.c b/src/ui/util.c index e4e4f50b..8cf21538 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -1684,6 +1684,22 @@ void useSheetStyle_Widget(iWidget *d) { iTrue); } +static iLabelWidget *addDialogTitle_(iWidget *dlg, const char *text, const char *id) { + iLabelWidget *label = new_LabelWidget(text, NULL); + addChildFlags_Widget(dlg, iClob(label), alignLeft_WidgetFlag | frameless_WidgetFlag | + resizeToParentWidth_WidgetFlag); + setAllCaps_LabelWidget(label, iTrue); + setTextColor_LabelWidget(label, uiHeading_ColorId); + if (id) { + setId_Widget(as_Widget(label), id); + } + return label; +} + +iLabelWidget *addDialogTitle_Widget(iWidget *dlg, const char *text, const char *idOrNull) { + return addDialogTitle_(dlg, text, idOrNull); +} + static void acceptValueInput_(iWidget *dlg) { const iInputWidget *input = findChild_Widget(dlg, "input"); if (!isEmpty_String(id_Widget(dlg))) { @@ -1858,10 +1874,7 @@ iWidget *makeValueInput_Widget(iWidget *parent, const iString *initialValue, con addChild_Widget(parent, iClob(dlg)); } if (deviceType_App() == desktop_AppDeviceType) { /* conserve space on mobile */ - setId_Widget( - addChildFlags_Widget(dlg, iClob(new_LabelWidget(title, NULL)), - frameless_WidgetFlag), - "valueinput.title"); + addDialogTitle_(dlg, title, "valueinput.title"); } iLabelWidget *promptLabel; setId_Widget(addChildFlags_Widget( @@ -2007,9 +2020,7 @@ iWidget *makeQuestion_Widget(const char *title, const char *msg, } iWidget *dlg = makeSheet_Widget(""); setCommandHandler_Widget(dlg, messageHandler_); - setId_Widget( - addChildFlags_Widget(dlg, iClob(new_LabelWidget(title, NULL)), frameless_WidgetFlag), - "question.title"); + addDialogTitle_(dlg, title, "question.title"); iLabelWidget *msgLabel; setId_Widget(addChildFlags_Widget(dlg, iClob(msgLabel = new_LabelWidget(msg, NULL)), @@ -2186,28 +2197,6 @@ static const iArray *makeFontItems_(const char *id) { 0, format_CStr("!font.set %s:%s", id, cstr_String(&spec->id)) }); } -#if 0 - const struct { - const char * name; - enum iTextFont cfgId; - } fonts[] = { { "Nunito", nunito_TextFont }, - { "Source Sans 3", sourceSans3_TextFont }, - { "Fira Sans", firaSans_TextFont }, - { "---", -1 }, - { "Literata", literata_TextFont }, - { "Tinos", tinos_TextFont }, - { "---", -1 }, - { "Iosevka", iosevka_TextFont } }; - iForIndices(i, fonts) { - pushBack_Array(items, - &(iMenuItem){ fonts[i].name, - 0, - 0, - fonts[i].cfgId >= 0 - ? format_CStr("!%s.set arg:%d", id, fonts[i].cfgId) - : NULL }); - } -#endif pushBack_Array(items, &(iMenuItem){ NULL }); /* terminator */ return items; } @@ -2223,13 +2212,6 @@ static void addFontButtons_(iWidget *parent, const char *id) { addChildFlags_Widget(parent, iClob(button), alignLeft_WidgetFlag); } -#if 0 -static int cmp_MenuItem_(const void *e1, const void *e2) { - const iMenuItem *a = e1, *b = e2; - return iCmpStr(a->label, b->label); -} -#endif - void updatePreferencesLayout_Widget(iWidget *prefs) { if (!prefs || deviceType_App() != desktop_AppDeviceType) { return; @@ -2635,9 +2617,7 @@ iWidget *makePreferences_Widget(void) { return dlg; } iWidget *dlg = makeSheet_Widget("prefs"); - setAllCaps_LabelWidget(addChildFlags_Widget(dlg, - iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.prefs}", NULL)), - frameless_WidgetFlag), iTrue); + addDialogTitle_(dlg, "${heading.prefs}", NULL); iWidget *tabs = makeTabs_Widget(dlg); setBackgroundColor_Widget(findChild_Widget(tabs, "tabs.buttons"), uiBackgroundSidebar_ColorId); setId_Widget(tabs, "prefs.tabs"); @@ -3021,11 +3001,7 @@ iWidget *makeBookmarkEditor_Widget(void) { } else { dlg = makeSheet_Widget("bmed"); - setId_Widget(addChildFlags_Widget( - dlg, - iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.bookmark.edit}", NULL)), - frameless_WidgetFlag), - "bmed.heading"); + addDialogTitle_(dlg, "${heading.bookmark.edit}", "bmed.heading"); iWidget *headings, *values; addChild_Widget(dlg, iClob(makeTwoColumns_Widget(&headings, &values))); iInputWidget *inputs[4]; @@ -3205,9 +3181,7 @@ iWidget *makeFeedSettings_Widget(uint32_t bookmarkId) { } else { dlg = makeSheet_Widget("feedcfg"); - setId_Widget( - addChildFlags_Widget(dlg, iClob(new_LabelWidget(headingText, NULL)), frameless_WidgetFlag), - "feedcfg.heading"); + addDialogTitle_(dlg, headingText, "feedcfg.heading"); iWidget *headings, *values; addChild_Widget(dlg, iClob(makeTwoColumns_Widget(&headings, &values))); iInputWidget *input = new_InputWidget(0); @@ -3286,11 +3260,7 @@ iWidget *makeIdentityCreation_Widget(void) { } else { dlg = makeSheet_Widget("ident"); - setId_Widget(addChildFlags_Widget( - dlg, - iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.newident}", NULL)), - frameless_WidgetFlag), - "ident.heading"); + addDialogTitle_(dlg, "${heading.newident}", "ident.heading"); iWidget *page = new_Widget(); addChildFlags_Widget( dlg, iClob(new_LabelWidget("${dlg.newident.rsa.selfsign}", NULL)), frameless_WidgetFlag); @@ -3428,10 +3398,7 @@ iWidget *makeTranslation_Widget(iWidget *parent) { dlg = makeSheet_Widget("xlt"); setFlags_Widget(dlg, keepOnTop_WidgetFlag, iFalse); dlg->minSize.x = 70 * gap_UI; - addChildFlags_Widget( - dlg, - iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.translate}", NULL)), - frameless_WidgetFlag); + addDialogTitle_(dlg, "${heading.translate}", NULL); addChild_Widget(dlg, iClob(makePadding_Widget(lineHeight_Text(uiLabel_FontId)))); iWidget *headings, *values; iWidget *page; diff --git a/src/ui/util.h b/src/ui/util.h index 618022b9..98ce784c 100644 --- a/src/ui/util.h +++ b/src/ui/util.h @@ -317,6 +317,7 @@ iWidget * makeTwoColumns_Widget (iWidget **headings, iWidget **values); iLabelWidget *dialogAcceptButton_Widget (const iWidget *); +iLabelWidget *addDialogTitle_Widget (iWidget *, const char *text, const char *idOrNull); iInputWidget *addTwoColumnDialogInputField_Widget(iWidget *headings, iWidget *values, const char *labelText, const char *inputId, iInputWidget *input); -- cgit v1.2.3 From d9b4dd25357d6c6d016d2ac7456df560c086ba84 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 6 Jan 2022 07:21:30 +0200 Subject: App: Tell user about IPC command destination --- src/app.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/app.c b/src/app.c index 883ced60..1a24e86c 100644 --- a/src/app.c +++ b/src/app.c @@ -713,18 +713,22 @@ static void communicateWithRunningInstance_App_(iApp *d, iProcessId instance, appendCStr_String(cmds, "tabs.new\n"); requestRaise = iTrue; } + iBool gotResult = iFalse; if (!isEmpty_String(cmds)) { iString *result = communicate_Ipc(cmds, requestRaise); if (result) { fwrite(cstr_String(result), 1, size_String(result), stdout); fflush(stdout); + if (!isEmpty_String(result)) { + gotResult = iTrue; + } } delete_String(result); } iUnused(instance); -// else { -// printf("Lagrange already running (PID %d)\n", instance); -// } + if (!gotResult) { + printf("Commands sent to Lagrange process %d\n", instance); + } terminate_App_(0); } #endif /* defined (LAGRANGE_ENABLE_IPC) */ -- cgit v1.2.3 From d4be7bc3016f0ea74b13eed494f3a38259eb1d0a Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 6 Jan 2022 09:06:21 +0200 Subject: Detect Devanagari text Tell HarfBuzz when Devanagari has been detected. TODO: Check for other languages supported by HarfBuzz (there's a long list). IssueID #345 --- lib/the_Foundation | 2 +- res/about/version.gmi | 1 + src/ui/text.c | 30 ++++++++++++++++++++++++------ 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/lib/the_Foundation b/lib/the_Foundation index cd0a22f5..e2900ed9 160000 --- a/lib/the_Foundation +++ b/lib/the_Foundation @@ -1 +1 @@ -Subproject commit cd0a22f5f003b723ecc038f287a320fb9b7c1d88 +Subproject commit e2900ed99bf8de0768bc091fcbf49d133da167a0 diff --git a/res/about/version.gmi b/res/about/version.gmi index 351e1edc..9e1c4a5f 100644 --- a/res/about/version.gmi +++ b/res/about/version.gmi @@ -30,6 +30,7 @@ Changes and enhancements: * Prevent state file corruption if the app happens to get killed while state is being saved. * Gempub: Open books in 1:2 split mode instead of 1:1. * Minor improvements in page caching. +* Detect when text is Devanagari. Fixes: * Fixed a history caching issue: if there were multiple instances of the same URL in history, only the latest one's content would be used when navigating back/forward. diff --git a/src/ui/text.c b/src/ui/text.c index 4b43f872..0482bfd4 100644 --- a/src/ui/text.c +++ b/src/ui/text.c @@ -651,6 +651,12 @@ static iBool isControl_Char_(iChar c) { /*----------------------------------------------------------------------------------------------*/ iDeclareType(AttributedRun) + +enum iScript { + unspecified_Script, + arabic_Script, + devanagari_Script, +}; struct Impl_AttributedRun { iRangei logical; /* UTF-32 codepoint indices in the logical-order text */ @@ -660,8 +666,7 @@ struct Impl_AttributedRun { iColor bgColor_; /* any RGB color; A > 0 */ struct { uint8_t isLineBreak : 1; -// uint8_t isRTL : 1; - uint8_t isArabic : 1; /* Arabic script detected */ + uint8_t script : 7; /* if script detected */ } flags; }; @@ -753,7 +758,7 @@ static void finishRun_AttributedText_(iAttributedText *d, iAttributedRun *run, i #endif pushBack_Array(&d->runs, &finishedRun); run->flags.isLineBreak = iFalse; - run->flags.isArabic = iFalse; + run->flags.script = unspecified_Script; } run->logical.start = endAt; } @@ -985,11 +990,17 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh (int)logicalText[pos]); #endif } + /* Detect the script. */ + // printf("Char %08x %lc => %s\n", ch, (int) ch, script_Char(ch)); #if defined (LAGRANGE_ENABLE_FRIBIDI) if (fribidi_get_bidi_type(ch) == FRIBIDI_TYPE_AL) { - run.flags.isArabic = iTrue; /* Arabic letter */ + run.flags.script = arabic_Script; } + else #endif + if (!iCmpStr(script_Char(ch), "Devanagari")) { + run.flags.script = devanagari_Script; + } } if (!isEmpty_Range(&run.logical)) { pushBack_Array(&d->runs, &run); @@ -1409,8 +1420,15 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) { } hb_buffer_set_content_type(buf->hb, HB_BUFFER_CONTENT_TYPE_UNICODE); hb_buffer_set_direction(buf->hb, HB_DIRECTION_LTR); /* visual */ - if (run->flags.isArabic) { - hb_buffer_set_script(buf->hb, HB_SCRIPT_ARABIC); + switch (run->flags.script) { + case arabic_Script: + hb_buffer_set_script(buf->hb, HB_SCRIPT_ARABIC); + break; + case devanagari_Script: + hb_buffer_set_script(buf->hb, HB_SCRIPT_DEVANAGARI); + break; + default: + break; } } if (isMonospaced) { -- cgit v1.2.3 From 3a0ea6423cb9e29b1dbadcab375ec1954e47abc4 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 6 Jan 2022 17:44:12 +0200 Subject: Text: Detect Bengali, Oriya, Tamil scripts IssueID #345 --- src/ui/text.c | 43 ++++++++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/src/ui/text.c b/src/ui/text.c index 0482bfd4..c0661ee8 100644 --- a/src/ui/text.c +++ b/src/ui/text.c @@ -655,9 +655,24 @@ iDeclareType(AttributedRun) enum iScript { unspecified_Script, arabic_Script, + bengali_Script, devanagari_Script, + oriya_Script, + tamil_Script, + max_Script }; +#if defined (LAGRANGE_ENABLE_HARFBUZZ) +static const hb_script_t hbScripts_[max_Script] = { + 0, + HB_SCRIPT_ARABIC, + HB_SCRIPT_BENGALI, + HB_SCRIPT_DEVANAGARI, + HB_SCRIPT_ORIYA, + HB_SCRIPT_TAMIL, +}; +#endif + struct Impl_AttributedRun { iRangei logical; /* UTF-32 codepoint indices in the logical-order text */ iTextAttrib attrib; @@ -998,8 +1013,20 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh } else #endif - if (!iCmpStr(script_Char(ch), "Devanagari")) { - run.flags.script = devanagari_Script; + { + const char *scr = script_Char(ch); + if (!iCmpStr(scr, "Bengali")) { + run.flags.script = bengali_Script; + } + else if (!iCmpStr(scr, "Devanagari")) { + run.flags.script = devanagari_Script; + } + else if (!iCmpStr(scr, "Oriya")) { + run.flags.script = oriya_Script; + } + else if (!iCmpStr(scr, "Tamil")) { + run.flags.script = tamil_Script; + } } } if (!isEmpty_Range(&run.logical)) { @@ -1420,15 +1447,9 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) { } hb_buffer_set_content_type(buf->hb, HB_BUFFER_CONTENT_TYPE_UNICODE); hb_buffer_set_direction(buf->hb, HB_DIRECTION_LTR); /* visual */ - switch (run->flags.script) { - case arabic_Script: - hb_buffer_set_script(buf->hb, HB_SCRIPT_ARABIC); - break; - case devanagari_Script: - hb_buffer_set_script(buf->hb, HB_SCRIPT_DEVANAGARI); - break; - default: - break; + const hb_script_t script = hbScripts_[run->flags.script]; + if (script) { + hb_buffer_set_script(buf->hb, script); } } if (isMonospaced) { -- cgit v1.2.3 From ccaae980f8bd91fb58c79730f40f81d38825e3f7 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 7 Jan 2022 07:05:50 +0200 Subject: Layout glitch workaround --- src/ui/util.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ui/util.c b/src/ui/util.c index 8cf21538..a5f48e62 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -3431,6 +3431,7 @@ iWidget *makeTranslation_Widget(iWidget *parent) { addChild_Widget(dlg, iClob(makeDialogButtons_Widget(actions, iElemCount(actions)))); addChild_Widget(parent, iClob(dlg)); arrange_Widget(dlg); + arrange_Widget(dlg); /* TODO: Augh, another layout bug: two arranges required. */ } /* Update choices. */ updateDropdownSelection_LabelWidget(findChild_Widget(dlg, "xlt.from"), -- cgit v1.2.3 From 5a82a64ea145a99482817952b14cb8befd62369c Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 7 Jan 2022 07:05:56 +0200 Subject: Updated release notes --- res/about/version.gmi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/about/version.gmi b/res/about/version.gmi index 9e1c4a5f..efff3e10 100644 --- a/res/about/version.gmi +++ b/res/about/version.gmi @@ -30,7 +30,7 @@ Changes and enhancements: * Prevent state file corruption if the app happens to get killed while state is being saved. * Gempub: Open books in 1:2 split mode instead of 1:1. * Minor improvements in page caching. -* Detect when text is Devanagari. +* Detect when text is Bengali, Devanagari, Oriya, or Tamil. Fixes: * Fixed a history caching issue: if there were multiple instances of the same URL in history, only the latest one's content would be used when navigating back/forward. -- cgit v1.2.3 From ed7b61cf6b4e0e0b23a4a450a8155ce56c93674b Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 7 Jan 2022 07:47:47 +0200 Subject: Text: CJK word wrapping Improve mixed-language word wrapping, and position U+3001 and U+3002 near the baseline. IssueID #380 --- src/ui/text.c | 50 ++++++++++++++++++++++++++++++++++++++++---------- src/ui/util.c | 8 -------- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/src/ui/text.c b/src/ui/text.c index c0661ee8..618f3d79 100644 --- a/src/ui/text.c +++ b/src/ui/text.c @@ -657,17 +657,27 @@ enum iScript { arabic_Script, bengali_Script, devanagari_Script, + han_Script, + hiragana_Script, + katakana_Script, oriya_Script, tamil_Script, max_Script }; +iLocalDef iBool isCJK_Script_(enum iScript d) { + return d == han_Script || d == hiragana_Script || d == katakana_Script; +} + #if defined (LAGRANGE_ENABLE_HARFBUZZ) static const hb_script_t hbScripts_[max_Script] = { 0, HB_SCRIPT_ARABIC, HB_SCRIPT_BENGALI, HB_SCRIPT_DEVANAGARI, + HB_SCRIPT_HAN, + HB_SCRIPT_HIRAGANA, + HB_SCRIPT_KATAKANA, HB_SCRIPT_ORIYA, HB_SCRIPT_TAMIL, }; @@ -1006,7 +1016,6 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh #endif } /* Detect the script. */ - // printf("Char %08x %lc => %s\n", ch, (int) ch, script_Char(ch)); #if defined (LAGRANGE_ENABLE_FRIBIDI) if (fribidi_get_bidi_type(ch) == FRIBIDI_TYPE_AL) { run.flags.script = arabic_Script; @@ -1015,12 +1024,22 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh #endif { const char *scr = script_Char(ch); +// printf("Char %08x %lc => %s\n", ch, (int) ch, scr); if (!iCmpStr(scr, "Bengali")) { run.flags.script = bengali_Script; } else if (!iCmpStr(scr, "Devanagari")) { run.flags.script = devanagari_Script; } + else if (!iCmpStr(scr, "Han")) { + run.flags.script = han_Script; + } + else if (!iCmpStr(scr, "Hiragana")) { + run.flags.script = hiragana_Script; + } + else if (!iCmpStr(scr, "Katakana")) { + run.flags.script = katakana_Script; + } else if (!iCmpStr(scr, "Oriya")) { run.flags.script = oriya_Script; } @@ -1538,8 +1557,11 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) { const float xOffset = run->font->xScale * buf->glyphPos[i].x_offset; const float xAdvance = run->font->xScale * buf->glyphPos[i].x_advance; const iChar ch = logicalText[logPos]; + const enum iWrapTextMode wrapMode = isCJK_Script_(run->flags.script) + ? anyCharacter_WrapTextMode + : args->wrap->mode; iAssert(xAdvance >= 0); - if (args->wrap->mode == word_WrapTextMode) { + if (wrapMode == word_WrapTextMode) { /* When word wrapping, only consider certain places breakable. */ if ((prevCh == '-' || prevCh == '/') && !isPunct_Char(ch)) { safeBreakPos = logPos; @@ -1584,7 +1606,7 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) { wrapPosRange.end = safeBreakPos; } else { - if (args->wrap->mode == word_WrapTextMode && run->logical.start > wrapPosRange.start) { + if (wrapMode == word_WrapTextMode && run->logical.start > wrapPosRange.start) { /* Don't have a word break position, so the whole run needs to be cut. */ wrapPosRange.end = run->logical.start; @@ -1598,7 +1620,7 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) { breakRunIndex = runIndex; } wrapResumePos = wrapPosRange.end; - if (args->wrap->mode != anyCharacter_WrapTextMode) { + if (wrapMode != anyCharacter_WrapTextMode) { while (wrapResumePos < textLen && isSpace_Char(logicalText[wrapResumePos])) { wrapResumePos++; /* skip space */ } @@ -1737,12 +1759,13 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) { /* Already handled this part of the run. */ continue; } - const float xOffset = run->font->xScale * buf->glyphPos[i].x_offset; - const float yOffset = run->font->yScale * buf->glyphPos[i].y_offset; - const float xAdvance = run->font->xScale * buf->glyphPos[i].x_advance; - const float yAdvance = run->font->yScale * buf->glyphPos[i].y_advance; - const iGlyph *glyph = glyphByIndex_Font_(run->font, glyphId); - if (logicalText[logPos] == '\t') { + const float xOffset = run->font->xScale * buf->glyphPos[i].x_offset; + float yOffset = run->font->yScale * buf->glyphPos[i].y_offset; + const float xAdvance = run->font->xScale * buf->glyphPos[i].x_advance; + const float yAdvance = run->font->yScale * buf->glyphPos[i].y_advance; + const iGlyph *glyph = glyphByIndex_Font_(run->font, glyphId); + const iChar ch = logicalText[logPos]; + if (ch == '\t') { #if 0 if (mode & draw_RunMode) { /* Tab indicator. */ @@ -1761,6 +1784,13 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) { } const float xf = xCursor + xOffset; const int hoff = enableHalfPixelGlyphs_Text ? (xf - ((int) xf) > 0.5f ? 1 : 0) : 0; + if (ch == 0x3001 || ch == 0x3002) { + /* Vertical misalignment?? */ + if (yOffset == 0.0f) { + /* Move down to baseline. Why doesn't HarfBuzz do this? */ + yOffset = glyph->d[hoff].y + glyph->rect[hoff].size.y + glyph->d[hoff].y / 4; + } + } /* Output position for the glyph. */ SDL_Rect dst = { orig.x + xCursor + xOffset + glyph->d[hoff].x, orig.y + yCursor - yOffset + glyph->font->baseline + glyph->d[hoff].y, diff --git a/src/ui/util.c b/src/ui/util.c index a5f48e62..77d9a63a 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -3438,14 +3438,6 @@ iWidget *makeTranslation_Widget(iWidget *parent) { languages[prefs_App()->langFrom].command); updateDropdownSelection_LabelWidget(findChild_Widget(dlg, "xlt.to"), languages[prefs_App()->langTo].command); -// updateText_LabelWidget( -// findChild_Widget(dlg, "xlt.from"), -// text_LabelWidget(child_Widget(findChild_Widget(findChild_Widget(dlg, "xlt.from"), "menu"), -// prefs_App()->langFrom))); -// updateText_LabelWidget( -// findChild_Widget(dlg, "xlt.to"), -// text_LabelWidget(child_Widget(findChild_Widget(findChild_Widget(dlg, "xlt.to"), "menu"), -// prefs_App()->langTo))); setCommandHandler_Widget(dlg, translationHandler_); setupSheetTransition_Mobile(dlg, iTrue); return dlg; -- cgit v1.2.3 From a4e740be9aca75789c2fa3bfce4f3e46ac4b7ef3 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 7 Jan 2022 07:54:05 +0200 Subject: Updated release notes --- res/about/version.gmi | 1 + 1 file changed, 1 insertion(+) diff --git a/res/about/version.gmi b/res/about/version.gmi index efff3e10..0131bd82 100644 --- a/res/about/version.gmi +++ b/res/about/version.gmi @@ -39,6 +39,7 @@ Fixes: * Fixed initial scroll position in multiline input fields. * Fixed lookup results list becoming too narrow. * Fixed glitches when a widget has multiple simultanous animations. +* Fixed mixed-language CJK word wrapping. * macOS: Line breaks inside menu items (e.g., info about current identity). ## 1.9.5 -- cgit v1.2.3 From 9d5a4c29efe45644d379eaaa020058beb4dd96f6 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 7 Jan 2022 14:39:19 +0200 Subject: Text: Minor optimization When drawing glyphs, each one of them may be individually cached. Allocate less heap memory during these calls. --- src/ui/text.c | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/ui/text.c b/src/ui/text.c index 618f3d79..7bb418eb 100644 --- a/src/ui/text.c +++ b/src/ui/text.c @@ -1107,10 +1107,10 @@ struct Impl_RasterGlyph { iRect rect; }; -static void cacheGlyphs_Font_(iFont *d, const iArray *glyphIndices) { +static void cacheGlyphs_Font_(iFont *d, const uint32_t *glyphIndices, size_t numGlyphIndices) { /* TODO: Make this an object so it can be used sequentially without reallocating buffers. */ SDL_Surface *buf = NULL; - const iInt2 bufSize = init_I2(iMin(512, d->height * iMin(2 * size_Array(glyphIndices), 20)), + const iInt2 bufSize = init_I2(iMin(512, d->height * iMin(2 * numGlyphIndices, 20)), d->height * 4 / 3); int bufX = 0; iArray * rasters = NULL; @@ -1119,9 +1119,9 @@ static void cacheGlyphs_Font_(iFont *d, const iArray *glyphIndices) { iAssert(isExposed_Window(get_Window())); /* We'll flush the buffered rasters periodically until everything is cached. */ size_t index = 0; - while (index < size_Array(glyphIndices)) { - for (; index < size_Array(glyphIndices); index++) { - const uint32_t glyphIndex = constValue_Array(glyphIndices, index, uint32_t); + while (index < numGlyphIndices) { + for (; index < numGlyphIndices; index++) { + const uint32_t glyphIndex = glyphIndices[index]; const int lastCacheBottom = activeText_->cacheBottom; iGlyph *glyph = glyphByIndex_Font_(d, glyphIndex); if (activeText_->cacheBottom < lastCacheBottom) { @@ -1223,12 +1223,8 @@ static void cacheGlyphs_Font_(iFont *d, const iArray *glyphIndices) { } } -static void cacheSingleGlyph_Font_(iFont *d, uint32_t glyphIndex) { - iArray indices; - init_Array(&indices, sizeof(uint32_t)); - pushBack_Array(&indices, &glyphIndex); - cacheGlyphs_Font_(d, &indices); - deinit_Array(&indices); +iLocalDef void cacheSingleGlyph_Font_(iFont *d, uint32_t glyphIndex) { + cacheGlyphs_Font_(d, &glyphIndex, 1); } static void cacheTextGlyphs_Font_(iFont *d, const iRangecc text) { @@ -1257,7 +1253,7 @@ static void cacheTextGlyphs_Font_(iFont *d, const iRangecc text) { } deinit_AttributedText(&attrText); /* TODO: Cache glyphs from ALL the fonts we encountered above. */ - cacheGlyphs_Font_(d, &glyphIndices); + cacheGlyphs_Font_(d, constData_Array(&glyphIndices), size_Array(&glyphIndices)); deinit_Array(&glyphIndices); } -- cgit v1.2.3 From dbbeddecb55c7bd5ca5239f0c040886cde7e29d7 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 7 Jan 2022 17:07:31 +0200 Subject: App: Optimizing periodic events IssueID #416 --- src/app.c | 12 ++++-------- src/app.h | 1 + src/periodic.c | 25 +++++++++++++++++++++++++ src/periodic.h | 1 + 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/app.c b/src/app.c index 1a24e86c..0a17a665 100644 --- a/src/app.c +++ b/src/app.c @@ -1237,9 +1237,6 @@ iBool findCachedContent_App(const iString *url, iString *mime_out, iBlock *data_ #endif iLocalDef iBool isWaitingAllowed_App_(iApp *d) { - if (!isEmpty_Periodic(&d->periodic)) { - return iFalse; - } if (d->warmupFrames > 0) { return iFalse; } @@ -1253,10 +1250,6 @@ iLocalDef iBool isWaitingAllowed_App_(iApp *d) { static iBool nextEvent_App_(iApp *d, enum iAppEventMode eventMode, SDL_Event *event) { if (eventMode == waitForNewEvents_AppEventMode && isWaitingAllowed_App_(d)) { - /* If there are periodic commands pending, wait only for a short while. */ - if (!isEmpty_Periodic(&d->periodic)) { - return SDL_WaitEventTimeout(event, 500); - } /* We may be allowed to block here until an event comes in. */ if (isWaitingAllowed_App_(d)) { return SDL_WaitEvent(event); @@ -1348,6 +1341,10 @@ void processEvents_App(enum iAppEventMode eventMode) { break; } default: { + if (ev.type == SDL_USEREVENT && ev.user.code == periodic_UserEventCode) { + dispatchCommands_Periodic(&d->periodic); + continue; + } #if defined (LAGRANGE_ENABLE_IDLE_SLEEP) if (ev.type == SDL_USEREVENT && ev.user.code == asleep_UserEventCode) { if (SDL_GetTicks() - d->lastEventTime > idleThreshold_App_ && @@ -1580,7 +1577,6 @@ static int run_App_(iApp *d) { SDL_AddEventWatch(resizeWatcher_, d); /* redraw window during resizing */ #endif while (d->isRunning) { - dispatchCommands_Periodic(&d->periodic); processEvents_App(waitForNewEvents_AppEventMode); runTickers_App_(d); refresh_App(); diff --git a/src/app.h b/src/app.h index d15e1f21..5968de0d 100644 --- a/src/app.h +++ b/src/app.h @@ -61,6 +61,7 @@ enum iUserEventCode { command_UserEventCode = 1, refresh_UserEventCode, asleep_UserEventCode, + periodic_UserEventCode, /* The start of a potential touch tap event is notified via a custom event because sending SDL_MOUSEBUTTONDOWN would be premature: we don't know how long the tap will take, it could turn into a tap-and-hold for example. */ diff --git a/src/periodic.c b/src/periodic.c index c65c8b07..b4f51ed3 100644 --- a/src/periodic.c +++ b/src/periodic.c @@ -57,6 +57,25 @@ iDefineTypeConstructionArgs(PeriodicCommand, (iAny *ctx, const char *cmd), ctx, static const uint32_t postingInterval_Periodic_ = 500; +static uint32_t postEvent_Periodic_(uint32_t interval, void *context) { + iUnused(context); + SDL_UserEvent ev = { .type = SDL_USEREVENT, + .timestamp = SDL_GetTicks(), + .code = periodic_UserEventCode }; + SDL_PushEvent((SDL_Event *) &ev); + return interval; +} + +static void startOrStopWakeupTimer_Periodic_(iPeriodic *d, iBool start) { + if (start && !d->wakeupTimer) { + d->wakeupTimer = SDL_AddTimer(postingInterval_Periodic_, postEvent_Periodic_, d); + } + else if (!start && d->wakeupTimer) { + SDL_RemoveTimer(d->wakeupTimer); + d->wakeupTimer = 0; + } +} + static void removePending_Periodic_(iPeriodic *d) { iForEach(PtrSet, i, &d->pendingRemoval) { size_t pos; @@ -68,6 +87,9 @@ static void removePending_Periodic_(iPeriodic *d) { } } clear_PtrSet(&d->pendingRemoval); + if (isEmpty_SortedArray(&d->commands)) { + startOrStopWakeupTimer_Periodic_(d, iFalse); + } } static iBool isDispatching_; @@ -109,9 +131,11 @@ void init_Periodic(iPeriodic *d) { init_SortedArray(&d->commands, sizeof(iPeriodicCommand), cmp_PeriodicCommand_); d->lastPostTime = 0; init_PtrSet(&d->pendingRemoval); + d->wakeupTimer = 0; } void deinit_Periodic(iPeriodic *d) { + startOrStopWakeupTimer_Periodic_(d, iFalse); deinit_PtrSet(&d->pendingRemoval); iForEach(Array, i, &d->commands.values) { deinit_PeriodicCommand(i.value); @@ -134,6 +158,7 @@ void add_Periodic(iPeriodic *d, iAny *context, const char *command) { init_PeriodicCommand(&pc, context, command); insert_SortedArray(&d->commands, &pc); } + startOrStopWakeupTimer_Periodic_(d, iTrue); unlock_Mutex(d->mutex); } diff --git a/src/periodic.h b/src/periodic.h index a56310a8..f65a4299 100644 --- a/src/periodic.h +++ b/src/periodic.h @@ -35,6 +35,7 @@ struct Impl_Periodic { iSortedArray commands; uint32_t lastPostTime; iPtrSet pendingRemoval; /* contexts */ + int wakeupTimer; /* running while there are pending periodic commands */ }; void init_Periodic (iPeriodic *); -- cgit v1.2.3 From 9166dd94bd21a4feafb2be05e718c09c8abb8ba6 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 7 Jan 2022 17:45:05 +0200 Subject: Updated release notes --- res/about/version.gmi | 1 + 1 file changed, 1 insertion(+) diff --git a/res/about/version.gmi b/res/about/version.gmi index 0131bd82..0579cd74 100644 --- a/res/about/version.gmi +++ b/res/about/version.gmi @@ -40,6 +40,7 @@ Fixes: * Fixed lookup results list becoming too narrow. * Fixed glitches when a widget has multiple simultanous animations. * Fixed mixed-language CJK word wrapping. +* macOS: Fixed high CPU usage during audio playback and UI animations. * macOS: Line breaks inside menu items (e.g., info about current identity). ## 1.9.5 -- cgit v1.2.3 From dcf2ae0ae1c96eee908985c3f2092c9d20fc42ce Mon Sep 17 00:00:00 2001 From: Alyssa Liddell Date: Thu, 6 Jan 2022 11:29:42 +0000 Subject: Translated using Weblate (Russian) Currently translated at 100.0% (644 of 644 strings) Translation: Lagrange/User Interface Translate-URL: http://weblate.skyjake.fi/projects/lagrange/ui/ru/ --- po/ru.po | 184 ++++++++++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 136 insertions(+), 48 deletions(-) diff --git a/po/ru.po b/po/ru.po index 05cfc315..f2fd4591 100644 --- a/po/ru.po +++ b/po/ru.po @@ -3,8 +3,8 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" "POT-Creation-Date: 2021-03-23 19:02+0000\n" -"PO-Revision-Date: 2021-12-08 14:50+0000\n" -"Last-Translator: Nikolay Korotkiy \n" +"PO-Revision-Date: 2022-01-08 14:50+0000\n" +"Last-Translator: Alyssa Liddell \n" "Language-Team: Russian \n" "Language: ru\n" "MIME-Version: 1.0\n" @@ -350,7 +350,7 @@ msgid "history.clear" msgstr "Очистить историю…" msgid "heading.history.clear" -msgstr "ОЧИСТКА ИСТОРИИ" +msgstr "Очистка истории" msgid "dlg.confirm.history.clear" msgstr "Вы действительно хотите стереть историю посещений страниц?" @@ -413,13 +413,13 @@ msgid "ident.showuse" msgstr "Показать использование" msgid "heading.ident.use" -msgstr "ИСПОЛЬЗОВАНИЕ КЛИЕНТСКОГО СЕРТИФИКАТА" +msgstr "Использование клиентского сертификата" msgid "menu.edit.notes" msgstr "Редактировать заметки…" msgid "heading.ident.notes" -msgstr "ЗАМЕТКИ КЛИЕНТСКОГО СЕРТИФИКАТА" +msgstr "Заметки клиентского сертификата" msgid "dlg.ident.notes" msgstr "Заметки о %s:" @@ -431,7 +431,7 @@ msgid "ident.delete" msgstr "Удалить клиентский сертификат…" msgid "heading.ident.delete" -msgstr "УДАЛЕНИЕ КЛИЕНТСКОГО СЕРТИФИКАТА" +msgstr "Удаление клиентского сертификата" msgid "dlg.confirm.ident.delete" msgstr "" @@ -450,7 +450,7 @@ msgstr "" "сертификатах." msgid "heading.unsub" -msgstr "ОТПИСКА" +msgstr "Отписка" msgid "dlg.confirm.unsub" msgstr "Действительно отписаться от ленты «%s»?" @@ -464,7 +464,7 @@ msgstr "" "«%s»." msgid "heading.pageinfo" -msgstr "ИНФОРМАЦИЯ О СТРАНИЦЕ" +msgstr "Информация о странице" msgid "pageinfo.header.cached" msgstr "(кешированный контент)" @@ -509,10 +509,10 @@ msgid "dlg.input.send" msgstr "Отправить" msgid "heading.save" -msgstr "ФАЙЛ СОХРАНЁН" +msgstr "Файл сохранён" msgid "heading.save.incomplete" -msgstr "СТРАНИЦА В ПРОЦЕССЕ ЗАГРУЗКИ" +msgstr "Страница в процессе загрузки" msgid "dlg.save.incomplete" msgstr "Содержимое страницы всё еще загружается." @@ -521,10 +521,10 @@ msgid "dlg.save.size" msgstr "Размер:" msgid "heading.save.error" -msgstr "ОШИБКА СОХРАНЕНИЯ ФАЙЛА" +msgstr "Ошибка сохранения файла" msgid "heading.import.bookmarks" -msgstr "ИМПОРТ ЗАКЛАДОК" +msgstr "Импорт закладок" msgid "dlg.import.found" msgid_plural "dlg.import.found.n" @@ -542,7 +542,7 @@ msgid "dlg.import.notnew" msgstr "Все ссылки на этой странице уже добавлены в закладки." msgid "heading.autoreload" -msgstr "АВТОЗАГРУЗКА" +msgstr "Автозагрузка" msgid "dlg.autoreload" msgstr "Выберите интервал автоматической перезагрузки для этой вкладки." @@ -569,7 +569,7 @@ msgid "link.download" msgstr "Скачать файл" msgid "heading.openlink" -msgstr "ОТКРЫТЬ ССЫЛКУ" +msgstr "Открыть ссылку" msgid "dlg.openlink.confirm" msgstr "" @@ -616,16 +616,16 @@ msgstr "" "домену." msgid "link.hint.audio" -msgstr "Воспроизвести аудио" +msgstr "Аудио" msgid "link.hint.image" -msgstr "Показать изображение" +msgstr "Изображение" msgid "bookmark.title.blank" msgstr "Пустая страница" msgid "heading.translate" -msgstr "ПЕРЕВОД СТРАНИЦЫ" +msgstr "Перевод страницы" msgid "dlg.translate.unavail" msgstr "Сервис недоступен" @@ -676,7 +676,7 @@ msgid "lang.es" msgstr "Испанский" msgid "heading.newident" -msgstr "НОВЫЙ КЛИЕНТСКИЙ СЕРТИФИКАТ" +msgstr "Новый клиентский сертификат" msgid "dlg.newident.rsa.selfsign" msgstr "Создание самоподписанного 2048-битного RSA-сертификата." @@ -718,10 +718,10 @@ msgid "dlg.newident.create" msgstr "Создать сертификат" msgid "heading.feedcfg" -msgstr "НАСТРОЙКИ ЛЕНТЫ" +msgstr "Настройки ленты" msgid "heading.subscribe" -msgstr "ПОДПИСКА НА СТРАНИЦУ" +msgstr "Подписка на страницу" msgid "dlg.feed.title" msgstr "Заголовок:" @@ -742,10 +742,10 @@ msgid "dlg.feed.sub" msgstr "Подписаться" msgid "heading.bookmark.add" -msgstr "ДОБАВИТЬ ЗАКЛАДКУ" +msgstr "Добавить закладку" msgid "heading.bookmark.edit" -msgstr "РЕДАКТИРОВАНИЕ ЗАКЛАДКИ" +msgstr "Редактирование закладки" msgid "dlg.bookmark.save" msgstr "Сохранить закладку" @@ -763,10 +763,10 @@ msgid "dlg.bookmark.icon" msgstr "Иконка:" msgid "heading.prefs" -msgstr "НАСТРОЙКИ" +msgstr "Настройки" msgid "heading.prefs.certs" -msgstr "СЕРТИФИКАТЫ" +msgstr "Сертификаты" msgid "heading.prefs.colors" msgstr "Цвета" @@ -787,22 +787,22 @@ msgid "heading.prefs.network" msgstr "Сеть" msgid "heading.prefs.paragraph" -msgstr "ПАРАГРАФ" +msgstr "Параграф" msgid "heading.prefs.pagecontent" -msgstr "ЦВЕТА СТРАНИЦЫ" +msgstr "Цвета страницы" msgid "heading.prefs.proxies" -msgstr "ПРОКСИ" +msgstr "Прокси" msgid "heading.prefs.scrolling" -msgstr "ПРОКРУТКА" +msgstr "Прокрутка" msgid "heading.prefs.sizing" -msgstr "РАЗМЕР" +msgstr "Размер" msgid "heading.prefs.widelayout" -msgstr "ШИРОКИЙ МАКЕТ" +msgstr "Широкий макет" msgid "heading.prefs.style" msgstr "Стиль" @@ -1060,7 +1060,7 @@ msgid "prefs.uilang" msgstr "Язык:" msgid "heading.certimport" -msgstr "ИМПОРТ КЛИЕНТСКОГО СЕРТИФИКАТА" +msgstr "Импорт клиентского сертификата" msgid "dlg.certimport.notfound" msgstr "Сертификат или закрытый ключ не найдены." @@ -1075,7 +1075,7 @@ msgid "lang.fi" msgstr "Финский" msgid "heading.certimport.pasted" -msgstr "ВСТАВЛЕНО ИЗ БУФЕРА ОБМЕНА" +msgstr "Вставлено из буфера обмена" msgid "dlg.certimport.import" msgstr "Импортировать" @@ -1252,7 +1252,7 @@ msgid "keys.subscribe" msgstr "Подписаться на страницу" msgid "heading.certimport.dropped" -msgstr "ПЕРЕТАЩЕН ФАЙЛ" +msgstr "Перетащен файл" msgid "error.permanent.msg" msgstr "" @@ -1498,10 +1498,10 @@ msgid "dlg.newident.date.past" msgstr "Срок действия не должен заканчиваться в прошлом." msgid "heading.newident.date.bad" -msgstr "НЕПРАВИЛЬНАЯ ДАТА" +msgstr "Неправильная дата" msgid "heading.newident.missing" -msgstr "ТРЕБУЕТСЯ ИНФОРМАЦИЯ" +msgstr "Требуется информация" msgid "keys.split.menu" msgstr "Управлять разделением окна" @@ -1648,7 +1648,7 @@ msgid "link.file.delete" msgstr "Удалить файл" msgid "heading.file.delete" -msgstr "УДАЛИТЬ ФАЙЛ" +msgstr "Удалить файл" msgid "dlg.file.delete.confirm" msgstr "Вы действительно хотите удалить этот файл?" @@ -1657,7 +1657,7 @@ msgid "dlg.file.delete" msgstr "Удалить" msgid "heading.prefs.uitheme" -msgstr "ЦВЕТА ИНТЕРФЕЙСА" +msgstr "Цвета интерфейса" msgid "prefs.scrollspeed.keyboard" msgstr "Скорость клавиатуры:" @@ -1681,10 +1681,10 @@ msgid "menu.unexpire" msgstr "Все равно загрузить" msgid "menu.page.upload" -msgstr "Загрузить страницу (Titan)…" +msgstr "Загрузить (Titan)…" msgid "heading.upload" -msgstr "ЗАГРУЗКА ΤΙΤΑΝ" +msgstr "Загрузка Titan" msgid "heading.upload.text" msgstr "Текст" @@ -1723,7 +1723,7 @@ msgid "prefs.returnkey.accept" msgstr "Подтверждение" msgid "keys.upload" -msgstr "Загрузить страницу (Titan)" +msgstr "Загрузить (Titan)" msgid "error.certexpired" msgstr "Сертификат просрочен" @@ -1736,7 +1736,7 @@ msgid "upload.port" msgstr "Порт…" msgid "heading.uploadport" -msgstr "ПОРТ ЗАГРУЗОК TITAN" +msgstr "Порт загрузок Titan" msgid "dlg.uploadport.msg" msgstr "" @@ -1845,10 +1845,10 @@ msgid "lang.isv" msgstr "Междуславянский" msgid "heading.bookmark.tags" -msgstr "ОСОБЫЕ ТЕГИ" +msgstr "Особые теги" msgid "heading.addfolder" -msgstr "ДОБАВИТЬ ПАПКУ" +msgstr "Добавить папку" msgid "dlg.addfolder.defaulttitle" msgstr "Новая папка" @@ -1861,7 +1861,7 @@ msgstr "Добавить папку" # used on mobile msgid "heading.settings" -msgstr "НАСТРОЙКИ" +msgstr "Настройки" msgid "prefs.imagestyle" msgstr "Окрашивать изображения:" @@ -1898,7 +1898,7 @@ msgid "menu.undo" msgstr "Отмена" msgid "heading.confirm.bookmarks.delete" -msgstr "УДАЛИТЬ ЗАКЛАДКИ" +msgstr "Удалить закладки" # button in the mobile New Identity dialog msgid "dlg.certimport.pickfile" @@ -1941,7 +1941,7 @@ msgstr[1] "%u шрифта" msgstr[2] "%u шрифтов" msgid "heading.fontpack.classic" -msgstr "ЗАГРУЗИТЬ ПАКЕТ ШРИФТОВ" +msgstr "Загрузить пакет шрифтов" msgid "dlg.fontpack.classic.msg" msgstr "" @@ -2050,7 +2050,7 @@ msgid "fontpack.delete" msgstr "Безвозвратно удалить «%s»" msgid "heading.fontpack.delete" -msgstr "УДАЛИТЬ ПАКЕТ ШРИФТОВ" +msgstr "Удалить пакет шрифтов" msgid "dlg.fontpack.delete" msgstr "Удалить пакет шрифтов" @@ -2105,7 +2105,7 @@ msgid "truetype.help.installed" msgstr "Этот шрифт установлен в каталог пользовательских шрифтов." msgid "heading.dismiss.warning" -msgstr "СКРЫТЬ ПРЕДУПРЕЖДЕНИЕ?" +msgstr "Скрыть предупреждение?" msgid "dlg.dismiss.warning" msgstr "Скрыть предупреждение" @@ -2142,3 +2142,91 @@ msgstr "24-часовой формат времени" # This label should be fairly short so it fits in a button in the sidebar. msgid "sidebar.action.feeds.markallread" msgstr "Прочитать все" + +msgid "menu.open.external" +msgstr "Открыть в другом приложении" + +msgid "menu.identities" +msgstr "Управление клиентскими сертификатами" + +# The %s represents the name of an identity. +#, c-format +msgid "ident.switch" +msgstr "Использовать %s" + +# Paste the line preceding the clicked link into the input prompt. +msgid "menu.input.precedingline" +msgstr "Вставить предыдущую строку" + +msgid "menu.page.upload.edit" +msgstr "Редактировать страницу (Titan)…" + +msgid "menu.upload.export" +msgstr "Экспортировать текст" + +# Mobile subheading: buttons for entering uploaded data. +msgid "upload.content" +msgstr "Содержимое" + +msgid "prefs.blink" +msgstr "Мигание курсора мыши:" + +# Active identity toolbar menu. +msgid "menu.hide.identities" +msgstr "Скрыть панель клиентских сертификатов" + +msgid "menu.home" +msgstr "Домой" + +msgid "sidebar.close" +msgstr "Готово" + +msgid "sidebar.action.bookmarks.newfolder" +msgstr "Новая папка" + +msgid "sidebar.action.bookmarks.edit" +msgstr "Редактировать" + +msgid "sidebar.action.history.clear" +msgstr "Очистить" + +msgid "sidebar.empty.unread" +msgstr "Непрочитанных записей нет" + +# Shows where a local file is using the Finder. +msgid "menu.reveal.macos" +msgstr "Показать в Finder" + +msgid "menu.share" +msgstr "Поделиться" + +msgid "heading.upload.id" +msgstr "Авторизация" + +msgid "menu.upload.delete" +msgstr "Удалить всё" + +msgid "menu.upload.delete.confirm" +msgstr "Действительно удалить всё (безвозвратно)" + +# Mobile subheading in the Upload dialog. +msgid "upload.url" +msgstr "URL" + +msgid "hint.upload.path" +msgstr "Путь URL" + +msgid "hint.upload.token.long" +msgstr "токен — см. инструкции сервера" + +msgid "heading.prefs.toolbaractions" +msgstr "Панель инструментов" + +msgid "prefs.toolbaraction1" +msgstr "Кнопка 1" + +msgid "prefs.toolbaraction2" +msgstr "Кнопка 2" + +msgid "keys.upload.edit" +msgstr "Редактировать страницу (Titan)" -- cgit v1.2.3 From 1ff80c99a47f0e674cbedb278fac47ee20e50369 Mon Sep 17 00:00:00 2001 From: Weblate Admin Date: Wed, 5 Jan 2022 14:25:28 +0000 Subject: Translated using Weblate (Finnish) Currently translated at 100.0% (644 of 644 strings) Translation: Lagrange/User Interface Translate-URL: http://weblate.skyjake.fi/projects/lagrange/ui/fi/ --- po/fi.po | 182 ++++++++++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 135 insertions(+), 47 deletions(-) diff --git a/po/fi.po b/po/fi.po index f7c68267..2310fb14 100644 --- a/po/fi.po +++ b/po/fi.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" "POT-Creation-Date: 2021-03-23 09:09+0000\n" -"PO-Revision-Date: 2021-11-30 05:35+0000\n" +"PO-Revision-Date: 2022-01-08 14:50+0000\n" "Last-Translator: Weblate Admin \n" "Language-Team: Finnish \n" "Language: fi\n" @@ -348,7 +348,7 @@ msgid "history.clear" msgstr "Tyhjennä historia…" msgid "heading.history.clear" -msgstr "TYHJENNÄ HISTORIA" +msgstr "Tyhjennä historia" msgid "dlg.confirm.history.clear" msgstr "Haluatko varmasti tyhjentää selaushistorian?" @@ -410,13 +410,13 @@ msgid "ident.showuse" msgstr "Näytä käyttö" msgid "heading.ident.use" -msgstr "IDENTITEETIN KÄYTTÖ" +msgstr "Identiteetin käyttö" msgid "menu.edit.notes" msgstr "Muokkaa muistiinpanoja…" msgid "heading.ident.notes" -msgstr "IDENTITEETIN MUISTIINPANOT" +msgstr "Identiteetin muistiinpanot" msgid "dlg.ident.notes" msgstr "Muistiinpanot liittyen identiteettiin %s:" @@ -428,7 +428,7 @@ msgid "ident.delete" msgstr "Poista identiteetti…" msgid "heading.ident.delete" -msgstr "POISTA IDENTITEETTI" +msgstr "Poista identiteetti" msgid "dlg.confirm.ident.delete" msgstr "" @@ -445,7 +445,7 @@ msgid "ident.gotohelp" msgstr "Katso %sOhjeesta%s lisätietoja TLS-asiakassertifikaateista." msgid "heading.unsub" -msgstr "POISTA TILAUS" +msgstr "Poista tilaus" msgid "dlg.confirm.unsub" msgstr "Haluatko varmasti poistaa syötteen \"%s\" tilauksen?" @@ -458,7 +458,7 @@ msgstr "" "Voit tallentaa sen latauskansioon: paina %s tai valitse \"%s\" valikosta." msgid "heading.pageinfo" -msgstr "SIVUN TIEDOT" +msgstr "Sivun tiedot" msgid "pageinfo.header.cached" msgstr "(sisältö välimuistista)" @@ -503,10 +503,10 @@ msgid "dlg.input.send" msgstr "Lähetä" msgid "heading.save" -msgstr "TIEDOSTO TALLENNETTU" +msgstr "Tiedosto tallennettu" msgid "heading.save.incomplete" -msgstr "SIVUN LATAUS KESKEN" +msgstr "Sivun lataus kesken" msgid "dlg.save.incomplete" msgstr "Sivun sisällön lataus ei ole vielä päättynyt." @@ -515,10 +515,10 @@ msgid "dlg.save.size" msgstr "Koko:" msgid "heading.save.error" -msgstr "VIRHE TALLENTAESSA" +msgstr "Virhe tallentaessa" msgid "heading.import.bookmarks" -msgstr "TUO KIRJANMERKKEJÄ" +msgstr "Tuo kirjanmerkkejä" msgid "dlg.import.found" msgid_plural "dlg.import.found.n" @@ -534,7 +534,7 @@ msgid "dlg.import.notnew" msgstr "Kaikki sivun linkit on jo kirjanmerkkeinä." msgid "heading.autoreload" -msgstr "AUTOMAATTINEN LATAUS" +msgstr "Automaattinen lataus" msgid "dlg.autoreload" msgstr "Valitse välilehden uudelleenlataustiheys." @@ -561,7 +561,7 @@ msgid "link.download" msgstr "Lataa kohde tiedostoon" msgid "heading.openlink" -msgstr "AVAA LINKKI" +msgstr "Avaa linkki" msgid "dlg.openlink.confirm" msgstr "" @@ -604,16 +604,16 @@ msgstr "" "verkkotunnukselle." msgid "link.hint.audio" -msgstr "Toista äänitiedosto" +msgstr "Ääni" msgid "link.hint.image" -msgstr "Katso kuva" +msgstr "Kuva" msgid "bookmark.title.blank" msgstr "Tyhjä sivu" msgid "heading.translate" -msgstr "KÄÄNNÄ SIVU" +msgstr "Käännä sivu" msgid "dlg.translate.unavail" msgstr "Palvelu ei ole toiminnassa" @@ -664,7 +664,7 @@ msgid "lang.es" msgstr "Espanja" msgid "heading.newident" -msgstr "UUSI IDENTITEETTI" +msgstr "Uusi identiteetti" msgid "dlg.newident.rsa.selfsign" msgstr "Luodaan itse allekirjoitettu 2048-bittinen RSA-sertifikaatti." @@ -706,10 +706,10 @@ msgid "dlg.newident.create" msgstr "Luo identiteetti" msgid "heading.feedcfg" -msgstr "SYÖTTEEN ASETUKSET" +msgstr "Syötteen asetukset" msgid "heading.subscribe" -msgstr "TILAA SIVU SYÖTTEENÄ" +msgstr "Tilaa sivu syötteenä" msgid "dlg.feed.title" msgstr "Otsikko:" @@ -730,10 +730,10 @@ msgid "dlg.feed.sub" msgstr "Tilaa" msgid "heading.bookmark.add" -msgstr "LISÄÄ KIRJANMERKKI" +msgstr "Lisää kirjanmerkki" msgid "heading.bookmark.edit" -msgstr "MUOKKAA KIRJANMERKKIÄ" +msgstr "Muokkaa kirjanmerkkiä" msgid "dlg.bookmark.save" msgstr "Talleta kirjanmerkki" @@ -751,10 +751,10 @@ msgid "dlg.bookmark.icon" msgstr "Kuvake:" msgid "heading.prefs" -msgstr "ASETUKSET" +msgstr "Asetukset" msgid "heading.prefs.certs" -msgstr "SERTIFIKAATIT" +msgstr "Sertifikaatit" msgid "heading.prefs.colors" msgstr "Värit" @@ -775,22 +775,22 @@ msgid "heading.prefs.network" msgstr "Verkko" msgid "heading.prefs.paragraph" -msgstr "KAPPALE" +msgstr "Kappale" msgid "heading.prefs.pagecontent" -msgstr "SIVUN VÄRIT" +msgstr "Sivun värit" msgid "heading.prefs.proxies" -msgstr "VÄLIPALVELIMET" +msgstr "Välipalvelimet" msgid "heading.prefs.scrolling" -msgstr "VIERITYS" +msgstr "Vieritys" msgid "heading.prefs.sizing" -msgstr "KOKO" +msgstr "Koko" msgid "heading.prefs.widelayout" -msgstr "LEVEÄ ULKOASU" +msgstr "Leveä ulkoasu" msgid "heading.prefs.style" msgstr "Tyyli" @@ -1042,7 +1042,7 @@ msgid "keys.hoverurl" msgstr "Näytä URL kursorin alla" msgid "heading.certimport" -msgstr "TUO IDENTITEETTI" +msgstr "Tuo identiteetti" msgid "dlg.certimport.help" msgstr "" @@ -1059,10 +1059,10 @@ msgid "dlg.certimport.notfound.page" msgstr "Sivulta ei löytynyt sertifikaattia tai avainta." msgid "heading.certimport.pasted" -msgstr "LIIMATTU LEIKEPÖYDÄLTÄ" +msgstr "Liimattu leikepöydältä" msgid "heading.certimport.dropped" -msgstr "PUDOTETTU TIEDOSTO" +msgstr "Pudotettu tiedosto" msgid "dlg.certimport.import" msgstr "Tuo" @@ -1508,13 +1508,13 @@ msgid "lang.tok" msgstr "Toki Pona" msgid "heading.newident.missing" -msgstr "PUUTTUVA TIETO" +msgstr "Puuttuva tieto" msgid "dlg.newindent.missing.commonname" msgstr "\"Yleisnimi\" on pakollinen tieto." msgid "heading.newident.date.bad" -msgstr "VIRHEELLINEN PÄIVÄMÄÄRÄ" +msgstr "Virheellinen päivämäärä" msgid "dlg.newident.date.past" msgstr "Voimassaolon täytyy päättyä tulevaisuudessa." @@ -1657,7 +1657,7 @@ msgid "link.file.delete" msgstr "Poista tiedosto" msgid "heading.file.delete" -msgstr "POISTA TIEDOSTO" +msgstr "Poista tiedosto" msgid "dlg.file.delete.confirm" msgstr "Oletko varma, että haluat poistaa tämän tiedoston?" @@ -1666,7 +1666,7 @@ msgid "dlg.file.delete" msgstr "Poista" msgid "heading.prefs.uitheme" -msgstr "KÄYTTÖLIITTYMÄN VÄRIT" +msgstr "Käyttöliittymän värit" msgid "prefs.scrollspeed.keyboard" msgstr "Näppäimistön nopeus:" @@ -1690,10 +1690,10 @@ msgid "menu.unexpire" msgstr "Jatka lataamista vanhentumisesta huolimatta" msgid "menu.page.upload" -msgstr "Lähetä sivu Titanilla…" +msgstr "Lähetä Titanilla…" msgid "heading.upload" -msgstr "TITAN-LÄHETYS" +msgstr "Titan-lähetys" msgid "heading.upload.text" msgstr "Teksti" @@ -1735,7 +1735,7 @@ msgid "prefs.returnkey" msgstr "Palautusnäppäin:" msgid "keys.upload" -msgstr "Lähetä sivu Titanilla" +msgstr "Lähetä Titanilla" msgid "error.certexpired" msgstr "Vanhentunut sertifikaatti" @@ -1749,7 +1749,7 @@ msgid "upload.port" msgstr "Portti…" msgid "heading.uploadport" -msgstr "TITAN-LÄHETYSPORTTI" +msgstr "Titan-lähetysportti" msgid "dlg.uploadport.msg" msgstr "" @@ -1780,7 +1780,7 @@ msgid "menu.sort.alpha" msgstr "Järjestä aakkosjärjestykseen" msgid "heading.confirm.bookmarks.delete" -msgstr "TUHOA KIRJANMERKIT" +msgstr "Tuhoa kirjanmerkit" #, c-format msgid "dlg.confirm.bookmarks.delete" @@ -1803,7 +1803,7 @@ msgstr "Lähetä tekstiä" # used on mobile msgid "heading.settings" -msgstr "ASETUKSET" +msgstr "Asetukset" msgid "prefs.imagestyle" msgstr "Kuvien väritys:" @@ -1862,10 +1862,10 @@ msgid "dlg.upload.pickfile" msgstr "Valitse tiedosto" msgid "heading.bookmark.tags" -msgstr "ERIKOISAVAINSANAT" +msgstr "Erikoisavainsanat" msgid "heading.addfolder" -msgstr "LISÄÄ KANSIO" +msgstr "Lisää kansio" msgid "dlg.addfolder.defaulttitle" msgstr "Uusi kansio" @@ -2074,7 +2074,7 @@ msgid "fontpack.delete" msgstr "Poista ja tuhoa \"%s\"" msgid "heading.fontpack.delete" -msgstr "POISTA FONTTIPAKETTI" +msgstr "Poista fonttipaketti" msgid "dlg.fontpack.delete" msgstr "Tuhoa fonttipaketti" @@ -2092,7 +2092,7 @@ msgid "truetype.help.installed" msgstr "Tämä fontti on asennettu käyttäjän fonttihakemistoon." msgid "heading.dismiss.warning" -msgstr "POISTA VAROITUS?" +msgstr "Poista varoitus?" #, c-format msgid "dlg.dismiss.ansi" @@ -2105,7 +2105,7 @@ msgid "dlg.fontpack.classic" msgstr "Lataa fonttipaketti (25 Mt)" msgid "heading.fontpack.classic" -msgstr "LATAA FONTTIPAKETTI" +msgstr "Lataa fonttipaketti" msgid "dlg.fontpack.classic.msg" msgstr "" @@ -2140,3 +2140,91 @@ msgstr "24 tunnin kellonajat" # This label should be fairly short so it fits in a button in the sidebar. msgid "sidebar.action.feeds.markallread" msgstr "Lue kaikki" + +msgid "menu.page.upload.edit" +msgstr "Muokkaa sivua Titanilla…" + +msgid "heading.upload.id" +msgstr "Valtuudet" + +msgid "menu.upload.export" +msgstr "Vie teksti" + +msgid "menu.upload.delete" +msgstr "Poista kaikki" + +# Mobile subheading in the Upload dialog. +msgid "upload.url" +msgstr "URL" + +# Mobile subheading: buttons for entering uploaded data. +msgid "upload.content" +msgstr "Sisältö" + +msgid "hint.upload.path" +msgstr "Polku" + +msgid "hint.upload.token.long" +msgstr "avainsana — katso palvelimen ohjeet" + +msgid "prefs.toolbaraction1" +msgstr "Painike 1" + +msgid "prefs.toolbaraction2" +msgstr "Painike 2" + +msgid "keys.upload.edit" +msgstr "Muokkaa sivua Titanilla" + +# The %s represents the name of an identity. +#, c-format +msgid "ident.switch" +msgstr "Ota käyttöön %s" + +msgid "menu.upload.delete.confirm" +msgstr "Poista kaikki (ei voi peruuttaa)" + +msgid "heading.prefs.toolbaractions" +msgstr "Työkalupalkin toiminnot" + +msgid "prefs.blink" +msgstr "Vilkkuva kursori:" + +msgid "menu.open.external" +msgstr "Avaa toisessa sovelluksessa" + +# Active identity toolbar menu. +msgid "menu.hide.identities" +msgstr "Piilota identiteetit" + +msgid "menu.home" +msgstr "Mene kotiin" + +msgid "menu.identities" +msgstr "Hallinnoi identiteettejä" + +msgid "sidebar.close" +msgstr "Valmis" + +msgid "sidebar.action.bookmarks.newfolder" +msgstr "Uusi kansio" + +msgid "sidebar.action.bookmarks.edit" +msgstr "Muokkaa" + +msgid "sidebar.action.history.clear" +msgstr "Tyhjennä" + +msgid "sidebar.empty.unread" +msgstr "Kaikki luettu" + +# Paste the line preceding the clicked link into the input prompt. +msgid "menu.input.precedingline" +msgstr "Liimaa edeltävä rivi" + +# Shows where a local file is using the Finder. +msgid "menu.reveal.macos" +msgstr "Näytä Finderissa" + +msgid "menu.share" +msgstr "Jaa" -- cgit v1.2.3 From 99df3baa941c5033286ad97ed5f07d5b5bfa3a6a Mon Sep 17 00:00:00 2001 From: Alyssa Liddell Date: Thu, 6 Jan 2022 21:57:10 +0000 Subject: Translated using Weblate (German) Currently translated at 94.5% (609 of 644 strings) Translation: Lagrange/User Interface Translate-URL: http://weblate.skyjake.fi/projects/lagrange/ui/de/ --- po/de.po | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/po/de.po b/po/de.po index 2b662ed6..cae63f6c 100644 --- a/po/de.po +++ b/po/de.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" -"PO-Revision-Date: 2021-11-27 04:58+0000\n" +"PO-Revision-Date: 2022-01-08 14:50+0000\n" "Last-Translator: Alyssa Liddell \n" "Language-Team: German \n" "Language: de\n" @@ -1167,7 +1167,7 @@ msgid "dlg.certimport.nokey" msgstr "Kein privater Schlüssel" msgid "link.hint.audio" -msgstr "Audio abspielen" +msgstr "Audio" # tab button msgid "heading.prefs.userinterface" @@ -2125,3 +2125,32 @@ msgstr "Hintergrundfarbe" msgid "sidebar.action.show" msgstr "Anzeigen:" + +msgid "sidebar.close" +msgstr "Fertig" + +msgid "sidebar.action.bookmarks.newfolder" +msgstr "Neuer Ordner" + +msgid "sidebar.action.bookmarks.edit" +msgstr "Bearbeiten" + +msgid "menu.upload.delete" +msgstr "Alles löschen" + +# Mobile subheading in the Upload dialog. +msgid "upload.url" +msgstr "URL" + +# Mobile subheading: buttons for entering uploaded data. +msgid "upload.content" +msgstr "Inhalt" + +msgid "menu.page.upload.edit" +msgstr "Seite mit Titan bearbeiten…" + +msgid "prefs.toolbaraction1" +msgstr "Taste 1" + +msgid "prefs.toolbaraction2" +msgstr "Taste 2" -- cgit v1.2.3 From 5a24521d8c12054b6a7ffb409b69eb8500da0612 Mon Sep 17 00:00:00 2001 From: Страхиња Радић Date: Thu, 6 Jan 2022 18:16:15 +0000 Subject: Translated using Weblate (Serbian) Currently translated at 100.0% (644 of 644 strings) Translation: Lagrange/User Interface Translate-URL: http://weblate.skyjake.fi/projects/lagrange/ui/sr/ --- po/sr.po | 184 ++++++++++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 136 insertions(+), 48 deletions(-) diff --git a/po/sr.po b/po/sr.po index a28f5bb7..4fdd4ee0 100644 --- a/po/sr.po +++ b/po/sr.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" -"PO-Revision-Date: 2021-12-01 16:50+0000\n" +"PO-Revision-Date: 2022-01-08 14:50+0000\n" "Last-Translator: Страхиња Радић \n" "Language-Team: Serbian \n" "Language: sr\n" @@ -289,7 +289,7 @@ msgid "dlg.unsub" msgstr "Уклонити претплату" msgid "heading.pageinfo" -msgstr "ПОДАЦИ О СТРАНИЦИ" +msgstr "Подаци о страници" msgid "pageinfo.header.cached" msgstr "(кеширани садржај)" @@ -316,7 +316,7 @@ msgid "pageinfo.cert.untrusted" msgstr "Без поверења" msgid "heading.unsub" -msgstr "УКЛАЊАЊЕ ПРЕТПЛАТЕ" +msgstr "Уклањање претплате" msgid "pageinfo.domain.mismatch" msgstr "Назив домена не одговара" @@ -331,10 +331,10 @@ msgid "dlg.input.send" msgstr "Пошаљи" msgid "heading.save" -msgstr "ДАТОТЕКА САЧУВАНА" +msgstr "Датотека је сачувана" msgid "heading.save.incomplete" -msgstr "НЕПОТПУНА СТРАНИЦА" +msgstr "Непотпуна страница" msgid "dlg.save.incomplete" msgstr "Садржај странице се и даље преузима." @@ -343,7 +343,7 @@ msgid "dlg.save.size" msgstr "Величина:" msgid "heading.import.bookmarks" -msgstr "УВОЗ ОБЕЛЕЖИВАЧА" +msgstr "Увоз обележивача" #, c-format msgid "dlg.import.add" @@ -356,7 +356,7 @@ msgid "dlg.import.notnew" msgstr "Све везе на овој страници су већ додате у обележиваче." msgid "heading.autoreload" -msgstr "АУТОМАТСКО УЧИТАВАЊЕ" +msgstr "Аутоматско освежавање" msgid "reload.never" msgstr "Никад" @@ -394,7 +394,7 @@ msgid "link.bookmark" msgstr "Додај везу у обележиваче…" msgid "heading.openlink" -msgstr "ОТВАРАЊЕ ВЕЗЕ" +msgstr "Отварање везе" msgid "dlg.openlink" msgstr "Отвори везу" @@ -403,10 +403,10 @@ msgid "dlg.certimport.notfound.page" msgstr "Ниједан сертификат нити кључ није пронађен на текућој страници." msgid "heading.certimport.pasted" -msgstr "НАЛЕПЉЕНО ИЗ КЛИПБОРДА" +msgstr "Налепљено из клипборда" msgid "heading.certimport.dropped" -msgstr "ПРЕВУЧЕНА ДАТОТЕКА" +msgstr "Превучена датотека" msgid "dlg.certimport.import" msgstr "Увези" @@ -421,7 +421,7 @@ msgid "dlg.certimport.nocert" msgstr "Без сертификата" msgid "link.hint.audio" -msgstr "Пусти аудио" +msgstr "Аудио" msgid "bookmark.title.blank" msgstr "Празна страница" @@ -447,7 +447,7 @@ msgid "heading.lookup.other" msgstr "ОСТАЛО" msgid "heading.translate" -msgstr "ПРЕВОД СТРАНИЦЕ" +msgstr "Превод странице" msgid "dlg.translate.unavail" msgstr "Сервис недоступан" @@ -495,7 +495,7 @@ msgid "lang.es" msgstr "Шпански" msgid "heading.newident" -msgstr "НОВ ИДЕНТИТЕТ" +msgstr "Нов идентитет" msgid "dlg.newident.until" msgstr "Важи до:" @@ -534,7 +534,7 @@ msgid "dlg.feed.sub" msgstr "Претплати се" msgid "heading.bookmark.add" -msgstr "ДОДАВАЊЕ ОБЕЛЕЖИВАЧА" +msgstr "Додавање обележивача" msgid "dlg.bookmark.save" msgstr "Сачувај обележивач" @@ -552,10 +552,10 @@ msgid "dlg.bookmark.icon" msgstr "Икона:" msgid "heading.prefs" -msgstr "ПОДЕШАВАЊА" +msgstr "Подешавања" msgid "heading.prefs.certs" -msgstr "СЕРТИФИКАТИ" +msgstr "Сертификати" msgid "heading.prefs.fonts" msgstr "Фонтови" @@ -573,16 +573,16 @@ msgid "heading.prefs.network" msgstr "Мрежа" msgid "heading.prefs.paragraph" -msgstr "ПАСУС" +msgstr "Пасус" msgid "heading.prefs.pagecontent" -msgstr "БОЈЕ СТРАНИЦЕ" +msgstr "Боје странице" msgid "heading.prefs.sizing" -msgstr "ВЕЛИЧИНА" +msgstr "Величина" msgid "heading.prefs.widelayout" -msgstr "ШИРОКИ ИЗГЛЕД" +msgstr "Широки изглед" # tab button msgid "heading.prefs.style" @@ -890,7 +890,7 @@ msgid "about.tagline" msgstr "Прелепи Џемини клијент" msgid "heading.history.clear" -msgstr "ЧИШЋЕЊЕ ИСТОРИЈЕ" +msgstr "Чишћење историје" msgid "error.unavail.msg" msgstr "" @@ -915,7 +915,7 @@ msgid "menu.title.file" msgstr "Датотека" msgid "heading.certimport" -msgstr "УВОЗ ИДЕНТИТЕТА" +msgstr "Увоз идентитета" msgid "error.proxy" msgstr "Грешка проксија" @@ -949,7 +949,7 @@ msgid "dlg.certimport.nokey" msgstr "Без приватног кључа" msgid "link.hint.image" -msgstr "Прикажи слику" +msgstr "Слика" msgid "menu.closetab.other" msgstr "Затвори остале картице" @@ -1021,13 +1021,13 @@ msgid "menu.show.outline" msgstr "Прикажи структуру странице" msgid "heading.feedcfg" -msgstr "ПОДЕШАВАЊА ФИДА" +msgstr "Подешавања фида" msgid "menu.autoreload" msgstr "Постави аутоматско учитавање…" msgid "heading.subscribe" -msgstr "ПРЕТПЛАТА НА СТРАНИЦУ" +msgstr "Претплата на страницу" msgid "menu.page.subscribe" msgstr "Претплати се на страницу…" @@ -1045,7 +1045,7 @@ msgid "menu.bookmarks.refresh" msgstr "Освежи удаљене обележиваче" msgid "heading.bookmark.edit" -msgstr "УРЕЂИВАЊЕ ОБЕЛЕЖИВАЧА" +msgstr "Уређивање обележивача" msgid "sidebar.feeds" msgstr "Фидови" @@ -1065,7 +1065,7 @@ msgid "sidebar.identities" msgstr "Идентитети" msgid "heading.prefs.proxies" -msgstr "ПРОКСИЈИ" +msgstr "Проксији" # Usage: "(count) Unread" in the sidebar tab title, referring to feed entries. msgid "sidebar.unread" @@ -1075,7 +1075,7 @@ msgstr[1] "непрочитана" msgstr[2] "непрочитаних" msgid "heading.prefs.scrolling" -msgstr "СКРОЛОВАЊЕ" +msgstr "Скроловање" msgid "status.query" msgstr "Упит за претрагу" @@ -1205,7 +1205,7 @@ msgid "prefs.proxy.gopher" msgstr "Прокси за Gopher:" msgid "heading.ident.use" -msgstr "УПОТРЕБА ИДЕНТИТЕТА" +msgstr "Употреба идентитета" msgid "keys.scroll.halfpage.up" msgstr "Скроловање нагоре за пола странице" @@ -1217,13 +1217,13 @@ msgid "keys.scroll.page.down" msgstr "Скроловање надоле за једну страницу" msgid "heading.ident.notes" -msgstr "ЗАБЕЛЕШКЕ О ИДЕНТИТЕТУ" +msgstr "Забелешке о идентитету" msgid "keys.link.modkey" msgstr "Отварање везе преко тастера модификатора" msgid "heading.ident.delete" -msgstr "БРИСАЊЕ ИДЕНТИТЕТА" +msgstr "Брисање идентитета" msgid "keys.link.homerow.newtab" msgstr "Отварање везе у новој картици преко тастера основног реда" @@ -1277,7 +1277,7 @@ msgid "keys.hoverurl" msgstr "Смена приказивања УРЛ-ова при надношењу показивача" msgid "heading.save.error" -msgstr "ГРЕШКА ПРИ ЧУВАЊУ ДАТОТЕКЕ" +msgstr "Грешка при чувању датотеке" msgid "error.badstatus.msg" msgstr "" @@ -1390,7 +1390,7 @@ msgstr[2] "пре %d дана" # Link download progress message. msgid "doc.fetching" -msgstr "Допремање" +msgstr "Учитавање" # Inline download status message. msgid "media.download.warnclose" @@ -1521,10 +1521,10 @@ msgid "link.side" msgstr "Отвори везу са стране" msgid "heading.newident.missing" -msgstr "НЕМА ИНФОРМАЦИЈА" +msgstr "Нема информација" msgid "heading.newident.date.bad" -msgstr "НЕИСПРАВАН ДАТУМ" +msgstr "Неисправан датум" msgid "dlg.newident.date.past" msgstr "Рок трајања мора бити у будућности." @@ -1696,7 +1696,7 @@ msgid "link.file.delete" msgstr "Обриши датотеку" msgid "heading.file.delete" -msgstr "БРИСАЊЕ ДАТОТЕКЕ" +msgstr "Брисање датотеке" msgid "dlg.file.delete.confirm" msgstr "Да ли сте сигурни да желите да обришете ову датотеку?" @@ -1705,7 +1705,7 @@ msgid "dlg.file.delete" msgstr "Обриши" msgid "heading.prefs.uitheme" -msgstr "БОЈЕ ИНТЕРФЕЈСА" +msgstr "Боје интерфејса" msgid "prefs.scrollspeed.keyboard" msgstr "Брзина тастатуре:" @@ -1742,7 +1742,7 @@ msgid "upload.port" msgstr "Порт…" msgid "heading.uploadport" -msgstr "ПОРТ ЗА СЛАЊЕ ПРЕКО ТИТАНА" +msgstr "Порт за слање преко Титана" msgid "dlg.uploadport.msg" msgstr "" @@ -1761,10 +1761,10 @@ msgid "prefs.returnkey.accept" msgstr "Прихватање" msgid "menu.page.upload" -msgstr "Пошаљи страницу преко Титана…" +msgstr "Пошаљи преко Титана…" msgid "heading.upload" -msgstr "СЛАЊЕ ПРЕКО ТИТАНА" +msgstr "Слање преко Титана" msgid "heading.upload.text" msgstr "Текст" @@ -1801,7 +1801,7 @@ msgstr "" "Повезивање са сервером је поништено јер је његов TLS сертификат истекао." msgid "keys.upload" -msgstr "Слање странице преко Титана" +msgstr "Слање преко Титана" msgid "media.untitled.image" msgstr "Слика" @@ -1907,7 +1907,7 @@ msgid "menu.undo" msgstr "Опозови" msgid "heading.confirm.bookmarks.delete" -msgstr "БРИСАЊЕ ОБЕЛЕЖИВАЧА" +msgstr "Брисање обележивача" #, c-format msgid "dlg.bookmarks.delete" @@ -1934,10 +1934,10 @@ msgid "dlg.upload.pickfile" msgstr "Изабери датотеку" msgid "heading.bookmark.tags" -msgstr "ПОСЕБНЕ ОЗНАКЕ" +msgstr "Посебне ознаке" msgid "heading.addfolder" -msgstr "ДОДАВАЊЕ ФАСЦИКЛЕ" +msgstr "Додавање фасцикле" msgid "dlg.addfolder.defaulttitle" msgstr "Нова фасцикла" @@ -1947,7 +1947,7 @@ msgstr "Додавање фасцикле" # used on mobile msgid "heading.settings" -msgstr "ПОДЕШАВАЊА" +msgstr "Подешавања" msgid "prefs.imagestyle.original" msgstr "Ниједно" @@ -1980,7 +1980,7 @@ msgid "lang.isv" msgstr "Међусловенски" msgid "heading.fontpack.classic" -msgstr "ПРЕУЗИМАЊЕ ПАКЕТА ФОНТОВА" +msgstr "Преузимање пакета фонтова" msgid "prefs.gemtext.ansi.fontstyle" msgstr "Стил фонта" @@ -2087,7 +2087,7 @@ msgid "fontpack.install" msgstr "Инсталирај „%s“" msgid "heading.fontpack.delete" -msgstr "БРИСАЊЕ ПАКЕТА ФОНТОВА" +msgstr "Брисање пакета фонтова" msgid "dlg.fontpack.delete" msgstr "Обриши пакет" @@ -2102,7 +2102,7 @@ msgid "truetype.help.installed" msgstr "Фонт је инсталиран у корисничком директоријуму са фонтовима." msgid "heading.dismiss.warning" -msgstr "ОТКЛОНИТИ УПОЗОРЕЊЕ?" +msgstr "Отклонити упозорење?" msgid "dlg.dismiss.warning" msgstr "Отклони упозорење" @@ -2189,3 +2189,91 @@ msgstr "24-часовно време" # This label should be fairly short so it fits in a button in the sidebar. msgid "sidebar.action.feeds.markallread" msgstr "Прочитај све" + +msgid "menu.open.external" +msgstr "Отвори у другој апликацији" + +# Active identity toolbar menu. +msgid "menu.hide.identities" +msgstr "Сакриј идентитете" + +msgid "menu.identities" +msgstr "Управљај идентитетима" + +msgid "sidebar.action.history.clear" +msgstr "Очисти" + +# Paste the line preceding the clicked link into the input prompt. +msgid "menu.input.precedingline" +msgstr "Уметни претходни ред" + +msgid "menu.upload.delete.confirm" +msgstr "Заиста обриши све (неповратно)" + +# Mobile subheading: buttons for entering uploaded data. +msgid "upload.content" +msgstr "Садржај" + +msgid "hint.upload.token.long" +msgstr "токен — видети инструкције сервера" + +msgid "menu.home" +msgstr "Иди на почетну" + +msgid "sidebar.close" +msgstr "Готово" + +msgid "sidebar.action.bookmarks.newfolder" +msgstr "Нова фасцикла" + +msgid "sidebar.action.bookmarks.edit" +msgstr "Уреди" + +# The %s represents the name of an identity. +#, c-format +msgid "ident.switch" +msgstr "Користити %s" + +msgid "sidebar.empty.unread" +msgstr "Нема непрочитаних ставки" + +# Shows where a local file is using the Finder. +msgid "menu.reveal.macos" +msgstr "Прикажи у Finder-у" + +msgid "menu.share" +msgstr "Подели" + +msgid "menu.page.upload.edit" +msgstr "Уреди страницу преко Титана…" + +msgid "heading.upload.id" +msgstr "Ауторизација" + +msgid "menu.upload.export" +msgstr "Извези текст" + +msgid "menu.upload.delete" +msgstr "Обриши све" + +# Mobile subheading in the Upload dialog. +msgid "upload.url" +msgstr "УРЛ" + +msgid "hint.upload.path" +msgstr "УРЛ путања" + +msgid "heading.prefs.toolbaractions" +msgstr "Акције палете алатки" + +msgid "prefs.toolbaraction1" +msgstr "Дугме 1" + +msgid "prefs.toolbaraction2" +msgstr "Дугме 2" + +msgid "prefs.blink" +msgstr "Трепћући курсор:" + +msgid "keys.upload.edit" +msgstr "Уређивање странице преко Титана" -- cgit v1.2.3 From dca6c9cec06495bde7374342e85a0ae81441f1b0 Mon Sep 17 00:00:00 2001 From: jan Anja Date: Wed, 5 Jan 2022 19:35:30 +0000 Subject: Translated using Weblate (Toki Pona (tok)) Currently translated at 96.5% (622 of 644 strings) Translation: Lagrange/User Interface Translate-URL: http://weblate.skyjake.fi/projects/lagrange/ui/tok/ --- po/tok.po | 147 ++++++++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 100 insertions(+), 47 deletions(-) diff --git a/po/tok.po b/po/tok.po index a8e7e901..01698208 100644 --- a/po/tok.po +++ b/po/tok.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" -"PO-Revision-Date: 2021-12-08 14:50+0000\n" +"PO-Revision-Date: 2022-01-08 14:50+0000\n" "Last-Translator: jan Anja \n" "Language-Team: Toki Pona \n" @@ -29,7 +29,7 @@ msgstr "sitelen namako li jo ala e sitelen pona" # Link download progress message. msgid "doc.fetching" -msgstr "mi kama jo" +msgstr "lipu li kama" # Inline download status message. msgid "media.download.complete" @@ -389,7 +389,7 @@ msgid "history.clear" msgstr "o kama sin e sona pi tenpo pini…" msgid "heading.history.clear" -msgstr "SONA PI TENPO PINI LI WILE KAMA SIN" +msgstr "o kama sin e sona pi tenpo pini" msgid "ident.using" msgstr "lipu ni li kepeken e ona" @@ -428,25 +428,25 @@ msgid "dlg.ident.notes" msgstr "toki lili pi nimi %s:" msgid "heading.ident.use" -msgstr "LIPU NI LI KEPEKEN E ONA" +msgstr "lipu ni li kepeken e ona" msgid "menu.edit.notes" msgstr "o ante e toki lili…" msgid "heading.ident.notes" -msgstr "TOKI LILI NIMI" +msgstr "toki lili nimi" msgid "ident.delete" msgstr "o weka e ona…" msgid "heading.ident.delete" -msgstr "NIMI LI WILE TAWA WEKA" +msgstr "o weka e nimi" msgid "ident.fingerprint" msgstr "o kama sona e sitelen len lili" msgid "heading.unsub" -msgstr "O PINI LUKIN" +msgstr "o pini lukin" #, c-format msgid "dlg.confirm.unsub" @@ -499,10 +499,10 @@ msgid "dlg.cert.trust" msgstr "o kepeken" msgid "heading.save" -msgstr "IJO LI AWEN" +msgstr "ijo li awen" msgid "heading.save.incomplete" -msgstr "LIPU LI WAN ALA" +msgstr "lipu li wan ala" msgid "dlg.save.incomplete" msgstr "mi kin kama jo e lipu ni." @@ -511,10 +511,10 @@ msgid "dlg.save.size" msgstr "suli:" msgid "heading.save.error" -msgstr "PAKALA! MI AWEN ALA E IJO" +msgstr "pakala! mi awen ala e ijo" msgid "heading.import.bookmarks" -msgstr "O KAMA JO E LIPU AWEN" +msgstr "o kama jo e lipu awen" msgid "reload.onceperday" msgstr "tenpo wan pi tenpo suno" @@ -532,7 +532,7 @@ msgid "link.bookmark" msgstr "o awen e nimi tawa…" msgid "heading.openlink" -msgstr "O OPEN E NIMI TAWA" +msgstr "o open e nimi tawa" #, c-format msgid "dlg.import.add" @@ -546,7 +546,7 @@ msgid "link.noproxy" msgstr "o open kepeken ala lupa" msgid "heading.autoreload" -msgstr "O SIN E LIPU" +msgstr "o sin e lipu" #, c-format msgid "num.minutes" @@ -586,16 +586,16 @@ msgid "dlg.certimport.notfound.page" msgstr "sitelen len en ijo len li lon ala lon lipu ni." msgid "heading.certimport" -msgstr "O KAMA JO E NIMI" +msgstr "o kama jo e nimi" msgid "heading.certimport.pasted" -msgstr "PANA TAN SONA" +msgstr "pana tan sona" msgid "bookmark.title.blank" msgstr "lipu li jo e ala insa" msgid "heading.certimport.dropped" -msgstr "TAWA TAN IJO" +msgstr "tawa tan ijo" msgid "dlg.certimport.import" msgstr "o kama jo" @@ -610,10 +610,10 @@ msgid "dlg.certimport.nokey" msgstr "ijo len li lon ala" msgid "link.hint.audio" -msgstr "o kute e kalama musi" +msgstr "kalama musi" msgid "link.hint.image" -msgstr "o lukin e sitelen" +msgstr "sitelen" # Interpret as "Results from bookmarks..." msgid "heading.lookup.bookmarks" @@ -632,7 +632,7 @@ msgid "heading.lookup.other" msgstr "TAN IJO ANTE" msgid "heading.translate" -msgstr "O TOKI ANTE E LIPU" +msgstr "o toki ante e lipu" msgid "dlg.translate.fail" msgstr "toki li pakala" @@ -719,7 +719,7 @@ msgid "dlg.newident.create" msgstr "o pali e nimi" msgid "heading.subscribe" -msgstr "O LUKIN E IJO SIN LON LIPU" +msgstr "o lukin e ijo sin lon lipu" msgid "dlg.feed.title" msgstr "nimi:" @@ -740,10 +740,10 @@ msgid "dlg.feed.sub" msgstr "o lukin" msgid "heading.bookmark.add" -msgstr "O AWEN E LIPU" +msgstr "o awen e lipu" msgid "heading.bookmark.edit" -msgstr "O ANTE E LIPU AWEN" +msgstr "o ante e lipu awen" msgid "dlg.bookmark.save" msgstr "o awen" @@ -758,7 +758,7 @@ msgid "dlg.bookmark.icon" msgstr "sitelen:" msgid "heading.prefs" -msgstr "IJO ANTE" +msgstr "ijo ante" # tab button msgid "heading.prefs.colors" @@ -780,7 +780,7 @@ msgid "heading.prefs.network" msgstr "kon" msgid "heading.prefs.paragraph" -msgstr "WAN SITELEN" +msgstr "wan sitelen" msgid "prefs.searchurl" msgstr "nimi tawa lukin e nimi:" @@ -801,19 +801,19 @@ msgid "prefs.hidetoolbarscroll" msgstr "tawa la mi len e ijo sewi:" msgid "heading.prefs.pagecontent" -msgstr "KULE LIPU" +msgstr "kule lipu" msgid "heading.prefs.proxies" -msgstr "LUPA" +msgstr "lupa" msgid "heading.prefs.scrolling" -msgstr "TAWA" +msgstr "tawa" msgid "heading.prefs.sizing" -msgstr "SULI" +msgstr "suli" msgid "heading.prefs.widelayout" -msgstr "SULI MUTE" +msgstr "suli mute" # tab button msgid "heading.prefs.style" @@ -1288,7 +1288,7 @@ msgid "ident.gotohelp" msgstr "sina wile kama sona e sitelen jan pi ilo TLS la o lukin e %ssona%s." msgid "heading.pageinfo" -msgstr "SONA PI LIPU NI" +msgstr "sona pi lipu ni" msgid "pageinfo.header.cached" msgstr "(ijo lon sona pi tenpo pini lili)" @@ -1317,7 +1317,7 @@ msgid "dlg.newident.rsa.selfsign" msgstr "sina ken pali e sitelen len kepeken e sitelen RSA pi tu 2048." msgid "heading.prefs.certs" -msgstr "SITELEN LEN MUTE" +msgstr "sitelen len mute" msgid "error.cert.needed" msgstr "sina wile e sitelen len" @@ -1348,7 +1348,7 @@ msgid "lang.hi" msgstr "toki Insi" msgid "heading.newident" -msgstr "NIMI SIN" +msgstr "nimi sin" msgid "dlg.newident.domain" msgstr "nimi pi ilo sona:" @@ -1357,7 +1357,7 @@ msgid "dlg.newident.country" msgstr "ma:" msgid "heading.feedcfg" -msgstr "IJO ANTE PI LIPU LUKIN" +msgstr "ijo ante pi lipu lukin" msgid "dlg.bookmark.tags" msgstr "nimi lili:" @@ -1448,7 +1448,7 @@ msgid "dlg.save.opendownload" msgstr "o open e ijo" msgid "heading.newident.date.bad" -msgstr "TENPO LI PAKALA" +msgstr "tenpo li pakala" msgid "gempub.cover.aboutbook" msgstr "sona lipu" @@ -1476,7 +1476,7 @@ msgid "lang.ia" msgstr "toki Intelinwa" msgid "heading.newident.missing" -msgstr "NIMI LI LON ALA" +msgstr "nimi li lon ala" msgid "link.side" msgstr "o open e nimi tawa lon poki ante" @@ -1611,7 +1611,7 @@ msgid "dlg.newident.scope" msgstr "o kepeken e ona lon:" msgid "keys.upload" -msgstr "o pana e lipu kepeken kon Tetan" +msgstr "o pana kepeken kon Tetan" msgid "menu.pageinfo" msgstr "o lukin e sona lipu" @@ -1629,7 +1629,7 @@ msgid "upload.port" msgstr "nanpa kon…" msgid "heading.uploadport" -msgstr "NANPA PI KON TETAN" +msgstr "nanpa pi kon Tetan" msgid "dlg.uploadport.set" msgstr "o ante" @@ -1640,13 +1640,13 @@ msgstr "" "wile awen." msgid "heading.prefs.uitheme" -msgstr "KULE PI ILO LAKELAN" +msgstr "kule pi ilo Lakelan" msgid "link.file.delete" msgstr "o weka e ijo" msgid "heading.file.delete" -msgstr "O WEKA E IJO" +msgstr "o weka e ijo" msgid "dlg.file.delete.confirm" msgstr "sina wile ala wile weka e ijo ni?" @@ -1655,10 +1655,10 @@ msgid "dlg.file.delete" msgstr "o weka" msgid "heading.upload" -msgstr "O PANA KEPEKEN KON TETAN" +msgstr "o pana kepeken kon Tetan" msgid "menu.page.upload" -msgstr "o pana e lipu kepeken kon Tetan…" +msgstr "o pana kepeken kon Tetan…" msgid "heading.upload.text" msgstr "lipu" @@ -1768,7 +1768,7 @@ msgid "dlg.upload.pickfile" msgstr "open e ijo" msgid "heading.addfolder" -msgstr "O PALI E POKI" +msgstr "o pali e poki" msgid "dlg.addfolder.defaulttitle" msgstr "poki sin" @@ -1781,7 +1781,7 @@ msgstr "o pali e poki" # used on mobile msgid "heading.settings" -msgstr "IJO ANTE" +msgstr "ijo ante" msgid "prefs.imagestyle" msgstr "o kule e sitelen:" @@ -1806,7 +1806,7 @@ msgid "keys.bookmark.addfolder" msgstr "o pali e poki tawa lipu awen" msgid "heading.bookmark.tags" -msgstr "NIMI LILI NAMAKO" +msgstr "nimi lili namako" msgid "media.untitled.image" msgstr "sitelen" @@ -1834,7 +1834,7 @@ msgid "menu.undo" msgstr "o ante monsi" msgid "heading.confirm.bookmarks.delete" -msgstr "LIPU AWEN LI WILE KAMA WEKA" +msgstr "o weka e lipu awen" #, c-format msgid "dlg.confirm.bookmarks.delete" @@ -1854,7 +1854,7 @@ msgid "dlg.fontpack.classic" msgstr "o kama jo e kulupu sitelen (tu suli meka 25)" msgid "heading.fontpack.classic" -msgstr "O KAMA JO E KULUPU SITELEN" +msgstr "o kama jo e kulupu sitelen" #, c-format msgid "fontpack.install" @@ -1972,7 +1972,7 @@ msgid "fontpack.delete" msgstr "o weka e kulupu sitelen \"%s\"" msgid "heading.fontpack.delete" -msgstr "KULUPU SITELEN LI KAMA WEKA" +msgstr "o weka e kulupu sitelen" #, c-format msgid "dlg.fontpack.delete.confirm" @@ -2068,3 +2068,56 @@ msgstr "poki:" msgid "sidebar.action.show" msgstr "o len ala:" + +msgid "menu.open.external" +msgstr "o open kepeken ilo ante" + +msgid "sidebar.action.history.clear" +msgstr "o weka" + +msgid "keys.upload.edit" +msgstr "o ante e lipu kepeken kon Tetan" + +# Active identity toolbar menu. +msgid "menu.hide.identities" +msgstr "o len e nimi" + +msgid "menu.home" +msgstr "o tawa lipu tomo" + +msgid "menu.identities" +msgstr "o ante e nimi" + +msgid "sidebar.close" +msgstr "awen" + +msgid "sidebar.action.bookmarks.newfolder" +msgstr "poki sin" + +msgid "sidebar.action.bookmarks.edit" +msgstr "o ante" + +# The %s represents the name of an identity. +#, c-format +msgid "ident.switch" +msgstr "o kepeken e nimi %s" + +msgid "menu.share" +msgstr "o pana" + +msgid "menu.page.upload.edit" +msgstr "o ante e lipu kepeken kon Tetan…" + +# Shows where a local file is using the Finder. +msgid "menu.reveal.macos" +msgstr "o lukin kepeken ilo Finder" + +msgid "menu.upload.delete" +msgstr "o weka e ijo ale" + +msgid "menu.upload.delete.confirm" +msgstr "sina wile ala wile weka e ijo ale? sina ken ala ante monsi" + +# Mobile subheading in the Upload dialog. +msgid "upload.url" +msgstr "nimi tawa" -- cgit v1.2.3 From 31914cbfdef8fa9757e148b7676f7ebff2c8e1cf Mon Sep 17 00:00:00 2001 From: Xosé M Date: Thu, 6 Jan 2022 08:31:29 +0000 Subject: Translated using Weblate (Galician) Currently translated at 100.0% (644 of 644 strings) Translation: Lagrange/User Interface Translate-URL: http://weblate.skyjake.fi/projects/lagrange/ui/gl/ --- po/gl.po | 182 ++++++++++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 135 insertions(+), 47 deletions(-) diff --git a/po/gl.po b/po/gl.po index 55ec7aa2..6d9f7e91 100644 --- a/po/gl.po +++ b/po/gl.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" -"PO-Revision-Date: 2021-12-19 06:50+0000\n" +"PO-Revision-Date: 2022-01-08 14:50+0000\n" "Last-Translator: Xosé M. \n" "Language-Team: Galician " "\n" @@ -122,13 +122,13 @@ msgstr "" "• 2021-12-31 23:59:59" msgid "heading.prefs.pagecontent" -msgstr "CORES DA PÁXINA" +msgstr "Cores da páxina" msgid "heading.prefs.sizing" -msgstr "TAMAÑO" +msgstr "Tamaño" msgid "heading.prefs.widelayout" -msgstr "ANCHO DA DISPOSICIÓN" +msgstr "Ancho da disposición" msgid "prefs.collapsepreonload" msgstr "Pregar texto preformatado:" @@ -572,7 +572,7 @@ msgid "history.clear" msgstr "Limpar historial…" msgid "heading.history.clear" -msgstr "LIMPAR HISTORIAL" +msgstr "Limpar historial" msgid "dlg.history.clear" msgstr "Limpar historial" @@ -631,13 +631,13 @@ msgid "ident.export" msgstr "Exportar" msgid "heading.ident.use" -msgstr "USO DA IDENTIDADE" +msgstr "Uso de identidade" msgid "menu.edit.notes" msgstr "Editar notas…" msgid "heading.ident.notes" -msgstr "NOTAS DA IDENTIDADE" +msgstr "Notas da identidade" msgid "ident.fingerprint" msgstr "Copiar impresión dixital" @@ -646,7 +646,7 @@ msgid "ident.delete" msgstr "Eliiminar identidade…" msgid "heading.ident.delete" -msgstr "ELIMINAR IDENTIDADE" +msgstr "Eliminar identidade" msgid "dlg.ident.delete" msgstr "Eliminar Identidade e Ficheiros" @@ -655,7 +655,7 @@ msgid "sidebar.empty.idents" msgstr "Sen identidades" msgid "heading.unsub" -msgstr "RETIRAR SUBSCRICIÓN" +msgstr "Retirar subscrición" #, c-format msgid "dlg.confirm.unsub" @@ -674,7 +674,7 @@ msgid "error.server.msg" msgstr "O servidor respondeu coa mensaxe:" msgid "heading.pageinfo" -msgstr "INFORMACIÓN DA PÁXINA" +msgstr "Información da páxina" msgid "pageinfo.header.cached" msgstr "(contido na caché)" @@ -723,10 +723,10 @@ msgid "dlg.input.send" msgstr "Enviar" msgid "heading.save" -msgstr "FICHEIRO GARDADO" +msgstr "Ficheiro gardado" msgid "heading.save.incomplete" -msgstr "PÁXINA INCOMPLETA" +msgstr "Páxina incompleta" msgid "dlg.save.size" msgstr "Tamaño:" @@ -735,10 +735,10 @@ msgid "dlg.save.opendownload" msgstr "Abrir ficheiro descargado" msgid "heading.save.error" -msgstr "ERRO AO GARDAR O FICHEIRO" +msgstr "Erro ao gardar o ficheiro" msgid "heading.import.bookmarks" -msgstr "IMPORTAR MARCADORES" +msgstr "Importar marcadores" #, c-format msgid "dlg.import.found" @@ -756,7 +756,7 @@ msgid "dlg.import.notnew" msgstr "Xa están marcadas tódalas ligazóns desta páxina." msgid "heading.autoreload" -msgstr "AUTO-RECARGA" +msgstr "Auto-Recarga" msgid "dlg.autoreload" msgstr "Elixe o intervalo para a auto-recarga nesta lapela." @@ -798,13 +798,13 @@ msgid "link.file.delete" msgstr "Eliminar ficheiro" msgid "heading.file.delete" -msgstr "ELIMINAR FICHEIRO" +msgstr "Eliminar ficheiro" msgid "dlg.file.delete" msgstr "Eliminar" msgid "heading.openlink" -msgstr "ABRIR LIGAZÓN" +msgstr "Abrir Ligazón" #, c-format msgid "dlg.openlink.confirm" @@ -827,7 +827,7 @@ msgid "dlg.certwarn.domain.expired" msgstr "O certificado recibido está caducado E era para un dominio incorrecto." msgid "heading.certimport" -msgstr "IMPORTAR IDENTIDADE" +msgstr "Importar identidade" msgid "dlg.certimport.help" msgstr "" @@ -838,10 +838,10 @@ msgid "dlg.certimport.notfound" msgstr "Non se atopa certificado ou chave privada." msgid "heading.certimport.pasted" -msgstr "PEGADO DESDE PORTAPAPEIS" +msgstr "Pegado desde portapapeis" msgid "heading.certimport.dropped" -msgstr "FICHEIRO APORTADO" +msgstr "Ficheiro aportado" msgid "dlg.certimport.import" msgstr "Importar" @@ -859,10 +859,10 @@ msgid "dlg.certimport.nokey" msgstr "Sen chave privada" msgid "link.hint.audio" -msgstr "Reproducir" +msgstr "Audio" msgid "link.hint.image" -msgstr "Ver imaxe" +msgstr "Imaxe" msgid "bookmark.title.blank" msgstr "Páxina baleira" @@ -912,10 +912,10 @@ msgid "heading.lookup.other" msgstr "OUTRO" msgid "menu.page.upload" -msgstr "Subir páxina con Titan…" +msgstr "Subir con Titan…" msgid "heading.upload" -msgstr "SUBIR CON TITAN" +msgstr "Subir con Titan" msgid "heading.upload.text" msgstr "Texto" @@ -951,13 +951,13 @@ msgid "upload.port" msgstr "Porto…" msgid "heading.uploadport" -msgstr "PORTO TITAN PARA SUBIDAS" +msgstr "Porto de subida Titan" msgid "dlg.uploadport.set" msgstr "Establecer porto" msgid "heading.translate" -msgstr "TRADUCIR PÁXINA" +msgstr "Traducir páxina" msgid "dlg.translate.unavail" msgstr "Servizo non dispoñible" @@ -1029,7 +1029,7 @@ msgid "lang.tok" msgstr "Toki Pona" msgid "heading.newident" -msgstr "NOVA IDENTIDADE" +msgstr "Nova Identidade" msgid "dlg.newident.until" msgstr "Válido ata:" @@ -1080,19 +1080,19 @@ msgid "dlg.newident.create" msgstr "Crear identidade" msgid "heading.newident.missing" -msgstr "INFO QUE FALTA" +msgstr "Info que falta" msgid "heading.newident.date.bad" -msgstr "DATA NON VÁLIDA" +msgstr "Data non válida" msgid "dlg.newident.date.past" msgstr "A data de caducidade ten que estar no futuro." msgid "heading.feedcfg" -msgstr "AXUSTES DA FONTE" +msgstr "Axustes da Fonte" msgid "heading.subscribe" -msgstr "SUBSCRIBIRSE A PÁXINA" +msgstr "Subscribirse á Páxina" msgid "dlg.feed.title" msgstr "Título:" @@ -1113,10 +1113,10 @@ msgid "dlg.feed.sub" msgstr "Subscribirse" msgid "heading.bookmark.add" -msgstr "ENGADIR MARCADOR" +msgstr "Engadir Marcador" msgid "heading.bookmark.edit" -msgstr "EDITAR MARCADOR" +msgstr "Editar Marcador" msgid "dlg.bookmark.save" msgstr "Gardar marcador" @@ -1134,10 +1134,10 @@ msgid "dlg.bookmark.icon" msgstr "Icona:" msgid "heading.prefs" -msgstr "PREFERENCIAS" +msgstr "Preferencias" msgid "heading.prefs.certs" -msgstr "CERTIFICADOS" +msgstr "Certificados" # tab button msgid "heading.prefs.colors" @@ -1163,16 +1163,16 @@ msgid "heading.prefs.network" msgstr "Rede" msgid "heading.prefs.paragraph" -msgstr "PARÁGRAFO" +msgstr "Parágrafo" msgid "heading.prefs.uitheme" -msgstr "CORES DA IU" +msgstr "Cores da IU" msgid "heading.prefs.proxies" -msgstr "PROXIES" +msgstr "Proxies" msgid "heading.prefs.scrolling" -msgstr "DESPRAZAMENTO" +msgstr "Desprazamento" # tab button msgid "heading.prefs.style" @@ -1480,7 +1480,7 @@ msgid "keys.tab.close.other" msgstr "Pechar outras lapelas" msgid "keys.upload" -msgstr "Subir páxina con Titan" +msgstr "Subir con Titan" msgid "error.badstatus.msg" msgstr "" @@ -1825,7 +1825,7 @@ msgid "menu.undo" msgstr "Desfacer" msgid "heading.confirm.bookmarks.delete" -msgstr "ELIMINAR MARCADORES" +msgstr "Eliminar marcadores" #, c-format msgid "dlg.confirm.bookmarks.delete" @@ -1840,7 +1840,7 @@ msgid "dlg.upload.id.none" msgstr "Ningunha" msgid "heading.bookmark.tags" -msgstr "ETIQUETAS ESPECIAIS" +msgstr "Etiquetas especiais" msgid "dlg.addfolder.defaulttitle" msgstr "Novo cartafol" @@ -1890,7 +1890,7 @@ msgid "dlg.upload.pickfile" msgstr "Elexir ficheiro" msgid "heading.addfolder" -msgstr "ENGADIR CARTAFOL" +msgstr "Engadir Cartafol" msgid "dlg.addfolder.prompt" msgstr "Escribe o nome do novo cartafol:" @@ -1900,7 +1900,7 @@ msgstr "Engadir cartafol" # used on mobile msgid "heading.settings" -msgstr "AXUSTES" +msgstr "Axustes" msgid "prefs.imagestyle" msgstr "Imaxes coloridas:" @@ -1946,7 +1946,7 @@ msgstr[0] "%u tipografía" msgstr[1] "%u tipografías" msgid "heading.fontpack.classic" -msgstr "DESCARGAR PAQUETE TIPOGRAFÍA" +msgstr "Descargar paquete de tipografía" msgid "dlg.fontpack.classic" msgstr "Descargar Paquete de tipografía (25 MB)" @@ -2084,7 +2084,7 @@ msgid "fontpack.delete" msgstr "Eliminar permanentemente \"%s\"" msgid "heading.fontpack.delete" -msgstr "ELIMINAR TIPOGRAFÍA" +msgstr "Eliminar tipografía" msgid "dlg.fontpack.delete" msgstr "Eliminar tipografía" @@ -2107,7 +2107,7 @@ msgid "truetype.help.installed" msgstr "Esta tipografía está instalada no directorio da usuaria." msgid "heading.dismiss.warning" -msgstr "DESBOTAR AVISO?" +msgstr "Desbotar aviso?" #, c-format msgid "dlg.dismiss.ansi" @@ -2145,3 +2145,91 @@ msgstr "Ler todo" msgid "menu.update" msgstr "Comprobar actualización…" + +msgid "menu.open.external" +msgstr "Abrir noutra app" + +# Active identity toolbar menu. +msgid "menu.hide.identities" +msgstr "Agochar identidades" + +msgid "menu.home" +msgstr "Ir a Inicio" + +msgid "menu.identities" +msgstr "Xestión Identidades" + +msgid "sidebar.close" +msgstr "Feito" + +msgid "sidebar.action.bookmarks.newfolder" +msgstr "Novo cartafol" + +msgid "sidebar.action.bookmarks.edit" +msgstr "Editar" + +msgid "sidebar.action.history.clear" +msgstr "Limpar" + +# The %s represents the name of an identity. +#, c-format +msgid "ident.switch" +msgstr "Usar %s" + +msgid "sidebar.empty.unread" +msgstr "Sen entradas non lidas" + +# Paste the line preceding the clicked link into the input prompt. +msgid "menu.input.precedingline" +msgstr "Pegar a liña anterior" + +# Shows where a local file is using the Finder. +msgid "menu.reveal.macos" +msgstr "Mostrar no buscador" + +msgid "menu.share" +msgstr "Compartir" + +msgid "menu.page.upload.edit" +msgstr "Editar páxina con Titan…" + +msgid "heading.upload.id" +msgstr "Autorización" + +msgid "menu.upload.export" +msgstr "Exportar texto" + +msgid "menu.upload.delete" +msgstr "Eliminar todo" + +msgid "menu.upload.delete.confirm" +msgstr "Quero Eliminar Todo (non ten volta)" + +# Mobile subheading in the Upload dialog. +msgid "upload.url" +msgstr "URL" + +# Mobile subheading: buttons for entering uploaded data. +msgid "upload.content" +msgstr "Contido" + +msgid "hint.upload.path" +msgstr "Ruta URL" + +msgid "hint.upload.token.long" +msgstr "token — mira instruccións no servidor" + +msgid "heading.prefs.toolbaractions" +msgstr "Accións da barra de ferramentas" + +msgid "prefs.toolbaraction1" +msgstr "Botón 1" + +msgid "prefs.toolbaraction2" +msgstr "Botón 2" + +msgid "prefs.blink" +msgstr "Cursor intermitente:" + +msgid "keys.upload.edit" +msgstr "Editar páxina con Titan" -- cgit v1.2.3 From 24fed0953066821cfbe93f6497462401638350f0 Mon Sep 17 00:00:00 2001 From: Alyssa Liddell Date: Thu, 6 Jan 2022 11:10:14 +0000 Subject: Translated using Weblate (Ukrainian) Currently translated at 100.0% (644 of 644 strings) Translation: Lagrange/User Interface Translate-URL: http://weblate.skyjake.fi/projects/lagrange/ui/uk/ --- po/uk.po | 186 ++++++++++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 137 insertions(+), 49 deletions(-) diff --git a/po/uk.po b/po/uk.po index d064f795..9b675221 100644 --- a/po/uk.po +++ b/po/uk.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" -"PO-Revision-Date: 2021-12-23 11:10+0000\n" +"PO-Revision-Date: 2022-01-08 14:50+0000\n" "Last-Translator: Alyssa Liddell \n" "Language-Team: Ukrainian " "\n" @@ -524,13 +524,13 @@ msgid "pageinfo.domain.match" msgstr "Доменне ім'я збігається" msgid "heading.save" -msgstr "ФАЙЛ ЗБЕРЕЖЕНО" +msgstr "Файл Збережено" msgid "heading.save.incomplete" -msgstr "СТОРІНКА НЕПОВНА" +msgstr "Сторінка неповна" msgid "dlg.save.incomplete" -msgstr "ЗАВАНТАЖЕННЯ ЇЇ ВМІСТУ ПРОДОВЖУЄТЬСЯ." +msgstr "Завантаження її вмісту ще триває." #, c-format msgid "dlg.import.found" @@ -556,7 +556,7 @@ msgid "link.browser" msgstr "Відкрити посилання у основному переглядачі" msgid "heading.file.delete" -msgstr "ВИДАЛИТИ ФАЙЛ" +msgstr "Видалити файл" msgid "dlg.file.delete.confirm" msgstr "Справді бажаєте видалити цей файл?" @@ -631,13 +631,13 @@ msgid "history.clear" msgstr "Очистити історію…" msgid "heading.history.clear" -msgstr "ЧИЩЕННЯ ІСТОРІЇ" +msgstr "Чищення історії" msgid "dlg.history.clear" msgstr "Очистити історію" msgid "heading.confirm.bookmarks.delete" -msgstr "ВИДАЛИТИ ЗАКЛАДКИ" +msgstr "Видалення закладок" #, c-format msgid "dlg.bookmarks.delete" @@ -688,10 +688,10 @@ msgid "ident.export" msgstr "Експортувати" msgid "heading.ident.use" -msgstr "ВИКОРИСТАННЯ ІДЕНТИЧНОСТІ" +msgstr "Використання ідентичності" msgid "heading.ident.notes" -msgstr "ПРИМІТКИ ІДЕНТИЧНОСТІ" +msgstr "Примітки про ідентичність" # %s refers to name of an identity. #, c-format @@ -711,7 +711,7 @@ msgid "error.server.msg" msgstr "Сервер повернув таке повідомлення:" msgid "heading.pageinfo" -msgstr "ІНФОРМАЦІЯ ПРО СТОРІНКУ" +msgstr "Інформація Про Сторінку" msgid "pageinfo.cert.ca.verified" msgstr "Підтверджено ЦС" @@ -754,16 +754,16 @@ msgid "dlg.save.opendownload" msgstr "Відкрити завантажений файл" msgid "heading.save.error" -msgstr "ПОМИЛКА ЗБЕРЕЖЕННЯ ФАЙЛУ" +msgstr "Помилка збереження файлу" msgid "heading.import.bookmarks" -msgstr "ІМПОРТ ЗАКЛАДОК" +msgstr "Імпорт закладок" msgid "dlg.import.notnew" msgstr "Усі посилання на сторінці вже додані до закладок." msgid "heading.autoreload" -msgstr "АВТООНОВЛЕННЯ" +msgstr "Автооновлення" msgid "reload.never" msgstr "Ніколи" @@ -813,7 +813,7 @@ msgid "dlg.file.delete" msgstr "Видалити" msgid "heading.openlink" -msgstr "ВІДКРИТИ ПОСИЛАННЯ" +msgstr "Відкрити посилання" #, c-format msgid "dlg.openlink.confirm" @@ -839,16 +839,16 @@ msgid "dlg.certwarn.domain.expired" msgstr "Отриманий сертифікат протермінований ТА належить іншому домену." msgid "heading.certimport" -msgstr "ІМПОРТ ІДЕНТИЧНОСТІ" +msgstr "Імпорт Ідентичності" msgid "dlg.certimport.notfound" msgstr "Не вдалося знайти сертифікат або закритий ключ." msgid "heading.certimport.pasted" -msgstr "ВСТАВЛЕНО З БУФЕРА ОБМІНУ" +msgstr "Вставлено з буфера обміну" msgid "heading.certimport.dropped" -msgstr "ПЕРЕТЯГНУТО ФАЙЛ" +msgstr "Перетягнуто файл" # button in the mobile New Identity dialog msgid "dlg.certimport.pickfile" @@ -870,10 +870,10 @@ msgid "dlg.certimport.nokey" msgstr "Закритий ключ відсутній" msgid "link.hint.audio" -msgstr "Відтворити аудіо" +msgstr "Аудіозапис" msgid "link.hint.image" -msgstr "Показати зображення" +msgstr "Зображення" msgid "bookmark.title.blank" msgstr "Порожня сторінка" @@ -926,7 +926,7 @@ msgid "ident.delete" msgstr "Видалити ідентичність…" msgid "heading.ident.delete" -msgstr "ВИДАЛЕННЯ ІДЕНТИЧНОСТІ" +msgstr "Видалення ідентичності" #, c-format msgid "dlg.confirm.ident.delete" @@ -942,7 +942,7 @@ msgstr "" "сертифікати." msgid "heading.unsub" -msgstr "ВІДПИСКА" +msgstr "Відписка" #, c-format msgid "dlg.confirm.unsub" @@ -997,7 +997,7 @@ msgid "heading.lookup.other" msgstr "ІНШІ" msgid "heading.upload" -msgstr "ПЕРЕДАЧА ЗА ДОПОМОГОЮ TITAN" +msgstr "Передача за допомогою Titan" msgid "upload.id" msgstr "Ідентичність:" @@ -1026,7 +1026,7 @@ msgid "heading.lookup.pagecontent" msgstr "ВМІСТ СТОРІНКИ" msgid "menu.page.upload" -msgstr "Передати сторінку з Titan…" +msgstr "Передати за допомогою Titan…" msgid "hint.upload.text" msgstr "Введіть текст для передачі" @@ -1050,7 +1050,7 @@ msgid "upload.port" msgstr "Порт…" msgid "heading.uploadport" -msgstr "ПОРТ ПЕРЕДАЧІ TITAN" +msgstr "Порт передачі Titan" msgid "dlg.uploadport.set" msgstr "Вказати порт" @@ -1060,7 +1060,7 @@ msgid "dlg.upload.pickfile" msgstr "Вибрати файл" msgid "heading.translate" -msgstr "ПЕРЕКЛАД СТОРІНКИ" +msgstr "Переклад сторінки" msgid "heading.upload.file" msgstr "Файл" @@ -1172,7 +1172,7 @@ msgid "lang.tok" msgstr "Токі-пона" msgid "heading.newident" -msgstr "НОВА ІДЕНТИЧНІСТЬ" +msgstr "Нова ідентичність" msgid "dlg.newident.rsa.selfsign" msgstr "Створення самопідписаного 2048-розрядного RSA сертифікату." @@ -1223,7 +1223,7 @@ msgid "dlg.newident.more" msgstr "Ще…" msgid "heading.newident.missing" -msgstr "ВІДСУТНЯ ІНФОРМАЦІЯ" +msgstr "Відсутня інформація" msgid "dlg.newindent.missing.commonname" msgstr "Потрібно вказати звичайне ім'я." @@ -1236,10 +1236,10 @@ msgstr "" "• 2021-12-31 23:59:59" msgid "heading.feedcfg" -msgstr "НАЛАШТУВАННЯ СТРІЧКИ" +msgstr "Налаштування стічки" msgid "heading.subscribe" -msgstr "ПІДПИСКА НА СТОРІНКУ" +msgstr "Підписка на сторінку" msgid "dlg.feed.title" msgstr "Заголовок:" @@ -1254,7 +1254,7 @@ msgid "dlg.feed.sub" msgstr "Підписатися" msgid "heading.bookmark.add" -msgstr "ДОДАТИ ЗАКЛАДКУ" +msgstr "Додати закладку" msgid "dlg.bookmark.save" msgstr "Збереги закладку" @@ -1269,7 +1269,7 @@ msgid "dlg.newident.create" msgstr "Створити ідентичність" msgid "heading.newident.date.bad" -msgstr "НЕДІЙСНА ДАТА" +msgstr "Недійсна дата" msgid "dlg.newident.date.past" msgstr "Термін дії має закінчуватися у майбутньому." @@ -1281,7 +1281,7 @@ msgid "dlg.feed.type.headings" msgstr "Нові заголовки" msgid "heading.bookmark.edit" -msgstr "РЕДАГУВАННЯ ЗАКЛАДКИ" +msgstr "Редагування закладки" msgid "dlg.bookmark.url" msgstr "URL:" @@ -1293,10 +1293,10 @@ msgid "dlg.bookmark.icon" msgstr "Значок:" msgid "heading.bookmark.tags" -msgstr "ОСОБЛИВІ МІТКИ" +msgstr "Особливі мітки" msgid "heading.addfolder" -msgstr "ДОДАТИ ТЕКУ" +msgstr "Додати теку" msgid "dlg.addfolder.defaulttitle" msgstr "Нова тека" @@ -1305,7 +1305,7 @@ msgid "dlg.addfolder.prompt" msgstr "Назва створюваної теки:" msgid "heading.prefs.certs" -msgstr "СЕРТИФІКАТИ" +msgstr "Сертифікати" # tab button msgid "heading.prefs.colors" @@ -1318,11 +1318,11 @@ msgid "dlg.addfolder" msgstr "Додати теку" msgid "heading.prefs" -msgstr "НАЛАШТУВАННЯ" +msgstr "Налаштування" # used on mobile msgid "heading.settings" -msgstr "НАЛАШТУВАННЯ" +msgstr "Налаштування" # tab button msgid "heading.prefs.general" @@ -1341,22 +1341,22 @@ msgid "heading.prefs.network" msgstr "Мережа" msgid "heading.prefs.paragraph" -msgstr "АБЗАЦ" +msgstr "Абзац" msgid "heading.prefs.uitheme" -msgstr "КОЛЬОРИ ІК" +msgstr "Кольори ІК" msgid "heading.prefs.pagecontent" -msgstr "КОЛЬОРИ СТОРІНКИ" +msgstr "Кольори сторінки" msgid "heading.prefs.proxies" -msgstr "ПРОКСІ" +msgstr "Проксі" msgid "heading.prefs.scrolling" -msgstr "ПРОКРУЧУВАННЯ" +msgstr "Прокручування" msgid "heading.prefs.sizing" -msgstr "РОЗМІРИ" +msgstr "Розміри" # tab button msgid "heading.prefs.style" @@ -1373,7 +1373,7 @@ msgid "prefs.hoverlink" msgstr "Показувати URL при наведенні:" msgid "heading.prefs.widelayout" -msgstr "ШИРОКИЙ МАКЕТ" +msgstr "Широкий макет" msgid "prefs.searchurl" msgstr "URL пошуку:" @@ -1807,7 +1807,7 @@ msgid "keys.split.item" msgstr "Меню розділеного перегляду:" msgid "keys.upload" -msgstr "Передати сторінку з Titan" +msgstr "Передати за допомогою Titan" msgid "error.badstatus" msgstr "Невідомий код стану" @@ -2016,7 +2016,7 @@ msgid "prefs.bookmarks.addbottom" msgstr "Додати закладки у кінець списку:" msgid "prefs.font.ui" -msgstr "ІК:" +msgstr "Інтерфейс Користувача:" msgid "prefs.font.heading" msgstr "Заголовки:" @@ -2103,7 +2103,7 @@ msgid "fontpack.delete" msgstr "Назавжди видалити \"%s\"" msgid "heading.fontpack.delete" -msgstr "ВИДАЛЕННЯ ПАКЕТУ ШРИФТІВ" +msgstr "Видалення пакету шрифтів" msgid "dlg.fontpack.delete" msgstr "Видалити пакет шрифту" @@ -2129,13 +2129,13 @@ msgid "dlg.dismiss.warning" msgstr "Усунути попередження" msgid "heading.dismiss.warning" -msgstr "УСУНУТИ ПОПЕРЕДЖЕННЯ?" +msgstr "Усунути попередження?" msgid "dlg.fontpack.classic" msgstr "Завантажити пакет шрифтів (25 МБ)" msgid "heading.fontpack.classic" -msgstr "ЗАВАНТАЖИТИ ПАКЕТ ШРИФТІВ" +msgstr "Завантажити пакет шрифтів" msgid "dlg.fontpack.classic.msg" msgstr "" @@ -2170,3 +2170,91 @@ msgstr "24-годинний час" # This label should be fairly short so it fits in a button in the sidebar. msgid "sidebar.action.feeds.markallread" msgstr "Прочитати всі" + +msgid "heading.prefs.toolbaractions" +msgstr "Панель інструментів" + +msgid "prefs.toolbaraction1" +msgstr "Кнопка 1" + +msgid "prefs.toolbaraction2" +msgstr "Кнопка 2" + +msgid "prefs.blink" +msgstr "Моргання курсору:" + +msgid "keys.upload.edit" +msgstr "Редагувати сторінку за дпомогою Titan" + +msgid "menu.open.external" +msgstr "Відкрити в іншому додатку" + +msgid "sidebar.action.history.clear" +msgstr "Очистити" + +# Active identity toolbar menu. +msgid "menu.hide.identities" +msgstr "Приховати панель ідентичностей" + +msgid "menu.home" +msgstr "Додому" + +msgid "menu.identities" +msgstr "Керувати ідентичностями" + +msgid "sidebar.close" +msgstr "Готово" + +msgid "sidebar.action.bookmarks.newfolder" +msgstr "Нова тека" + +msgid "sidebar.action.bookmarks.edit" +msgstr "Редагувати" + +# The %s represents the name of an identity. +#, c-format +msgid "ident.switch" +msgstr "Використовувати %s" + +msgid "sidebar.empty.unread" +msgstr "Непрочитані дописи відсутні" + +# Paste the line preceding the clicked link into the input prompt. +msgid "menu.input.precedingline" +msgstr "Вставити попередній рядок" + +# Shows where a local file is using the Finder. +msgid "menu.reveal.macos" +msgstr "Показати в Finder" + +msgid "menu.share" +msgstr "Поділитися" + +msgid "menu.page.upload.edit" +msgstr "Редагувати сторінку за допомогою Titan…" + +msgid "heading.upload.id" +msgstr "Авторизація" + +msgid "menu.upload.export" +msgstr "Експортувати текст" + +msgid "menu.upload.delete" +msgstr "Видалити все" + +msgid "menu.upload.delete.confirm" +msgstr "Справді видалити все (незворотньо)" + +# Mobile subheading in the Upload dialog. +msgid "upload.url" +msgstr "URL" + +# Mobile subheading: buttons for entering uploaded data. +msgid "upload.content" +msgstr "Вміст" + +msgid "hint.upload.path" +msgstr "Шлях URL" + +msgid "hint.upload.token.long" +msgstr "токен — див. вказівки сервера" -- cgit v1.2.3 From 801a982fc2aaeb63a9da5d780d93260354058e6f Mon Sep 17 00:00:00 2001 From: Emir SARI Date: Fri, 7 Jan 2022 07:20:42 +0000 Subject: Translated using Weblate (Turkish) Currently translated at 100.0% (644 of 644 strings) Translation: Lagrange/User Interface Translate-URL: http://weblate.skyjake.fi/projects/lagrange/ui/tr/ --- po/tr.po | 188 ++++++++++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 138 insertions(+), 50 deletions(-) diff --git a/po/tr.po b/po/tr.po index f317eb7d..5cff07d1 100644 --- a/po/tr.po +++ b/po/tr.po @@ -1,8 +1,8 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" -"PO-Revision-Date: 2021-12-27 13:50+0000\n" -"Last-Translator: Emir \n" +"PO-Revision-Date: 2022-01-08 14:50+0000\n" +"Last-Translator: Emir SARI \n" "Language-Team: Turkish \n" "Language: tr\n" "MIME-Version: 1.0\n" @@ -13,7 +13,7 @@ msgstr "" # Link download progress message. msgid "doc.fetching" -msgstr "Getiriliyor" +msgstr "Yükleniyor" msgid "doc.archive.view" msgstr "Arşiv içeriğini görüntüle" @@ -392,7 +392,7 @@ msgid "history.clear" msgstr "Geçmişi temizle…" msgid "heading.history.clear" -msgstr "GEÇMİŞİ TEMİZLE" +msgstr "Geçmişi temizle" msgid "dlg.history.clear" msgstr "Geçmişi temizle" @@ -425,7 +425,7 @@ msgid "bookmarks.reload" msgstr "Uzak konum kaynaklarını yenile" msgid "ident.using" -msgstr "Bu sayfada kullanılıyor" +msgstr "Bu sayfada kullan" msgid "ident.notused" msgstr "Kullanılmıyor" @@ -454,13 +454,13 @@ msgid "ident.export" msgstr "Dışa aktar" msgid "heading.ident.use" -msgstr "KİMLİK KULLANIMI" +msgstr "Kimlik kullanımı" msgid "menu.edit.notes" msgstr "Notları düzenle…" msgid "heading.ident.notes" -msgstr "KİMLİK NOTLARI" +msgstr "Kimlik notlar" msgid "ident.fingerprint" msgstr "Parmak izini kopyala" @@ -469,7 +469,7 @@ msgid "ident.delete" msgstr "Kimliği sil…" msgid "heading.ident.delete" -msgstr "KİMLİĞİ SİL" +msgstr "Kimliği sil" #, c-format msgid "dlg.confirm.ident.delete" @@ -484,7 +484,7 @@ msgid "sidebar.empty.idents" msgstr "Kimlik yok" msgid "heading.unsub" -msgstr "ABONELİKTEN ÇIK" +msgstr "Abonelikten çık" #, c-format msgid "dlg.confirm.unsub" @@ -494,7 +494,7 @@ msgid "dlg.unsub" msgstr "Abonelikten çık" msgid "heading.pageinfo" -msgstr "SAYFA BİLGİSİ" +msgstr "Sayfa bilgisi" msgid "pageinfo.header.cached" msgstr "(önbelleklenmiş içerik)" @@ -534,7 +534,7 @@ msgid "dlg.input.linebreak" msgstr "Satır sonu" msgid "heading.save" -msgstr "DOSYA KAYDEDİLDİ" +msgstr "Dosya kaydedildi" msgid "dlg.save.incomplete" msgstr "Sayfa içeriği hâlâ indiriliyor." @@ -546,7 +546,7 @@ msgid "dlg.save.opendownload" msgstr "İndirilen dosyayı aç" msgid "heading.import.bookmarks" -msgstr "YER İMLERİNİ İÇE AKTAR" +msgstr "Yer imlerini içe aktar" #, c-format msgid "dlg.import.add" @@ -555,7 +555,7 @@ msgstr[0] "%sYer imi ekle" msgstr[1] "%s%d yer imi ekle" msgid "heading.autoreload" -msgstr "OTOMATİK YENİDEN YÜKLEME" +msgstr "Otomatik yeniden yükleme" msgid "reload.never" msgstr "Hiçbir zaman" @@ -597,10 +597,10 @@ msgid "dlg.file.delete" msgstr "Sil" msgid "heading.openlink" -msgstr "BAĞLANTI AÇ" +msgstr "Bağlantı aç" msgid "heading.file.delete" -msgstr "DOSYA SİL" +msgstr "Dosyayı sil" #, c-format msgid "dlg.openlink.confirm" @@ -623,7 +623,7 @@ msgstr "" "sorunu olabilir." msgid "heading.certimport" -msgstr "KİMLİK İÇE AKTAR" +msgstr "Kimlik içe aktar" msgid "dlg.certimport.notfound" msgstr "Bir sertifika veya gizli anahtar bulunamadı." @@ -632,10 +632,10 @@ msgid "dlg.certimport.notfound.page" msgstr "Geçerli sayfada bir sertifika/anahtar bulunamadı." msgid "heading.certimport.pasted" -msgstr "PANODAN YAPIŞTIRILMIŞ" +msgstr "Panodan yapıştırılmış" msgid "heading.certimport.dropped" -msgstr "BIRAKILAN DOSYA" +msgstr "Bırakılan dosya" msgid "dlg.certimport.import" msgstr "İçe aktar" @@ -650,10 +650,10 @@ msgid "dlg.certimport.nokey" msgstr "Gizli anahtar yok" msgid "link.hint.audio" -msgstr "Ses çal" +msgstr "Ses" msgid "link.hint.image" -msgstr "Görsel görüntüle" +msgstr "Görsel" msgid "bookmark.title.blank" msgstr "Boş sayfa" @@ -701,7 +701,7 @@ msgid "heading.lookup.other" msgstr "DİĞER" msgid "heading.upload" -msgstr "TITAN İLE GÖNDER" +msgstr "Titan ile gönder" msgid "upload.id" msgstr "Kimlik:" @@ -737,7 +737,7 @@ msgid "upload.port" msgstr "Kapı…" msgid "heading.uploadport" -msgstr "TITAN GÖNDERME KAPISI" +msgstr "Titan gönderim kapısı" msgid "dlg.uploadport.set" msgstr "Kapı ayarla" @@ -755,7 +755,7 @@ msgid "dlg.upload.pickfile" msgstr "Dosya seç" msgid "heading.translate" -msgstr "SAYFAYI ÇEVİR" +msgstr "Sayfayı çevir" msgid "dlg.translate.fail" msgstr "İstek başarısız oldu" @@ -798,7 +798,7 @@ msgid "lang.ru" msgstr "Rusça" msgid "heading.newident" -msgstr "YENİ KİMLİK" +msgstr "Yeni kimlik" msgid "dlg.newident.until" msgstr "Şu tarihe kadar geçerli:" @@ -837,19 +837,19 @@ msgid "dlg.newident.more" msgstr "Daha fazla…" msgid "heading.newident.missing" -msgstr "EKSİK BİLGİ" +msgstr "Bilgi eksik" msgid "dlg.newindent.missing.commonname" msgstr "Bir \"Ortak ad\" belirtilmelidir." msgid "heading.newident.date.bad" -msgstr "GEÇERSİZ TARİH" +msgstr "Geçersiz tarih" msgid "heading.feedcfg" -msgstr "BESLEME AYARLARI" +msgstr "Besleme ayarları" msgid "heading.subscribe" -msgstr "SAYFAYA ABONE OL" +msgstr "Sayfaya abone ol" msgid "dlg.feed.title" msgstr "Başlık:" @@ -870,10 +870,10 @@ msgid "dlg.feed.sub" msgstr "Abone ol" msgid "heading.bookmark.add" -msgstr "YER İMİ EKLE" +msgstr "Yer imi ekle" msgid "heading.bookmark.edit" -msgstr "YER İMİNİ DÜZENLE" +msgstr "Yer imini düzenle" msgid "dlg.bookmark.title" msgstr "Başlık:" @@ -888,10 +888,10 @@ msgid "dlg.bookmark.icon" msgstr "Simge:" msgid "heading.bookmark.tags" -msgstr "ÖZEL ETİKETLER" +msgstr "Özel etiketler" msgid "heading.addfolder" -msgstr "KLASÖR EKLE" +msgstr "Klasör ekle" msgid "dlg.addfolder.prompt" msgstr "Yeni klasörün adını girin:" @@ -900,14 +900,14 @@ msgid "dlg.addfolder" msgstr "Klasör ekle" msgid "heading.prefs" -msgstr "TERCİHLER" +msgstr "Tercihler" # used on mobile msgid "heading.settings" -msgstr "AYARLAR" +msgstr "Ayarlar" msgid "heading.prefs.certs" -msgstr "SERTİFİKALAR" +msgstr "Sertifikalar" msgid "heading.prefs.fonts" msgstr "Yazıtipleri" @@ -929,19 +929,19 @@ msgid "heading.prefs.network" msgstr "Ağ" msgid "heading.prefs.paragraph" -msgstr "PARAGRAF" +msgstr "Paragraf" msgid "heading.prefs.pagecontent" -msgstr "SAYFA RENKLERİ" +msgstr "Sayfa renkleri" msgid "heading.prefs.proxies" -msgstr "VEKİLLER" +msgstr "Vekiller" msgid "heading.prefs.scrolling" -msgstr "KAYDIRMA" +msgstr "Kaydırma" msgid "heading.prefs.sizing" -msgstr "BOYUTLANDIRMA" +msgstr "Boyutlandırma" # tab button msgid "heading.prefs.style" @@ -1193,7 +1193,7 @@ msgid "keys.split.next" msgstr "Odağı bir sonraki bölüntüye taşı" msgid "keys.upload" -msgstr "Sayfayı Titan ile gönder" +msgstr "Titan ile gönder" msgid "error.openfile.msg" msgstr "" @@ -1412,7 +1412,7 @@ msgid "fontpack.delete" msgstr "\"%s\" ögesini kalıcı olarak sil" msgid "heading.fontpack.delete" -msgstr "YAZITİPİ PAKETİNİ SİL" +msgstr "Yazıtipi paketini sil" msgid "dlg.fontpack.delete" msgstr "Yazıtipi paketini sil" @@ -1435,7 +1435,7 @@ msgid "truetype.help.installed" msgstr "Bu yazıtipi, kullanıcı yazıtipleri dizinine yüklenmiş." msgid "heading.dismiss.warning" -msgstr "UYARIYI ATLA?" +msgstr "Uyarı atlansın mı?" #, c-format msgid "dlg.dismiss.ansi" @@ -1445,7 +1445,7 @@ msgid "dlg.dismiss.warning" msgstr "Uyarıyı atla" msgid "heading.fontpack.classic" -msgstr "YAZITİPİ PAKETİNİ İNDİR" +msgstr "Yazıtipi paketini indir" msgid "dlg.fontpack.classic" msgstr "Yazıtipi paketini indir (25 MB)" @@ -1621,7 +1621,7 @@ msgstr "" "Tüm ziyaret edilen sayfalar geçmişini temizlemek istediğinizden emin misiniz?" msgid "heading.confirm.bookmarks.delete" -msgstr "YER İMLERİNİ SİL" +msgstr "Yer imlerini sil" #, c-format msgid "dlg.confirm.bookmarks.delete" @@ -1669,10 +1669,10 @@ msgid "dlg.input.send" msgstr "Gönder" msgid "heading.save.incomplete" -msgstr "SAYFA TAM DEĞİL" +msgstr "Sayfa tam değil" msgid "heading.save.error" -msgstr "DOSYA KAYDEDİLİRKEN HATA" +msgstr "Dosya kaydedilirken hata" #, c-format msgid "dlg.import.found" @@ -1760,7 +1760,7 @@ msgid "heading.lookup.pagecontent" msgstr "SAYFA İÇERİĞİ" msgid "menu.page.upload" -msgstr "Sayfayı Titan ile gönder…" +msgstr "Titan ile gönder…" msgid "hint.upload.text" msgstr "gönderilecek metni girin" @@ -1837,10 +1837,10 @@ msgid "heading.prefs.colors" msgstr "Renkler" msgid "heading.prefs.uitheme" -msgstr "KULLANICI ARABİRİMİ RENKLERİ" +msgstr "Arabirim renkleri" msgid "heading.prefs.widelayout" -msgstr "GENİŞ DİZİLİM" +msgstr "Geniş dizilim" msgid "prefs.collapsepreonload" msgstr "Önceden biçimlendirilenleri daralt:" @@ -2094,3 +2094,91 @@ msgstr "24 saat biçimi" # This label should be fairly short so it fits in a button in the sidebar. msgid "sidebar.action.feeds.markallread" msgstr "Tümünü oku" + +msgid "menu.home" +msgstr "Ana sayfaya git" + +msgid "menu.identities" +msgstr "Kimlikleri yönet" + +msgid "prefs.toolbaraction1" +msgstr "Düğme 1" + +msgid "prefs.toolbaraction2" +msgstr "Düğme 2" + +msgid "prefs.blink" +msgstr "Yanıp sönen imleç:" + +msgid "sidebar.close" +msgstr "Tamam" + +# The %s represents the name of an identity. +#, c-format +msgid "ident.switch" +msgstr "%s kullan" + +msgid "menu.upload.delete.confirm" +msgstr "Tümünü gerçekten sil (geri alınamaz)" + +msgid "keys.upload.edit" +msgstr "Sayfayı Titan ile düzenle" + +msgid "menu.open.external" +msgstr "Başka bir uygulamada aç" + +# Active identity toolbar menu. +msgid "menu.hide.identities" +msgstr "Kimlikleri gizle" + +msgid "sidebar.action.bookmarks.newfolder" +msgstr "Yeni klasör" + +msgid "sidebar.action.bookmarks.edit" +msgstr "Düzenle" + +msgid "sidebar.action.history.clear" +msgstr "Temizle" + +msgid "sidebar.empty.unread" +msgstr "Okunmamış girdi yok" + +# Paste the line preceding the clicked link into the input prompt. +msgid "menu.input.precedingline" +msgstr "Önceki satırı yapıştır" + +# Shows where a local file is using the Finder. +msgid "menu.reveal.macos" +msgstr "Finder'da göster" + +msgid "menu.share" +msgstr "Paylaş" + +msgid "menu.page.upload.edit" +msgstr "Sayfayı Titan ile düzenle…" + +msgid "heading.upload.id" +msgstr "Kimlik doğrulama" + +msgid "menu.upload.export" +msgstr "Metni dışa aktar" + +msgid "menu.upload.delete" +msgstr "Tümünü sil" + +# Mobile subheading in the Upload dialog. +msgid "upload.url" +msgstr "URL" + +# Mobile subheading: buttons for entering uploaded data. +msgid "upload.content" +msgstr "İçerik" + +msgid "hint.upload.path" +msgstr "URL yolu" + +msgid "hint.upload.token.long" +msgstr "jeton — sunucunun yönergelerine bakın" + +msgid "heading.prefs.toolbaractions" +msgstr "Araç çubuğu eylemleri" -- cgit v1.2.3 From 31602c0f1bca4fd66d792662e1652c588bba8d9e Mon Sep 17 00:00:00 2001 From: Wally Hackenslacker Date: Mon, 10 Jan 2022 02:36:27 +0000 Subject: Translated using Weblate (Spanish) Currently translated at 100.0% (644 of 644 strings) Translation: Lagrange/User Interface Translate-URL: http://weblate.skyjake.fi/projects/lagrange/ui/es/ --- po/es.po | 184 ++++++++++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 136 insertions(+), 48 deletions(-) diff --git a/po/es.po b/po/es.po index d790bf2c..bf2c84de 100644 --- a/po/es.po +++ b/po/es.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" -"PO-Revision-Date: 2021-12-07 04:50+0000\n" +"PO-Revision-Date: 2022-01-11 02:50+0000\n" "Last-Translator: Wally Hackenslacker \n" "Language-Team: Spanish \n" "Language: es\n" @@ -18,13 +18,13 @@ msgid "dlg.newident.rsa.selfsign" msgstr "Creando un certificado RSA autofirmado de 2048 bits." msgid "heading.subscribe" -msgstr "SUSCRIBIR A PÁGINA" +msgstr "Suscribir a Página" msgid "dlg.bookmark.save" msgstr "Guardar Marcador" msgid "heading.prefs.sizing" -msgstr "TAMAÑOS" +msgstr "Tamaños" msgid "prefs.uiscale" msgstr "Factor de escalamiento de la Interfaz:" @@ -135,7 +135,7 @@ msgid "dlg.feed.title" msgstr "Título:" msgid "heading.bookmark.add" -msgstr "AGREGAR MARCADOR" +msgstr "Agregar Marcador" # tab button msgid "heading.prefs.colors" @@ -479,7 +479,7 @@ msgid "history.clear" msgstr "Limpiar Historial…" msgid "heading.history.clear" -msgstr "LIMPIAR HISTORIAL" +msgstr "Limpiar Historial" msgid "dlg.history.clear" msgstr "Limpiar Historial" @@ -535,7 +535,7 @@ msgid "menu.edit.notes" msgstr "Editar Notas…" msgid "heading.ident.notes" -msgstr "NOTAS SOBRE IDENTIDAD" +msgstr "Notas Sobre Identidades" msgid "ident.fingerprint" msgstr "Copiar Huella Digital" @@ -544,7 +544,7 @@ msgid "ident.delete" msgstr "Borrar Identidad…" msgid "heading.ident.delete" -msgstr "BORRAR IDENTIDAD" +msgstr "Borrar Identidad" msgid "dlg.ident.delete" msgstr "Borrar Identidad y Archivos" @@ -559,13 +559,13 @@ msgstr "" "Vea la %sAyuda%s para más información sobre los certificados TLS de cliente." msgid "heading.unsub" -msgstr "ELIMINAR SUSCRIPCIÓN" +msgstr "Eliminar Suscripción" msgid "dlg.unsub" msgstr "Eliminar suscripción" msgid "heading.pageinfo" -msgstr "INFORMACIÓN DE PÁGINA" +msgstr "Información de Página" msgid "pageinfo.header.cached" msgstr "(contenido en caché)" @@ -611,26 +611,26 @@ msgid "dlg.input.send" msgstr "Enviar" msgid "heading.save" -msgstr "ARCHIVO GUARDADO" +msgstr "Archivo Guardado" msgid "heading.save.incomplete" -msgstr "PÁGINA INCOMPLETA" +msgstr "Página Incompleta" msgid "dlg.save.size" msgstr "Tamaño:" msgid "heading.save.error" -msgstr "ERROR GUARDANDO ARCHIVO" +msgstr "Error Guardando Archivo" msgid "heading.import.bookmarks" -msgstr "IMPORTAR MARCADORES" +msgstr "Importar Marcadores" msgid "dlg.import.notnew" msgstr "" "Todos los enlaces de esta página ya se encuentran guardados en marcadores." msgid "heading.autoreload" -msgstr "AUTO-RECARGAR" +msgstr "Auto-Recargar" msgid "link.newtab" msgstr "Abrir Enlace en Nueva Pestaña" @@ -651,7 +651,7 @@ msgid "link.bookmark" msgstr "Guardar Enlace en Marcadores…" msgid "heading.openlink" -msgstr "ABRIR ENLACE" +msgstr "Abrir Enlace" #, c-format msgid "dlg.openlink.confirm" @@ -670,7 +670,7 @@ msgid "bookmark.title.blank" msgstr "Página en Blanco" msgid "heading.translate" -msgstr "TRADUCIR PÁGINA" +msgstr "Traducir Página" msgid "dlg.translate.unavail" msgstr "Servicio no Disponible" @@ -719,7 +719,7 @@ msgid "dlg.certwarn.domain.expired" msgstr "El certificado recibido ha expirado Y es para el dominio incorrecto." msgid "heading.certimport" -msgstr "IMPORTAR IDENTIDAD" +msgstr "Importar Identidad" msgid "dlg.certimport.help" msgstr "" @@ -733,10 +733,10 @@ msgid "dlg.certimport.notfound.page" msgstr "No se encontró un certificado/clave en la página actual." msgid "heading.certimport.pasted" -msgstr "PEGADO DESDE EL PORTAPAPELES" +msgstr "Pegado del Portapapeles" msgid "heading.certimport.dropped" -msgstr "ARCHIVO ARRASTRADO Y SOLTADO" +msgstr "Archivo Soltado" msgid "dlg.certimport.import" msgstr "Importar" @@ -754,7 +754,7 @@ msgid "dlg.certimport.nokey" msgstr "Sin Clave Privada" msgid "link.hint.image" -msgstr "Ver Imagen" +msgstr "Imagen" msgid "keys.subscribe" msgstr "Suscribirse a página" @@ -861,7 +861,7 @@ msgid "ident.temporary" msgstr "Temporal" msgid "heading.ident.use" -msgstr "USO DE IDENTIDADES" +msgstr "Uso de Identidades" # %s refers to name of an identity. #, c-format @@ -911,7 +911,7 @@ msgid "lang.ru" msgstr "Ruso" msgid "heading.newident" -msgstr "NUEVA IDENTIDAD" +msgstr "Nueva Identidad" msgid "dlg.newident.until" msgstr "Válido hasta:" @@ -944,7 +944,7 @@ msgid "dlg.newident.create" msgstr "Crear Identidad" msgid "heading.feedcfg" -msgstr "CONFIGURACIÓN DE SUSCRIPCIONES" +msgstr "Configuración de Suscripciones" msgid "dlg.feed.entrytype" msgstr "Tipo de entrada:" @@ -962,7 +962,7 @@ msgid "dlg.feed.sub" msgstr "Suscribir" msgid "heading.bookmark.edit" -msgstr "EDITAR MARCADOR" +msgstr "Editar Marcador" msgid "dlg.bookmark.title" msgstr "Título:" @@ -977,25 +977,25 @@ msgid "dlg.bookmark.icon" msgstr "Ícono:" msgid "heading.prefs" -msgstr "PREFERENCIAS" +msgstr "Preferencias" msgid "heading.prefs.certs" -msgstr "CERTIFICADOS" +msgstr "Certificados" msgid "heading.prefs.paragraph" -msgstr "PÁRRAFO" +msgstr "Párrafo" msgid "heading.prefs.pagecontent" -msgstr "COLORES DE LA PÁGINA" +msgstr "Colores de la Página" msgid "heading.prefs.proxies" -msgstr "PROXIES" +msgstr "Proxies" msgid "heading.prefs.scrolling" -msgstr "DESPLAZAMIENTO" +msgstr "Desplazamiento" msgid "heading.prefs.widelayout" -msgstr "DISPOSICIÓN AMPLIA" +msgstr "Disposición Amplia" msgid "prefs.smoothscroll" msgstr "Desplazamiento suave:" @@ -1313,7 +1313,7 @@ msgid "prefs.doctheme.name.highcontrast" msgstr "Alto Contraste" msgid "link.hint.audio" -msgstr "Reproducir Audio" +msgstr "Audio" #, c-format msgid "num.minutes" @@ -1358,7 +1358,7 @@ msgstr "En Tema Claro" # Link download progress message. msgid "doc.fetching" -msgstr "Recibiendo" +msgstr "Cargando" # Inline download status message. msgid "media.download.warnclose" @@ -1492,13 +1492,13 @@ msgid "lang.ia" msgstr "Interlingua" msgid "heading.newident.missing" -msgstr "INFORMACIÓN FALTANTE" +msgstr "Información Faltante" msgid "dlg.newindent.missing.commonname" msgstr "Debe especificar un \"nombre común\"." msgid "heading.newident.date.bad" -msgstr "FECHA NO VÁLIDA" +msgstr "Fecha no Válida" msgid "dlg.newident.date.past" msgstr "La fecha de vencimiento debe ser en el futuro." @@ -1661,7 +1661,7 @@ msgid "upload.port" msgstr "Puerto…" msgid "heading.uploadport" -msgstr "PUERTO DE CARGA POR TITAN" +msgstr "Puerto de Carga con Titan" msgid "dlg.uploadport.msg" msgstr "" @@ -1678,7 +1678,7 @@ msgid "prefs.linespacing" msgstr "Espaciado de linea:" msgid "keys.upload" -msgstr "Cargar página con Titan" +msgstr "Cargar con Titan" msgid "error.certexpired" msgstr "Certificado Expirado" @@ -1698,7 +1698,7 @@ msgid "link.file.delete" msgstr "Borrar Archivo" msgid "heading.file.delete" -msgstr "BORRAR ARCHIVO" +msgstr "Borrar Archivo" msgid "dlg.file.delete.confirm" msgstr "Está seguro de que quiere borrar este archivo?" @@ -1707,10 +1707,10 @@ msgid "dlg.file.delete" msgstr "Borrar" msgid "menu.page.upload" -msgstr "Cargar Página con Titan…" +msgstr "Cargar con Titan…" msgid "heading.upload" -msgstr "CARGAR PÁGINA CON TITAN" +msgstr "Cargar con Titan" msgid "heading.upload.text" msgstr "Texto" @@ -1770,7 +1770,7 @@ msgid "menu.openfile" msgstr "Abrir Archivo…" msgid "heading.prefs.uitheme" -msgstr "COLORES DE IU" +msgstr "Colores de la IU" msgid "media.untitled.image" msgstr "Imagen" @@ -1830,7 +1830,7 @@ msgid "menu.newfolder" msgstr "Nueva Carpeta…" msgid "heading.confirm.bookmarks.delete" -msgstr "BORRAR MARCADORES" +msgstr "Borrar Marcadores" #, c-format msgid "dlg.confirm.bookmarks.delete" @@ -1849,7 +1849,7 @@ msgid "dlg.upload.id.none" msgstr "Ninguno" msgid "heading.bookmark.tags" -msgstr "ETIQUETAS ESPECIALES" +msgstr "Etiquetas Especiales" msgid "dlg.addfolder.prompt" msgstr "Nombre de la nueva carpeta:" @@ -1898,7 +1898,7 @@ msgid "dlg.upload.pickfile" msgstr "Escoger Archivo" msgid "heading.addfolder" -msgstr "AGREGAR CARPETA" +msgstr "Agregar Carpeta" msgid "dlg.addfolder.defaulttitle" msgstr "Nueva Carpeta" @@ -1908,7 +1908,7 @@ msgstr "Agregar Carpeta" # used on mobile msgid "heading.settings" -msgstr "CONFIGURACIÓN" +msgstr "Configuración" msgid "prefs.imagestyle" msgstr "Colorear imágenes:" @@ -1966,7 +1966,7 @@ msgid "fontpack.export" msgstr "Ver plantilla fontpack.ini" msgid "heading.fontpack.classic" -msgstr "DESCARGAR PAQUETE DE FUENTES" +msgstr "Descargar Paquete de Fuentes" msgid "dlg.fontpack.classic.msg" msgstr "" @@ -2041,7 +2041,7 @@ msgid "truetype.help.installed" msgstr "Esta fuente está instalada en el directorio de fuentes del usuario." msgid "heading.dismiss.warning" -msgstr "DESCARTAR ADVERTENCIA?" +msgstr "¿Descartar Advertencia?" #, c-format msgid "dlg.dismiss.ansi" @@ -2125,7 +2125,7 @@ msgid "fontpack.delete" msgstr "Borrar \"%s\" permanentemente" msgid "heading.fontpack.delete" -msgstr "BORRAR PAQUETE DE FUENTES" +msgstr "Borrar Paquete de Fuentes" msgid "lang.uk" msgstr "Ucraniano" @@ -2155,3 +2155,91 @@ msgstr "Hora de 24 Horas" # This label should be fairly short so it fits in a button in the sidebar. msgid "sidebar.action.feeds.markallread" msgstr "Leer Todo" + +msgid "menu.open.external" +msgstr "Abrir en Otra App" + +# Active identity toolbar menu. +msgid "menu.hide.identities" +msgstr "Ocultar Identidades" + +msgid "menu.home" +msgstr "Ir a la Página Principal" + +msgid "menu.identities" +msgstr "Gestionar Identidades" + +msgid "sidebar.close" +msgstr "Listo" + +msgid "sidebar.action.bookmarks.newfolder" +msgstr "Nueva Carpeta" + +msgid "sidebar.action.bookmarks.edit" +msgstr "Editar" + +msgid "sidebar.action.history.clear" +msgstr "Limpiar" + +# The %s represents the name of an identity. +#, c-format +msgid "ident.switch" +msgstr "Usar %s" + +msgid "sidebar.empty.unread" +msgstr "Sin Entradas no Leídas" + +# Paste the line preceding the clicked link into the input prompt. +msgid "menu.input.precedingline" +msgstr "Pegar la Linea Precedente" + +# Shows where a local file is using the Finder. +msgid "menu.reveal.macos" +msgstr "Mostrar en el Finder" + +msgid "menu.share" +msgstr "Compartir" + +msgid "menu.page.upload.edit" +msgstr "Editar Página con Titan…" + +msgid "heading.upload.id" +msgstr "Autorización" + +msgid "menu.upload.export" +msgstr "Exportar Texto" + +msgid "menu.upload.delete" +msgstr "Borrar Todo" + +msgid "menu.upload.delete.confirm" +msgstr "De Verdad Borrar Todo (No se Puede Deshacer)" + +# Mobile subheading in the Upload dialog. +msgid "upload.url" +msgstr "URL" + +# Mobile subheading: buttons for entering uploaded data. +msgid "upload.content" +msgstr "Contenido" + +msgid "hint.upload.path" +msgstr "Ruta del URL" + +msgid "hint.upload.token.long" +msgstr "Token — ver instrucciones del servidor" + +msgid "heading.prefs.toolbaractions" +msgstr "Acciones de la Barra de Herramientas" + +msgid "prefs.toolbaraction1" +msgstr "Botón 1" + +msgid "prefs.toolbaraction2" +msgstr "Botón 2" + +msgid "prefs.blink" +msgstr "Cursor parpadeante:" + +msgid "keys.upload.edit" +msgstr "Editar Página con Titan" -- cgit v1.2.3 From 061a5d5e37a145281d668c1d659cafbb62d5f0ec Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 13 Jan 2022 14:10:27 +0200 Subject: Fixed a build error --- src/ui/mobile.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/mobile.h b/src/ui/mobile.h index c19623f9..a719f20b 100644 --- a/src/ui/mobile.h +++ b/src/ui/mobile.h @@ -33,7 +33,7 @@ struct Impl_ToolbarActionSpec { const char *command; }; -const iToolbarActionSpec toolbarActions_Mobile[max_ToolbarAction]; +extern const iToolbarActionSpec toolbarActions_Mobile[max_ToolbarAction]; iDeclareType(Widget) iDeclareType(MenuItem) -- cgit v1.2.3 From 2c4faf01db772b642ccc068383df96ee5633321b Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 13 Jan 2022 14:10:58 +0200 Subject: Showing files using `dbus-send` under Freedesktop D-Bus command line tools can be called to make method calls to reveal a file's location in the file manager. --- po/en.po | 4 ++++ src/app.c | 40 ++++++++++++++++++++++++++++++++-------- src/ui/certlistwidget.c | 3 +++ src/ui/documentwidget.c | 8 ++++++++ src/ui/mediaui.c | 6 ++++++ 5 files changed, 53 insertions(+), 8 deletions(-) diff --git a/po/en.po b/po/en.po index 687c4321..36f5d0c3 100644 --- a/po/en.po +++ b/po/en.po @@ -861,6 +861,10 @@ msgstr "Download Linked File" msgid "menu.reveal.macos" msgstr "Show in Finder" +# Shows where a local file is using the File Manager. +msgid "menu.reveal.filemgr" +msgstr "Show in File Manager" + msgid "menu.share" msgstr "Share" diff --git a/src/app.c b/src/app.c index 0a17a665..65f6fde6 100644 --- a/src/app.c +++ b/src/app.c @@ -3404,28 +3404,52 @@ void openInDefaultBrowser_App(const iString *url) { "xdg-open", cstr_String(url), #elif defined (iPlatformMsys) - concatPath_CStr(cstr_String(execPath_App()), "../urlopen.bat"), - cstr_String(url), + concatPath_CStr(cstr_String(execPath_App()), "../urlopen.bat"), + cstr_String(url), /* TODO: The prompt window is shown momentarily... */ #endif NULL)) ); start_Process(proc); - waitForFinished_Process(proc); /* TODO: test on Windows */ + waitForFinished_Process(proc); iRelease(proc); } +#include + void revealPath_App(const iString *path) { #if defined (iPlatformAppleDesktop) - iProcess *proc = new_Process(); - setArguments_Process( + iProcess *proc = new_Process(); + setArguments_Process( proc, iClob(newStringsCStr_StringList("/usr/bin/open", "-R", cstr_String(path), NULL))); - start_Process(proc); - iRelease(proc); + start_Process(proc); + iRelease(proc); #elif defined (iPlatformAppleMobile) /* Use a share sheet. */ openFileActivityView_iOS(path); #elif defined (iPlatformLinux) || defined (iPlatformHaiku) + iProcess *proc = NULL; + /* Try with `dbus-send` first. */ { + proc = new_Process(); + setArguments_Process( + proc, + iClob(newStringsCStr_StringList( + "/usr/bin/dbus-send", + "--print-reply", + "--dest=org.freedesktop.FileManager1", + "/org/freedesktop/FileManager1", + "org.freedesktop.FileManager1.ShowItems", + format_CStr("array:string:%s", makeFileUrl_CStr(cstr_String(path))), + "string:", + NULL))); + start_Process(proc); + waitForFinished_Process(proc); + const iBool dbusDidSucceed = (exitStatus_Process(proc) == 0); + iRelease(proc); + if (dbusDidSucceed) { + return; + } + } iFileInfo *inf = iClob(new_FileInfo(path)); iRangecc target; if (isDirectory_FileInfo(inf)) { @@ -3434,7 +3458,7 @@ void revealPath_App(const iString *path) { else { target = dirName_Path(path); } - iProcess *proc = new_Process(); + proc = new_Process(); setArguments_Process( proc, iClob(newStringsCStr_StringList("/usr/bin/env", "xdg-open", cstr_Rangecc(target), NULL))); start_Process(proc); diff --git a/src/ui/certlistwidget.c b/src/ui/certlistwidget.c index 5a1c481b..2a7562d8 100644 --- a/src/ui/certlistwidget.c +++ b/src/ui/certlistwidget.c @@ -106,6 +106,9 @@ static void updateContextMenu_CertListWidget_(iCertListWidget *d) { { "${ident.fingerprint}", 0, 0, "ident.fingerprint" }, #if defined (iPlatformAppleDesktop) { magnifyingGlass_Icon " ${menu.reveal.macos}", 0, 0, "ident.reveal" }, +#endif +#if defined (iPlatformLinux) + { magnifyingGlass_Icon " ${menu.reveal.filemgr}", 0, 0, "ident.reveal" }, #endif { export_Icon " ${ident.export}", 0, 0, "ident.export" }, { "---", 0, 0, NULL }, diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 2ea0a4b7..58e6a11e 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -5031,6 +5031,14 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e 0, format_CStr("!reveal url:%s", cstr_String(linkUrl)) }); +#endif +#if defined (iPlatformLinux) + pushBack_Array(&items, + &(iMenuItem){ "${menu.reveal.filemgr}", + 0, + 0, + format_CStr("!reveal url:%s", + cstr_String(linkUrl)) }); #endif } } diff --git a/src/ui/mediaui.c b/src/ui/mediaui.c index ab88c94a..f0070688 100644 --- a/src/ui/mediaui.c +++ b/src/ui/mediaui.c @@ -268,6 +268,12 @@ iBool processEvent_DownloadUI(iDownloadUI *d, const SDL_Event *ev) { 0, 0, format_CStr("!reveal path:%s", cstr_String(path)) }, +#endif +#if defined (iPlatformLinux) + { "${menu.reveal.filemgr}", + 0, + 0, + format_CStr("!reveal path:%s", cstr_String(path)) }, #endif { "---" }, /* Generic items */ -- cgit v1.2.3 From f90b54cefecc2c9e65c283b9a49dd6450ac7122d Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 13 Jan 2022 14:23:56 +0200 Subject: Require the_Foundation v1.1 Calling `dbus-send` requires checking the exit status. --- Depends.cmake | 2 +- lib/the_Foundation | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Depends.cmake b/Depends.cmake index 9e69e69d..b7a6cd25 100644 --- a/Depends.cmake +++ b/Depends.cmake @@ -18,7 +18,7 @@ set (_dependsToBuild) if (NOT EXISTS ${CMAKE_SOURCE_DIR}/lib/the_Foundation/CMakeLists.txt) set (INSTALL_THE_FOUNDATION YES) - find_package (the_Foundation 1.0.1 REQUIRED) + find_package (the_Foundation 1.1.0 REQUIRED) else () if (EXISTS ${CMAKE_SOURCE_DIR}/lib/the_Foundation/.git) # the_Foundation is checked out as a submodule, make sure it's up to date. diff --git a/lib/the_Foundation b/lib/the_Foundation index e2900ed9..453f05f6 160000 --- a/lib/the_Foundation +++ b/lib/the_Foundation @@ -1 +1 @@ -Subproject commit e2900ed99bf8de0768bc091fcbf49d133da167a0 +Subproject commit 453f05f6efb46824ff0dca0174036c7624473e43 -- cgit v1.2.3 From 7251a904f8a37a2e94b459f4aa8f10f71c85fa2f Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 13 Jan 2022 14:24:38 +0200 Subject: Updated language strings --- res/lang/cs.bin | Bin 31844 -> 31885 bytes res/lang/de.bin | Bin 30813 -> 30861 bytes res/lang/en.bin | Bin 26889 -> 26930 bytes res/lang/eo.bin | Bin 25859 -> 25900 bytes res/lang/es.bin | Bin 30642 -> 30749 bytes res/lang/es_MX.bin | Bin 27959 -> 28000 bytes res/lang/fi.bin | Bin 30480 -> 30559 bytes res/lang/fr.bin | Bin 31617 -> 31658 bytes res/lang/gl.bin | Bin 29820 -> 29917 bytes res/lang/hu.bin | Bin 31642 -> 31683 bytes res/lang/ia.bin | Bin 29672 -> 29713 bytes res/lang/ie.bin | Bin 29579 -> 29620 bytes res/lang/isv.bin | Bin 25610 -> 25651 bytes res/lang/nl.bin | Bin 28999 -> 29040 bytes res/lang/pl.bin | Bin 30245 -> 30286 bytes res/lang/ru.bin | Bin 45055 -> 45571 bytes res/lang/sk.bin | Bin 25946 -> 25987 bytes res/lang/sr.bin | Bin 44462 -> 44885 bytes res/lang/tok.bin | Bin 27684 -> 27766 bytes res/lang/tr.bin | Bin 29818 -> 29867 bytes res/lang/uk.bin | Bin 44387 -> 44956 bytes res/lang/zh_Hans.bin | Bin 25854 -> 25895 bytes res/lang/zh_Hant.bin | Bin 26252 -> 26293 bytes 23 files changed, 0 insertions(+), 0 deletions(-) diff --git a/res/lang/cs.bin b/res/lang/cs.bin index abd4a735..1d650fa4 100644 Binary files a/res/lang/cs.bin and b/res/lang/cs.bin differ diff --git a/res/lang/de.bin b/res/lang/de.bin index 62893c64..bf857a70 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 1b26e61e..29608b71 100644 Binary files a/res/lang/en.bin and b/res/lang/en.bin differ diff --git a/res/lang/eo.bin b/res/lang/eo.bin index 318153d3..d1d9dba3 100644 Binary files a/res/lang/eo.bin and b/res/lang/eo.bin differ diff --git a/res/lang/es.bin b/res/lang/es.bin index 71fa7161..a434cab4 100644 Binary files a/res/lang/es.bin and b/res/lang/es.bin differ diff --git a/res/lang/es_MX.bin b/res/lang/es_MX.bin index 3de1f106..fd3f4e1f 100644 Binary files a/res/lang/es_MX.bin and b/res/lang/es_MX.bin differ diff --git a/res/lang/fi.bin b/res/lang/fi.bin index ad009896..1d1a5185 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 e6977fd9..1cb5ddb1 100644 Binary files a/res/lang/fr.bin and b/res/lang/fr.bin differ diff --git a/res/lang/gl.bin b/res/lang/gl.bin index 2dfb1059..ba86c15d 100644 Binary files a/res/lang/gl.bin and b/res/lang/gl.bin differ diff --git a/res/lang/hu.bin b/res/lang/hu.bin index 3ca7ee30..8dba97f9 100644 Binary files a/res/lang/hu.bin and b/res/lang/hu.bin differ diff --git a/res/lang/ia.bin b/res/lang/ia.bin index b2d71185..c3304a24 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 a6d526de..8988972e 100644 Binary files a/res/lang/ie.bin and b/res/lang/ie.bin differ diff --git a/res/lang/isv.bin b/res/lang/isv.bin index adeeb8cd..b14235a4 100644 Binary files a/res/lang/isv.bin and b/res/lang/isv.bin differ diff --git a/res/lang/nl.bin b/res/lang/nl.bin index 6ed111cd..8c40c833 100644 Binary files a/res/lang/nl.bin and b/res/lang/nl.bin differ diff --git a/res/lang/pl.bin b/res/lang/pl.bin index fab560a5..4018a495 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 ed7838bf..2c0ddc73 100644 Binary files a/res/lang/ru.bin and b/res/lang/ru.bin differ diff --git a/res/lang/sk.bin b/res/lang/sk.bin index ec171de2..259529e2 100644 Binary files a/res/lang/sk.bin and b/res/lang/sk.bin differ diff --git a/res/lang/sr.bin b/res/lang/sr.bin index c710e929..ca45035d 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 196cc5be..3a3f1ee0 100644 Binary files a/res/lang/tok.bin and b/res/lang/tok.bin differ diff --git a/res/lang/tr.bin b/res/lang/tr.bin index cf62799a..3ad7e349 100644 Binary files a/res/lang/tr.bin and b/res/lang/tr.bin differ diff --git a/res/lang/uk.bin b/res/lang/uk.bin index bbb5c3cf..7a9cbd28 100644 Binary files a/res/lang/uk.bin and b/res/lang/uk.bin differ diff --git a/res/lang/zh_Hans.bin b/res/lang/zh_Hans.bin index 0a12f0dd..63f4040c 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 2e7c8108..23cfa0e7 100644 Binary files a/res/lang/zh_Hant.bin and b/res/lang/zh_Hant.bin differ -- cgit v1.2.3 From 4939987a151da2ad81e6c2b42876f03c8ad09eb5 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 13 Jan 2022 14:33:32 +0200 Subject: App: Better way to determine directory for temporary files --- src/app.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/app.c b/src/app.c index 65f6fde6..229fde75 100644 --- a/src/app.c +++ b/src/app.c @@ -1109,8 +1109,13 @@ const iString *downloadPathForUrl_App(const iString *url, const iString *mime) { const iString *temporaryPathForUrl_App(const iString *url, const iString *mime) { iApp *d = &app_; - iString *tmpPath = collectNewCStr_String(tmpnam(NULL)); +#if defined (P_tmpdir) + iString * tmpPath = collectNew_String(); + const iRangecc tmpDir = range_CStr(P_tmpdir); +#else + iString * tmpPath = collectNewCStr_String(tmpnam(NULL)); const iRangecc tmpDir = dirName_Path(tmpPath); +#endif set_String( tmpPath, collect_String(concat_Path(collectNewRange_String(tmpDir), fileNameForUrl_App(url, mime)))); -- cgit v1.2.3 From de08efec17e1a0196fc422d720719313331940ba Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 14 Jan 2022 10:54:57 +0200 Subject: Feeds: Leading whitespace in of Atom feeds IssueID #433 --- src/mimehooks.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mimehooks.c b/src/mimehooks.c index c097bd1f..eb379106 100644 --- a/src/mimehooks.c +++ b/src/mimehooks.c @@ -142,7 +142,8 @@ static iBlock *translateAtomXmlToGeminiFeed_(const iString *mime, const iBlock * appendCStr_String(&out, cstr_Lang("feeds.atom.translated")); appendCStr_String(&out, "\n\n"); iRegExp *datePattern = - iClob(new_RegExp("^([0-9][0-9][0-9][0-9]-[0-1][0-9]-[0-3][0-9])(T|\\s).*", caseSensitive_RegExpOption)); + iClob(new_RegExp("^\\s*([0-9][0-9][0-9][0-9]-[0-1][0-9]-[0-3][0-9])(T|\\s).*", + caseSensitive_RegExpOption)); iBeginCollect(); iConstForEach(PtrArray, i, &feed->children) { iEndCollect(); -- cgit v1.2.3 From 5a68ff650ff2457242bb9cbe3f21c781f6e6de0d Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 14 Jan 2022 11:51:49 +0200 Subject: Updated release notes --- res/about/version.gmi | 1 + 1 file changed, 1 insertion(+) diff --git a/res/about/version.gmi b/res/about/version.gmi index 0579cd74..dd9245d1 100644 --- a/res/about/version.gmi +++ b/res/about/version.gmi @@ -40,6 +40,7 @@ Fixes: * Fixed lookup results list becoming too narrow. * Fixed glitches when a widget has multiple simultanous animations. * Fixed mixed-language CJK word wrapping. +* Fixed parsing Atom feed dates with leading whitespace. * macOS: Fixed high CPU usage during audio playback and UI animations. * macOS: Line breaks inside menu items (e.g., info about current identity). -- cgit v1.2.3 From e912bd13b95b3bb93becccfe45cf22f574afce9e Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 14 Jan 2022 12:23:35 +0200 Subject: Cleanup --- src/feeds.c | 4 +++- src/ui/sidebarwidget.c | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/feeds.c b/src/feeds.c index 7fe13617..a8cbf47a 100644 --- a/src/feeds.c +++ b/src/feeds.c @@ -65,7 +65,9 @@ const iString *url_FeedEntry(const iFeedEntry *d) { iBool isUnread_FeedEntry(const iFeedEntry *d) { const size_t fragPos = indexOf_String(&d->url, '#'); if (fragPos != iInvalidPos) { - /* Check if the entry is newer than the latest visit. */ + /* Check if the entry is newer than the latest visit. If the URL has not been visited, + `urlVisitTime_Visited` returns a zero timestamp that is always earlier than + `posted`. */ const iTime visTime = urlVisitTime_Visited(visited_App(), url_FeedEntry(d)); return cmp_Time(&visTime, &d->posted) < 0; } diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index 30e35b7c..ca45f013 100644 --- a/src/ui/sidebarwidget.c +++ b/src/ui/sidebarwidget.c @@ -266,7 +266,7 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct clear_ListWidget(d->list); releaseChildren_Widget(d->blank); if (!keepActions) { - releaseChildren_Widget(d->actions); + releaseChildren_Widget(d->actions); } d->actions->rect.size.y = 0; destroy_Widget(d->menu); -- cgit v1.2.3 From 185783d773f37831fc2666729305e235e7626c33 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 14 Jan 2022 14:20:04 +0200 Subject: DocumentWidget: Fixed possible crash during long downloads Hover can't be updated when the document is hidden. --- src/ui/documentwidget.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 58e6a11e..21ea5aba 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -392,7 +392,8 @@ static iBool isHoverAllowed_DocumentWidget_(const iDocumentWidget *d) { if (!(d->state == ready_RequestState || d->state == receivedPartialResponse_RequestState)) { return iFalse; } - if (d->flags & noHoverWhileScrolling_DocumentWidgetFlag) { + if (d->flags & (noHoverWhileScrolling_DocumentWidgetFlag | + drawDownloadCounter_DocumentWidgetFlag)) { return iFalse; } if (d->flags & pinchZoom_DocumentWidgetFlag) { -- cgit v1.2.3 From dbbbd178f9f603dae20513a9b180fd48070ce159 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 14 Jan 2022 14:47:59 +0200 Subject: Windows: Fixed installing a drag-and-dropped font file The name of the file was not determined correctly. --- src/ui/documentwidget.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 21ea5aba..b73866a8 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -20,7 +20,7 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -/* TODO: Move DocumentView into a source file of its own. Consider cleaning up the network +/* TODO: Move DocumentView into a source file of its own. Consider cleaning up the network request handling. */ #include "documentwidget.h" @@ -1283,7 +1283,7 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { iRect wideRect = { init_I2(origin.x - pad, visPos.y), init_I2(d->docBounds.size.x + 2 * pad, height_Rect(run->visBounds)) }; - adjustEdges_Rect(&wideRect, + adjustEdges_Rect(&wideRect, run->flags & startOfLine_GmRunFlag ? -pad * 3 / 4 : 0, 0, run->flags & endOfLine_GmRunFlag ? pad * 3 / 4 : 0, 0); /* The first line is composed of two runs that may be drawn in either order, so @@ -1397,7 +1397,7 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { range_String(&styled)); setAnsiFlags_Text(oldAnsi); deinit_String(&styled); - goto runDrawn; + goto runDrawn; } } } @@ -2612,7 +2612,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, setRange_String(&d->sourceMime, param); format_String(&str, "# TrueType Font\n"); iString *decUrl = collect_String(urlDecode_String(d->mod.url)); - iRangecc name = baseName_Path(decUrl); + iRangecc name = baseNameSep_Path(decUrl, "/"); iBool isInstalled = iFalse; if (startsWith_String(collect_String(localFilePathFromUrl_String(d->mod.url)), cstr_String(dataDir_App()))) { @@ -3120,7 +3120,7 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) { if (equal_String(&mostRecentUrl_History(d->mod.history)->url, url_GmRequest(mr->req))) { undo_History(d->mod.history); } - setUrl_DocumentWidget_(d, url_GmDocument(d->view.doc)); + setUrl_DocumentWidget_(d, url_GmDocument(d->view.doc)); updateFetchProgress_DocumentWidget_(d); postCommand_Widget(d, "media.updated link:%u request:%p", d->requestLinkId, mr); return; @@ -5041,7 +5041,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e format_CStr("!reveal url:%s", cstr_String(linkUrl)) }); #endif - } + } } else if (!willUseProxy_App(scheme)) { pushBack_Array( -- cgit v1.2.3 From 1c0eff7640bdfca33caa6e24cfabc0fc3b5de137 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Fri, 14 Jan 2022 19:04:01 +0200 Subject: Updated release notes --- res/about/version.gmi | 1 + 1 file changed, 1 insertion(+) diff --git a/res/about/version.gmi b/res/about/version.gmi index dd9245d1..3745a7cb 100644 --- a/res/about/version.gmi +++ b/res/about/version.gmi @@ -41,6 +41,7 @@ Fixes: * Fixed glitches when a widget has multiple simultanous animations. * Fixed mixed-language CJK word wrapping. * Fixed parsing Atom feed dates with leading whitespace. +* Windows: Fixed installing individual TrueType fonts via drag and drop. * macOS: Fixed high CPU usage during audio playback and UI animations. * macOS: Line breaks inside menu items (e.g., info about current identity). -- cgit v1.2.3 From 916b2926c5907ef885743836bd50c3c1ef721f30 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 16 Jan 2022 08:21:04 +0200 Subject: Fixed context menu in input fields The menu was being dismissed immediately. --- src/ui/util.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ui/util.c b/src/ui/util.c index 77d9a63a..8bf31c33 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -705,6 +705,9 @@ static iBool menuHandler_(iWidget *menu, const char *cmd) { closeMenu_Widget(menu); return iTrue; } + if (equal_Command(cmd, "contextclick") && pointer_Command(cmd) == menu) { + return iFalse; + } if (deviceType_App() == phone_AppDeviceType && equal_Command(cmd, "keyboard.changed") && arg_Command(cmd) == 0) { /* May need to reposition the menu. */ -- cgit v1.2.3 From 3a1865d2d14710fda4f97ff83d6147104d9a5ae0 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 16 Jan 2022 08:21:16 +0200 Subject: Cleanup --- src/ui/util.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ui/util.c b/src/ui/util.c index 8bf31c33..bdbbb98e 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -711,8 +711,10 @@ static iBool menuHandler_(iWidget *menu, const char *cmd) { if (deviceType_App() == phone_AppDeviceType && equal_Command(cmd, "keyboard.changed") && arg_Command(cmd) == 0) { /* May need to reposition the menu. */ - menu->rect.pos = windowToLocal_Widget(menu, init_I2(left_Rect(bounds_Widget(menu)), - bottom_Rect(safeRect_Root(menu->root)) - menu->rect.size.y)); + menu->rect.pos = windowToLocal_Widget( + menu, + init_I2(left_Rect(bounds_Widget(menu)), + bottom_Rect(safeRect_Root(menu->root)) - menu->rect.size.y)); return iFalse; } if (!isCommandIgnoredByMenus_(cmd)) { -- cgit v1.2.3 From 88a15ca1069f079d3d584a34a4d03c1d0b22c79a Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 16 Jan 2022 08:36:36 +0200 Subject: Event processing issue that causes menus to be dismissed immediately This seems like a regression in SDL. SDL_PollEvent() doesn't behave like before. As a workaround, wait for events with a very short timeout. IssueID #436 --- src/app.c | 7 +++++++ src/ui/util.c | 5 ++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/app.c b/src/app.c index 229fde75..d4e09d6f 100644 --- a/src/app.c +++ b/src/app.c @@ -1260,7 +1260,14 @@ static iBool nextEvent_App_(iApp *d, enum iAppEventMode eventMode, SDL_Event *ev return SDL_WaitEvent(event); } } + /* SDL regression circa 2.0.18? SDL_PollEvent() doesn't always return + events posted immediately beforehand. Waiting with a very short timeout + seems to work better. */ +#if defined (iPlatformLinux) + return SDL_WaitEventTimeout(event, 1); +#else return SDL_PollEvent(event); +#endif } static iPtrArray *listWindows_App_(const iApp *d, iPtrArray *windows) { diff --git a/src/ui/util.c b/src/ui/util.c index bdbbb98e..0adaebb6 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -705,6 +705,9 @@ static iBool menuHandler_(iWidget *menu, const char *cmd) { closeMenu_Widget(menu); return iTrue; } + if (equal_Command(cmd, "cancel") && pointerLabel_Command(cmd, "menu") == menu) { + return iFalse; + } if (equal_Command(cmd, "contextclick") && pointer_Command(cmd) == menu) { return iFalse; } @@ -1074,7 +1077,7 @@ void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, int menuOpenFlags) { const iBool isPortraitPhone = (isPhone && isPortrait_App()); const iBool isSlidePanel = (flags_Widget(d) & horizontalOffset_WidgetFlag) != 0; if (postCommands) { - postCommand_App("cancel"); /* dismiss any other menus */ + postCommandf_App("cancel menu:%p", d); /* dismiss any other menus */ } /* Menu closes when commands are emitted, so handle any pending ones beforehand. */ processEvents_App(postedEventsOnly_AppEventMode); -- cgit v1.2.3 From 731672c70665c7f24b416818a2137b5ee2e3ee19 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 16 Jan 2022 13:06:50 +0200 Subject: DocumentWidget: Banner glitches Banner warnings were duplicated for example when under some conditions. Internal "about:" pages should never show banners or warnings. IssueID #435 --- src/ui/banner.c | 7 +++++++ src/ui/documentwidget.c | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/src/ui/banner.c b/src/ui/banner.c index e8c2e6d2..11ae1574 100644 --- a/src/ui/banner.c +++ b/src/ui/banner.c @@ -160,6 +160,13 @@ void setSite_Banner(iBanner *d, iRangecc site, iChar icon) { void add_Banner(iBanner *d, enum iBannerType type, enum iGmStatusCode code, const iString *message, const iString *details) { + /* If there already is a matching item, don't add a second one. */ + iConstForEach(Array, i, &d->items) { + const iBannerItem *item = i.value; + if (item->type == type && item->code == code) { + return; + } + } iBannerItem item; init_BannerItem(&item); item.type = type; diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index b73866a8..6a535882 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -2851,6 +2851,12 @@ static void cacheDocumentGlyphs_DocumentWidget_(const iDocumentWidget *d) { } static void addBannerWarnings_DocumentWidget_(iDocumentWidget *d) { + updateBanner_DocumentWidget_(d); + /* Warnings are not shown on internal pages. */ + if (equalCase_Rangecc(urlScheme_String(d->mod.url), "about")) { + clear_Banner(d->banner); + return; + } /* Warnings related to certificates and trust. */ const int certFlags = d->certFlags; const int req = timeVerified_GmCertFlag | domainVerified_GmCertFlag | trusted_GmCertFlag; -- cgit v1.2.3 From 388a762a0f79b75e0c5e5b24ecb8e764b105d50c Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 16 Jan 2022 14:07:46 +0200 Subject: Customizable navbar button actions --- res/about/version.gmi | 3 ++ src/app.c | 17 ++++++++++- src/defs.h | 5 ++-- src/fontpack.c | 2 +- src/prefs.c | 4 +++ src/prefs.h | 3 ++ src/ui/mobile.c | 3 +- src/ui/root.c | 82 +++++++++++++++++++++++++++++++++++++++++---------- src/ui/util.c | 4 +-- 9 files changed, 100 insertions(+), 23 deletions(-) diff --git a/res/about/version.gmi b/res/about/version.gmi index 3745a7cb..d5e701c3 100644 --- a/res/about/version.gmi +++ b/res/about/version.gmi @@ -9,6 +9,7 @@ ## 1.10 New features: * macOS: Trackpad swipe navigation. +* Customizable navbar actions. Right-click on a button to change its action. (Identity and hamburger buttons cannot be changed.) * Identity toolbar menu can be used to switch between alternate identities. If you have used multiple identities on one site, this makes it more convenient to switch between them. * Added "Edit Page with Titan": opens the upload dialog with current page's content prefilled. * Added "Paste Preceding Line" in the input prompt dialog. Potential use cases include user-editable sections on a page and suggested/example input values. @@ -35,6 +36,8 @@ Changes and enhancements: Fixes: * Fixed a history caching issue: if there were multiple instances of the same URL in history, only the latest one's content would be used when navigating back/forward. * Fixed handling of reserved characters in URLs (cf. RFC 3986, section 2.2). +* Fixed the copy/paste context menu not showing in input fields. +* Fixed duplicated warnings showing in the page banner. * Fixed very narrow input fields causing the app to hang. * Fixed initial scroll position in multiline input fields. * Fixed lookup results list becoming too narrow. diff --git a/src/app.c b/src/app.c index d4e09d6f..4045610e 100644 --- a/src/app.c +++ b/src/app.c @@ -248,6 +248,9 @@ static iString *serializePrefs_App_(const iApp *d) { appendFormat_String(str, "linewidth.set arg:%d\n", d->prefs.lineWidth); appendFormat_String(str, "linespacing.set arg:%f\n", d->prefs.lineSpacing); appendFormat_String(str, "returnkey.set arg:%d\n", d->prefs.returnKey); + for (size_t i = 0; i < iElemCount(d->prefs.navbarActions); i++) { + appendFormat_String(str, "navbar.action.set arg:%d button:%d\n", d->prefs.navbarActions[i], i); + } #if defined (iPlatformMobile) appendFormat_String(str, "toolbar.action.set arg:%d button:0\n", d->prefs.toolbarActions[0]); appendFormat_String(str, "toolbar.action.set arg:%d button:1\n", d->prefs.toolbarActions[1]); @@ -961,6 +964,8 @@ static void init_App_(iApp *d, int argc, char **argv) { if (!loadState_App_(d)) { postCommand_Root(NULL, "open url:about:help"); } + postCommand_App("~navbar.actions.changed"); + postCommand_App("~toolbar.actions.changed"); postCommand_Root(NULL, "~window.unfreeze"); postCommand_Root(NULL, "font.reset"); d->autoReloadTimer = SDL_AddTimer(60 * 1000, postAutoReloadCommand_App_, NULL); @@ -2291,10 +2296,20 @@ iBool handleCommand_App(const char *cmd) { } return iTrue; } + else if (equal_Command(cmd, "navbar.action.set")) { + d->prefs.navbarActions[iClamp(argLabel_Command(cmd, "button"), 0, maxNavbarActions_Prefs - 1)] = + iClamp(arg_Command(cmd), 0, max_ToolbarAction - 1); + if (!isFrozen) { + postCommand_App("~navbar.actions.changed"); + } + return iTrue; + } else if (equal_Command(cmd, "toolbar.action.set")) { d->prefs.toolbarActions[iClamp(argLabel_Command(cmd, "button"), 0, 1)] = iClamp(arg_Command(cmd), 0, max_ToolbarAction - 1); - postCommand_App("~toolbar.actions.changed"); + if (!isFrozen) { + postCommand_App("~toolbar.actions.changed"); + } return iTrue; } else if (equal_Command(cmd, "translation.languages")) { diff --git a/src/defs.h b/src/defs.h index d2e13294..e2edd100 100644 --- a/src/defs.h +++ b/src/defs.h @@ -80,6 +80,7 @@ enum iToolbarAction { editPage_ToolbarAction = 10, findText_ToolbarAction = 11, settings_ToolbarAction = 12, + sidebar_ToolbarAction = 13, /* desktop only */ max_ToolbarAction }; @@ -116,7 +117,7 @@ iLocalDef int acceptKeyMod_ReturnKeyBehavior(int behavior) { #define menu_Icon "\U0001d362" #define rightArrowhead_Icon "\u27a4" -#define leftArrowhead_Icon "\u27a4" +#define leftArrowhead_Icon "\u2b9c" #define warning_Icon "\u26a0" #define openLock_Icon "\U0001f513" #define closedLock_Icon "\U0001f512" @@ -124,7 +125,7 @@ iLocalDef int acceptKeyMod_ReturnKeyBehavior(int behavior) { #define reload_Icon "\U0001f503" #define backArrow_Icon "\U0001f870" #define forwardArrow_Icon "\U0001f872" -#define upArrow_Icon "\u2191" +#define upArrow_Icon "\U0001f871" #define upArrowBar_Icon "\u2912" #define downArrowBar_Icon "\u2913" #define rightArrowWhite_Icon "\u21e8" diff --git a/src/fontpack.c b/src/fontpack.c index 79f35526..a440234e 100644 --- a/src/fontpack.c +++ b/src/fontpack.c @@ -818,7 +818,7 @@ const iArray *actions_FontPack(const iFontPack *d, iBool showInstalled) { pushBack_Array( items, &(iMenuItem){ format_Lang(isEnabled ? close_Icon " ${fontpack.disable}" - : leftArrowhead_Icon " ${fontpack.enable}", + : "${fontpack.enable}", fpId), 0, 0, diff --git a/src/prefs.c b/src/prefs.c index 3aa05f01..6164ca25 100644 --- a/src/prefs.c +++ b/src/prefs.c @@ -44,6 +44,10 @@ void init_Prefs(iPrefs *d) { d->uiAnimations = iTrue; d->uiScale = 1.0f; /* default set elsewhere */ d->zoomPercent = 100; + d->navbarActions[0] = back_ToolbarAction; + d->navbarActions[1] = forward_ToolbarAction; + d->navbarActions[2] = sidebar_ToolbarAction; + d->navbarActions[3] = home_ToolbarAction; #if defined (iPlatformAndroidMobile) /* Android has a system-wide back button so no need to have a duplicate. */ d->toolbarActions[0] = closeTab_ToolbarAction; diff --git a/src/prefs.h b/src/prefs.h index 43f7fc0e..ea864f51 100644 --- a/src/prefs.h +++ b/src/prefs.h @@ -101,6 +101,8 @@ enum iPrefsBool { max_PrefsBool }; +#define maxNavbarActions_Prefs 4 + /* TODO: Use a systematic command naming convention for notifications. */ struct Impl_Prefs { @@ -159,6 +161,7 @@ struct Impl_Prefs { enum iColorAccent accent; /* Window and User Interface */ float uiScale; + enum iToolbarAction navbarActions[maxNavbarActions_Prefs]; enum iToolbarAction toolbarActions[2]; /* Document presentation */ int zoomPercent; diff --git a/src/ui/mobile.c b/src/ui/mobile.c index e34cad3a..cf955423 100644 --- a/src/ui/mobile.c +++ b/src/ui/mobile.c @@ -50,7 +50,8 @@ const iToolbarActionSpec toolbarActions_Mobile[max_ToolbarAction] = { { upload_Icon, "${menu.page.upload}", "document.upload" }, { edit_Icon, "${menu.page.upload.edit}", "document.upload copy:1" }, { magnifyingGlass_Icon, "${menu.find}", "focus.set id:find.input" }, - { gear_Icon, "${menu.settings}", "preferences" }, + { gear_Icon, "${menu.settings}", "preferences" }, + { leftHalf_Icon, "${menu.sidebar.left}", "sidebar.toggle" }, }; iBool isUsingPanelLayout_Mobile(void) { diff --git a/src/ui/root.c b/src/ui/root.c index 0a366996..a3c2e0ae 100644 --- a/src/ui/root.c +++ b/src/ui/root.c @@ -596,11 +596,17 @@ static void updateNavBarIdentity_(iWidget *navBar) { } static void updateNavDirButtons_(iWidget *navBar) { - const iHistory *history = history_DocumentWidget(document_App()); - iBool atOldest = atOldest_History(history); - iBool atNewest = atNewest_History(history); - setFlags_Widget(findChild_Widget(navBar, "navbar.back"), disabled_WidgetFlag, atOldest); - setFlags_Widget(findChild_Widget(navBar, "navbar.forward"), disabled_WidgetFlag, atNewest); + iBeginCollect(); + const iHistory *history = history_DocumentWidget(document_App()); + const iBool atOldest = atOldest_History(history); + const iBool atNewest = atNewest_History(history); + /* Reset button state. */ + for (size_t i = 0; i < maxNavbarActions_Prefs; i++) { + const char *id = format_CStr("navbar.action%d", i + 1); + setFlags_Widget(findChild_Widget(navBar, id), disabled_WidgetFlag, iFalse); + } + setFlags_Widget(as_Widget(findMenuItem_Widget(navBar, "navigate.back")), disabled_WidgetFlag, atOldest); + setFlags_Widget(as_Widget(findMenuItem_Widget(navBar, "navigate.forward")), disabled_WidgetFlag, atNewest); iWidget *toolBar = findWidget_App("toolbar"); if (toolBar) { /* Reset the state. */ @@ -618,6 +624,7 @@ static void updateNavDirButtons_(iWidget *navBar) { setOutline_LabelWidget(fwd, atNewest); refresh_Widget(toolBar); } + iEndCollect(); } static const char *loadAnimationCStr_(void) { @@ -777,7 +784,7 @@ static void updateNavBarSize_(iWidget *navBar) { /* Button sizing. */ if (isNarrow ^ ((flags_Widget(navBar) & tight_WidgetFlag) != 0)) { setFlags_Widget(navBar, tight_WidgetFlag, isNarrow); - showCollapsed_Widget(findChild_Widget(navBar, "navbar.sidebar"), !isNarrow); + showCollapsed_Widget(findChild_Widget(navBar, "navbar.action3"), !isNarrow); iObjectList *lists[] = { children_Widget(navBar), children_Widget(findChild_Widget(navBar, "url")), @@ -798,8 +805,8 @@ static void updateNavBarSize_(iWidget *navBar) { updateUrlInputContentPadding_(navBar); } if (isPhone) { - static const char *buttons[] = { "navbar.back", "navbar.forward", "navbar.sidebar", - "navbar.ident", "navbar.home", "navbar.menu" }; + static const char *buttons[] = { "navbar.action1", "navbar.action2", "navbar.action3", + "navbar.action4", "navbar.ident", "navbar.menu" }; iWidget *toolBar = findWidget_Root("toolbar"); setVisualOffset_Widget(toolBar, 0, 0, 0); setFlags_Widget(toolBar, hidden_WidgetFlag, isLandscape_App()); @@ -824,6 +831,22 @@ static void updateNavBarSize_(iWidget *navBar) { postCommand_Widget(navBar, "layout.changed id:navbar"); } +static void updateNavBarActions_(iWidget *navBar) { + const iPrefs *prefs = prefs_App(); + for (size_t i = 0; i < iElemCount(prefs->navbarActions); i++) { + iBeginCollect(); + const int action = prefs->navbarActions[i]; + iLabelWidget *button = + findChild_Widget(navBar, format_CStr("navbar.action%d", i + 1)); + if (button) { + setFlags_Widget(as_Widget(button), disabled_WidgetFlag, iFalse); + updateTextCStr_LabelWidget(button, toolbarActions_Mobile[action].icon); + setCommand_LabelWidget(button, collectNewCStr_String(toolbarActions_Mobile[action].command)); + } + iEndCollect(); + } +} + static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) { if (equal_Command(cmd, "window.resized") || equal_Command(cmd, "metrics.changed")) { updateNavBarSize_(navBar); @@ -836,6 +859,33 @@ static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) { } return iFalse; } + else if (equal_Command(cmd, "navbar.actions.changed")) { + updateNavBarActions_(navBar); + return iTrue; + } + else if (equal_Command(cmd, "contextclick")) { + const iRangecc id = range_Command(cmd, "id"); + if (id.start && startsWith_CStr(id.start, "navbar.action")) { + const int buttonIndex = id.end[-1] - '1'; + iArray items; + init_Array(&items, sizeof(iMenuItem)); + for (size_t i = 0; i < max_ToolbarAction; i++) { + pushBack_Array( + &items, + &(iMenuItem){ + format_CStr( + "%s %s", toolbarActions_Mobile[i].icon, toolbarActions_Mobile[i].label), + 0, + 0, + format_CStr("navbar.action.set arg:%d button:%d", i, buttonIndex) }); + } + openMenu_Widget( + makeMenu_Widget(get_Root()->widget, constData_Array(&items), size_Array(&items)), + coord_Command(cmd)); + deinit_Array(&items); + } + return iFalse; + } else if (equal_Command(cmd, "navigate.focus")) { /* The upload dialog has its own path field. */ if (findWidget_App("upload")) { @@ -1068,9 +1118,7 @@ static iBool handleSearchBarCommands_(iWidget *searchBar, const char *cmd) { static void updateToolBarActions_(iWidget *toolBar) { const iPrefs *prefs = prefs_App(); for (int i = 0; i < 2; i++) { - int action = prefs->toolbarActions[i] - ? prefs->toolbarActions[i] - : (i == 0 ? back_ToolbarAction : forward_ToolbarAction); + const int action = prefs->toolbarActions[i]; iLabelWidget *button = findChild_Widget(toolBar, i == 0 ? "toolbar.action1" : "toolbar.action2"); if (button) { @@ -1260,14 +1308,14 @@ void createUserInterface_Root(iRoot *d) { addUnsplitButton_(navBar); #endif iWidget *navBack; - setId_Widget(navBack = addChildFlags_Widget(navBar, iClob(newIcon_LabelWidget(backArrow_Icon, 0, 0, "navigate.back")), collapse_WidgetFlag), "navbar.back"); - setId_Widget(addChildFlags_Widget(navBar, iClob(newIcon_LabelWidget(forwardArrow_Icon, 0, 0, "navigate.forward")), collapse_WidgetFlag), "navbar.forward"); + setId_Widget(navBack = addChildFlags_Widget(navBar, iClob(newIcon_LabelWidget(backArrow_Icon, 0, 0, "navigate.back")), collapse_WidgetFlag), "navbar.action1"); + setId_Widget(addChildFlags_Widget(navBar, iClob(newIcon_LabelWidget(forwardArrow_Icon, 0, 0, "navigate.forward")), collapse_WidgetFlag), "navbar.action2"); /* Button for toggling the left sidebar. */ setId_Widget(addChildFlags_Widget( navBar, iClob(newIcon_LabelWidget(leftHalf_Icon, 0, 0, "sidebar.toggle")), collapse_WidgetFlag), - "navbar.sidebar"); + "navbar.action3"); addChildFlags_Widget(navBar, iClob(new_Widget()), expand_WidgetFlag); iInputWidget *url; /* URL input field. */ { @@ -1421,9 +1469,9 @@ void createUserInterface_Root(iRoot *d) { addChildFlags_Widget(navBar, iClob(new_Widget()), expand_WidgetFlag); setId_Widget(addChildFlags_Widget(navBar, iClob(newIcon_LabelWidget( - home_Icon, SDLK_h, KMOD_PRIMARY | KMOD_SHIFT, "navigate.home")), + home_Icon, 0, 0, "navigate.home")), collapse_WidgetFlag), - "navbar.home"); + "navbar.action4"); #if defined (iPlatformMobile) const iBool isPhone = (deviceType_App() == phone_AppDeviceType); #endif @@ -1583,6 +1631,7 @@ void createUserInterface_Root(iRoot *d) { setId_Widget(menu, "toolbar.menu"); /* view menu */ } #endif + updateNavBarActions_(navBar); updatePadding_Root(d); /* Global context menus. */ { iWidget *tabsMenu = makeMenu_Widget( @@ -1651,6 +1700,7 @@ void createUserInterface_Root(iRoot *d) { setId_Widget(splitMenu, "splitmenu"); } /* Global keyboard shortcuts. */ { + addAction_Widget(root, SDLK_h, KMOD_PRIMARY | KMOD_SHIFT, "navigate.home"); addAction_Widget(root, 'l', KMOD_PRIMARY, "navigate.focus"); addAction_Widget(root, 'f', KMOD_PRIMARY, "focus.set id:find.input"); addAction_Widget(root, '1', KMOD_PRIMARY, "sidebar.mode arg:0 toggle:1"); diff --git a/src/ui/util.c b/src/ui/util.c index 0adaebb6..41e645de 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -2404,10 +2404,10 @@ iWidget *makePreferences_Widget(void) { format_CStr("returnkey.set arg:%d", acceptWithPrimaryMod_ReturnKeyBehavior) }, { NULL } }; - iMenuItem toolbarActionItems[2][max_ToolbarAction + 1]; + iMenuItem toolbarActionItems[2][max_ToolbarAction]; iZap(toolbarActionItems); for (int j = 0; j < 2; j++) { - for (int i = 0; i < max_ToolbarAction; i++) { + for (int i = 0; i < sidebar_ToolbarAction; i++) { toolbarActionItems[j][i].label = toolbarActions_Mobile[i].label; toolbarActionItems[j][i].command = format_CStr("toolbar.action.set arg:%d button:%d", i, j); -- cgit v1.2.3 From 60a13ff9e76d652fcac44b951217554da4220df1 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 16 Jan 2022 14:20:53 +0200 Subject: SidebarWidget: Blank tabs cleanup Contents of the empty sidebar (e.g., first launch) were a bit messy, with incorrect paddings and minor layout issues. --- res/about/version.gmi | 1 + src/ui/sidebarwidget.c | 72 ++++++++++++++++++++++++++++---------------------- 2 files changed, 42 insertions(+), 31 deletions(-) diff --git a/res/about/version.gmi b/res/about/version.gmi index d5e701c3..9a8f5a21 100644 --- a/res/about/version.gmi +++ b/res/about/version.gmi @@ -40,6 +40,7 @@ Fixes: * Fixed duplicated warnings showing in the page banner. * Fixed very narrow input fields causing the app to hang. * Fixed initial scroll position in multiline input fields. +* Fixed layout issues in the sidebar on empty Feeds and Identities tabs. * Fixed lookup results list becoming too narrow. * Fixed glitches when a widget has multiple simultanous animations. * Fixed mixed-language CJK word wrapping. diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index ca45f013..f5beb785 100644 --- a/src/ui/sidebarwidget.c +++ b/src/ui/sidebarwidget.c @@ -350,37 +350,47 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct } /* Actions. */ if (!isMobile) { - if (!keepActions) { + if (!keepActions && !isEmpty) { addActionButton_SidebarWidget_(d, - check_Icon " ${sidebar.action.feeds.markallread}", + check_Icon + " ${sidebar.action.feeds.markallread}", "feeds.markallread", expand_WidgetFlag | tight_WidgetFlag); - updateSize_LabelWidget(addChildFlags_Widget(d->actions, - iClob(new_LabelWidget("${sidebar.action.show}", NULL)), - frameless_WidgetFlag | tight_WidgetFlag)); - const iMenuItem items[] = { - { page_Icon " ${sidebar.action.feeds.showall}", SDLK_u, KMOD_SHIFT, "feeds.mode arg:0" }, - { circle_Icon " ${sidebar.action.feeds.showunread}", SDLK_u, 0, "feeds.mode arg:1" }, - }; - iWidget *dropButton = addChild_Widget( - d->actions, - iClob(makeMenuButton_LabelWidget(items[d->feedsMode].label, items, 2))); - setId_Widget(dropButton, "feeds.modebutton"); - checkIcon_LabelWidget((iLabelWidget *) dropButton); - setFixedSize_Widget( - dropButton, - init_I2(iMaxi(20 * gap_UI, - measure_Text(default_FontId, - translateCStr_Lang( - items[findWidestLabel_MenuItem(items, 2)].label)) - .advance.x + - 13 * gap_UI), + updateSize_LabelWidget( + addChildFlags_Widget(d->actions, + iClob(new_LabelWidget("${sidebar.action.show}", NULL)), + frameless_WidgetFlag | tight_WidgetFlag)); + const iMenuItem items[] = { + { page_Icon " ${sidebar.action.feeds.showall}", + SDLK_u, + KMOD_SHIFT, + "feeds.mode arg:0" }, + { circle_Icon " ${sidebar.action.feeds.showunread}", + SDLK_u, + 0, + "feeds.mode arg:1" }, + }; + iWidget *dropButton = addChild_Widget( + d->actions, + iClob(makeMenuButton_LabelWidget(items[d->feedsMode].label, items, 2))); + setId_Widget(dropButton, "feeds.modebutton"); + checkIcon_LabelWidget((iLabelWidget *) dropButton); + setFixedSize_Widget( + dropButton, + init_I2( + iMaxi(20 * gap_UI, + measure_Text(default_FontId, + translateCStr_Lang( + items[findWidestLabel_MenuItem(items, 2)].label)) + .advance.x + + 13 * gap_UI), -1)); - } - else { - updateDropdownSelection_LabelWidget(findChild_Widget(d->actions, "feeds.modebutton"), - format_CStr(" arg:%d", d->feedsMode)); - } + } + else { + updateDropdownSelection_LabelWidget( + findChild_Widget(d->actions, "feeds.modebutton"), + format_CStr(" arg:%d", d->feedsMode)); + } } else { if (!keepActions) { @@ -600,12 +610,12 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct if (isEmpty) { if (d->mode == feeds_SidebarMode) { iWidget *div = makeVDiv_Widget(); - //setPadding_Widget(div, 3 * gap_UI, 0, 3 * gap_UI, 2 * gap_UI); + setPadding_Widget(div, 3 * gap_UI, 0, 3 * gap_UI, 2 * gap_UI); arrange_Widget(d->actions); - setPadding_Widget(div, 0, 0, 0, height_Widget(d->actions)); +// setPadding_Widget(div, 0, 0, 0, height_Widget(d->actions)); addChildFlags_Widget(div, iClob(new_Widget()), expand_WidgetFlag); /* pad */ if (d->feedsMode == all_FeedsMode) { - addChild_Widget(div, iClob(new_LabelWidget("${menu.feeds.refresh}", "feeds.refresh"))); + addChild_Widget(div, iClob(new_LabelWidget("${menu.feeds.refresh}", "feeds.refresh"))); } else { iLabelWidget *msg = addChildFlags_Widget(div, iClob(new_LabelWidget("${sidebar.empty.unread}", NULL)), @@ -640,7 +650,7 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct setWrap_LabelWidget(linkLabel, iTrue); addChild_Widget(d->blank, iClob(div)); } -// arrange_Widget(d->blank); + arrange_Widget(d->blank); } #if 0 if (deviceType_App() != desktop_AppDeviceType) { -- cgit v1.2.3 From a5bd6f28363b565292d84618021a231d8c8686ed Mon Sep 17 00:00:00 2001 From: Страхиња Радић Date: Fri, 14 Jan 2022 09:44:52 +0000 Subject: Translated using Weblate (Serbian) Currently translated at 100.0% (644 of 644 strings) Translation: Lagrange/User Interface Translate-URL: http://weblate.skyjake.fi/projects/lagrange/ui/sr/ --- po/sr.po | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/po/sr.po b/po/sr.po index 4fdd4ee0..e9b31570 100644 --- a/po/sr.po +++ b/po/sr.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" -"PO-Revision-Date: 2022-01-08 14:50+0000\n" +"PO-Revision-Date: 2022-01-16 09:50+0000\n" "Last-Translator: Страхиња Радић \n" "Language-Team: Serbian \n" "Language: sr\n" @@ -2123,7 +2123,7 @@ msgstr "" msgid "error.glyphs.msg" msgstr "" -"Ова страница се не може потпуно приказати јер недостају неки знакови. Можете " +"Ова страница се не може потпуно приказати јер недостају неки знаци. Можете " "инсталирати додатне фонтове да бисте то исправили." # Action label -- cgit v1.2.3 From 8a5f1d96d1d803a87de566577bb300c2a62ed2be Mon Sep 17 00:00:00 2001 From: Tadeáš Erban Date: Fri, 14 Jan 2022 19:50:14 +0000 Subject: Translated using Weblate (Czech) Currently translated at 100.0% (644 of 644 strings) Translation: Lagrange/User Interface Translate-URL: http://weblate.skyjake.fi/projects/lagrange/ui/cs/ --- po/cs.po | 200 +++++++++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 144 insertions(+), 56 deletions(-) diff --git a/po/cs.po b/po/cs.po index 797eeac3..034286fa 100644 --- a/po/cs.po +++ b/po/cs.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" -"PO-Revision-Date: 2021-12-01 16:50+0000\n" +"PO-Revision-Date: 2022-01-16 09:50+0000\n" "Last-Translator: Tadeáš Erban \n" "Language-Team: Czech \n" "Language: cs\n" @@ -13,7 +13,7 @@ msgstr "" # Link download progress message. msgid "doc.fetching" -msgstr "Stahování" +msgstr "Načítání" #, c-format msgid "doc.archive" @@ -504,13 +504,13 @@ msgid "menu.copyurl" msgstr "Kopírovat adresu URL" msgid "heading.history.clear" -msgstr "VYMAZAT HISTORII" +msgstr "Vymazat historii" msgid "dlg.history.clear" msgstr "Vymazat historii" msgid "heading.confirm.bookmarks.delete" -msgstr "ODSTRANIT ZÁLOŽKY" +msgstr "Odstranit záložky" #, c-format msgid "dlg.bookmarks.delete" @@ -640,7 +640,7 @@ msgid "dlg.input.send" msgstr "Odeslat" msgid "heading.save" -msgstr "SOUBOR ULOŽEN" +msgstr "Soubor uložen" msgid "dlg.save.opendownload" msgstr "Otevřít stažený soubor" @@ -728,19 +728,19 @@ msgid "dlg.save.incomplete" msgstr "Obsah této stránky se ještě stahuje." msgid "heading.autoreload" -msgstr "AUTOMATICKÉ OBNOVOVÁNÍ" +msgstr "Automatické obnovování" msgid "dlg.autoreload" msgstr "Vyberte interval automatických aktualizací pro tuto kartu." msgid "heading.file.delete" -msgstr "ODSTRANIT SOUBOR" +msgstr "Odstranit soubor" msgid "dlg.certwarn.different" msgstr "Přijatý certifikát je platný, ale je jiný než ten, kterému důvěřujeme." msgid "link.hint.image" -msgstr "Zobrazit obrázek" +msgstr "Obrázek" msgid "bookmark.export.format.sub" msgstr "" @@ -787,7 +787,7 @@ msgid "lang.es.mx" msgstr "Španělština (Mexická)" msgid "heading.newident" -msgstr "NOVÁ TOTOŽNOST" +msgstr "Nová totožnost" msgid "lang.uk" msgstr "Ukrajinština" @@ -826,19 +826,19 @@ msgid "ident.export" msgstr "Exportovat" msgid "heading.ident.use" -msgstr "POUŽITÍ TOTOŽNOSTI" +msgstr "Použití totožnosti" msgid "menu.edit.notes" msgstr "Upravit poznámky…" msgid "heading.ident.notes" -msgstr "POZNÁMKY TOTOŽNOSTI" +msgstr "Poznámky k totožnosti" msgid "ident.fingerprint" msgstr "Zkopírovat otisk" msgid "heading.unsub" -msgstr "UKONČIT ODBĚR" +msgstr "Ukončit odběr" #, c-format msgid "dlg.confirm.unsub" @@ -857,7 +857,7 @@ msgid "error.server.msg" msgstr "Server odpověděl s následující zprávou:" msgid "heading.pageinfo" -msgstr "INFORMACE O STRÁNCE" +msgstr "Informace o stránce" msgid "pageinfo.cert.status" msgstr "Stav certifikátu:" @@ -869,7 +869,7 @@ msgid "ident.delete" msgstr "Odstranit totožnost…" msgid "heading.ident.delete" -msgstr "ODSTRANIT TOTOŽNOST" +msgstr "Odstranit totožnost" msgid "dlg.ident.delete" msgstr "Odstranit totožnost a její soubory" @@ -903,16 +903,16 @@ msgid "dlg.input.linebreak" msgstr "Zalomení řádku" msgid "heading.save.incomplete" -msgstr "STRÁNKA NEÚPLNÁ" +msgstr "Stránka neúplná" msgid "dlg.save.size" msgstr "Velikost:" msgid "heading.save.error" -msgstr "CHYBA PŘI UKLÁDÁNÍ SOUBORU" +msgstr "Chyba při ukládání souboru" msgid "heading.import.bookmarks" -msgstr "NAHRÁT ZÁLOŽKY" +msgstr "Nahrát záložky" #, c-format msgid "dlg.import.add" @@ -964,7 +964,7 @@ msgid "link.browser" msgstr "Otevřít odkaz ve výchozím prohlížeči" msgid "link.noproxy" -msgstr "Otevřít bez použití proxy" +msgstr "Otevřít bez použití prostředníka" msgid "link.copy" msgstr "Zkopírovat odkaz" @@ -979,7 +979,7 @@ msgid "dlg.file.delete" msgstr "Odstranit" msgid "heading.openlink" -msgstr "OTEVŘÍT ODKAZ" +msgstr "Otevřít odkaz" #, c-format msgid "dlg.openlink.confirm" @@ -1014,16 +1014,16 @@ msgid "dlg.certimport.notfound" msgstr "Žádný certifikát nebo soukromý klíč nebyl nalezen." msgid "heading.certimport.pasted" -msgstr "VLOŽENO ZE SCHRÁNKY" +msgstr "Vloženo ze schránky" msgid "heading.certimport.dropped" -msgstr "PŘETÁHNUTÝ SOUBOR" +msgstr "Přetáhnutý soubor" msgid "dlg.certimport.import" msgstr "Nahrát" msgid "heading.certimport" -msgstr "NAHRÁT TOTOŽNOST" +msgstr "Nahrát totožnost" msgid "dlg.certimport.notes" msgstr "Poznámky:" @@ -1038,7 +1038,7 @@ msgid "dlg.certimport.nokey" msgstr "Žádný soukromý klíč" msgid "link.hint.audio" -msgstr "Přehrát zvuk" +msgstr "Zvuk" msgid "bookmark.title.blank" msgstr "Prázdná stránka" @@ -1088,10 +1088,10 @@ msgid "hint.upload.text" msgstr "vložte text k odeslání" msgid "menu.page.upload" -msgstr "Nahrát stránku na server pomocí Titanu…" +msgstr "Nahrát na server pomocí Titanu…" msgid "heading.upload" -msgstr "NAHRÁT NA SERVER POMOCÍ TITANU" +msgstr "Nahrát pomocí titanu" msgid "upload.file.name" msgstr "Jméno souboru:" @@ -1127,7 +1127,7 @@ msgid "dlg.upload.pickfile" msgstr "Vybrat soubor" msgid "heading.translate" -msgstr "PŘELOŽIT STRÁNKU" +msgstr "Přeložit stránku" msgid "dlg.translate.unavail" msgstr "Služba nedostupná" @@ -1190,23 +1190,23 @@ msgid "lang.sk" msgstr "Slovenština" msgid "keys.upload" -msgstr "Nahrát stránku pomocí Titanu" +msgstr "Nahrát pomocí Titanu" msgid "error.proxy.msg" msgstr "" -"Požadavek pomocí proxy selhal, protože server nebyl schopen utvořit spojení " -"s vzdáleným hostitelem. Je možné, že existují problémy s konektivitou." +"Požadavek pomocí prostředníka selhal, protože server nebyl schopen utvořit " +"spojení s vzdáleným hostitelem. Je možné, že existují problémy s připojením." msgid "error.proxyrefusal.msg" msgstr "" "Váš požadavek byl pro zdroj na doméně, která není obsluhována tímto serverem " -"a ten server nepřijímá proxy požadavky." +"a ten server nepřijímá požadavky přes prostředníky." msgid "heading.bookmark.add" -msgstr "PŘIDAT ZÁLOŽKU" +msgstr "Přidat záložku" msgid "heading.bookmark.edit" -msgstr "UPRAVIT ZÁLOŽKU" +msgstr "Upravit záložku" msgid "dlg.bookmark.save" msgstr "Uložit záložku" @@ -1227,20 +1227,20 @@ msgid "dlg.addfolder.prompt" msgstr "Zadejte jméno nové složky:" msgid "heading.addfolder" -msgstr "PŘIDAT SLOŽKU" +msgstr "Přidat složku" msgid "dlg.bookmark.url" msgstr "URL:" msgid "heading.prefs.certs" -msgstr "CERTIFIKÁTY" +msgstr "Certifikáty" # used on mobile msgid "heading.settings" -msgstr "NASTAVENÍ" +msgstr "Nastavení" msgid "heading.prefs.uitheme" -msgstr "BARVY ROZHRANÍ" +msgstr "Barvy rozhraní" msgid "prefs.searchurl" msgstr "URL vyhledávání:" @@ -1289,7 +1289,7 @@ msgid "error.cgi" msgstr "CGI chyba" msgid "error.proxy" -msgstr "Chyba proxy" +msgstr "Chyba prostředníka" msgid "error.permanent" msgstr "Trvalé selhání" @@ -1386,13 +1386,13 @@ msgid "dlg.newident.more" msgstr "Více…" msgid "heading.newident.missing" -msgstr "CHYBĚJÍCÍ INFORMACE" +msgstr "Chybějící informace" msgid "dlg.newindent.missing.commonname" msgstr "\"Obecné jméno\" musí být specifikováno." msgid "heading.newident.date.bad" -msgstr "NEPLATNÉ DATUM" +msgstr "Neplatné datum" msgid "dlg.newident.date.past" msgstr "Datum vypršení platnosti musí být v budoucnosti." @@ -1408,10 +1408,10 @@ msgid "keys.split.menu" msgstr "Nastavit režim rozpůleného okna" msgid "heading.feedcfg" -msgstr "NASTAVENÍ KANÁLU" +msgstr "Nastavení kanálu" msgid "heading.subscribe" -msgstr "ODEBÍRAT STRÁNKU" +msgstr "Odebírat stránku" msgid "dlg.feed.title" msgstr "Název:" @@ -1438,22 +1438,22 @@ msgid "heading.prefs.network" msgstr "Síť" msgid "heading.prefs.paragraph" -msgstr "ODSTAVEC" +msgstr "Odstavec" msgid "heading.prefs.pagecontent" -msgstr "BARVY STRÁNKY" +msgstr "Barvy stránky" msgid "heading.prefs.proxies" -msgstr "PROXY" +msgstr "Prostředníci" msgid "heading.prefs.scrolling" -msgstr "POSOUVÁNÍ STRÁNKY" +msgstr "Posouvání stránky" msgid "heading.prefs.sizing" -msgstr "DIMENZOVÁNÍ" +msgstr "Dimenzování" msgid "heading.prefs.widelayout" -msgstr "ŠIROKÉ ROZLOŽENÍ" +msgstr "Široké rozložení" # tab button msgid "heading.prefs.style" @@ -1598,13 +1598,13 @@ msgid "prefs.ca.path" msgstr "Cesta k CA:" msgid "prefs.proxy.gemini" -msgstr "Proxy pro Gemini:" +msgstr "Prostředník pro Gemini:" msgid "prefs.proxy.gopher" -msgstr "Proxy pro Gopher:" +msgstr "Prostředník pro Gopher:" msgid "prefs.proxy.http" -msgstr "Proxy pro HTTP:" +msgstr "Prostředník pro HTTP:" msgid "menu.binding.reset" msgstr "Obnovit výchozí" @@ -1720,7 +1720,7 @@ msgid "error.gone.msg" msgstr "Požadovaný zdroj již není dostupný." msgid "error.proxyrefusal" -msgstr "Proxy požadavek byl odmítnut" +msgstr "Požadavek na prostředníka byl odmítnut" msgid "error.badheader.msg" msgstr "" @@ -1769,13 +1769,13 @@ msgid "dlg.bookmark.icon" msgstr "Ikona:" msgid "heading.bookmark.tags" -msgstr "SPECIÁLNÍ ZNAČENÍ" +msgstr "Speciální značení" msgid "dlg.addfolder" msgstr "Přidat složku" msgid "heading.prefs" -msgstr "PŘEDVOLBY" +msgstr "Předvolby" # tab button msgid "heading.prefs.colors" @@ -2009,7 +2009,7 @@ msgid "fontpack.delete" msgstr "Trvale odstranit \"%s\"" msgid "heading.fontpack.delete" -msgstr "ODSTRANIT PÍSMOVÝ BALÍČEK" +msgstr "Odstranit písmový balíček" #, c-format msgid "dlg.fontpack.delete.confirm" @@ -2030,7 +2030,7 @@ msgstr "" "zkopírovány do složky uživatelských písem." msgid "heading.dismiss.warning" -msgstr "PŘESTAT UKAZOVAT VAROVÁNÍ?" +msgstr "Skrýt varování?" #, c-format msgid "dlg.dismiss.ansi" @@ -2040,7 +2040,7 @@ msgid "dlg.dismiss.warning" msgstr "Přestat ukazovat varování" msgid "heading.fontpack.classic" -msgstr "STÁHNOUT PÍSMOVÝ BALÍČEK" +msgstr "Stáhnout písmový balíček" msgid "dlg.fontpack.classic.msg" msgstr "" @@ -2152,3 +2152,91 @@ msgstr "Zkontrolovat dostupné aktualizace…" # This label should be fairly short so it fits in a button in the sidebar. msgid "sidebar.action.feeds.markallread" msgstr "Označit vše jako přečtené" + +msgid "menu.open.external" +msgstr "Otevřít v jiné aplikaci" + +# Active identity toolbar menu. +msgid "menu.hide.identities" +msgstr "Skrýt totožnosti" + +msgid "menu.home" +msgstr "Jít domů" + +msgid "menu.identities" +msgstr "Spravovat totožnosti" + +msgid "sidebar.close" +msgstr "Hotovo" + +msgid "sidebar.action.bookmarks.newfolder" +msgstr "Nová složka" + +msgid "sidebar.action.bookmarks.edit" +msgstr "Upravit" + +msgid "sidebar.action.history.clear" +msgstr "Vymazat" + +# The %s represents the name of an identity. +#, c-format +msgid "ident.switch" +msgstr "Používat %s" + +msgid "sidebar.empty.unread" +msgstr "Žádné nepřečtené záznamy" + +# Paste the line preceding the clicked link into the input prompt. +msgid "menu.input.precedingline" +msgstr "Vložit předcházející řádek" + +# Shows where a local file is using the Finder. +msgid "menu.reveal.macos" +msgstr "Ukázat ve Finderu" + +msgid "menu.share" +msgstr "Sdílet" + +msgid "menu.page.upload.edit" +msgstr "Upravit pomocí Titanu…" + +msgid "heading.upload.id" +msgstr "Oprávnění" + +msgid "menu.upload.export" +msgstr "Exportovat text" + +msgid "menu.upload.delete" +msgstr "Odstranit vše" + +msgid "menu.upload.delete.confirm" +msgstr "Opravdu odstranit vše (není cesty zpět)" + +# Mobile subheading in the Upload dialog. +msgid "upload.url" +msgstr "URL" + +# Mobile subheading: buttons for entering uploaded data. +msgid "upload.content" +msgstr "Obsah" + +msgid "hint.upload.path" +msgstr "Cesta URL" + +msgid "hint.upload.token.long" +msgstr "token - podívejte se na instrukce serveru" + +msgid "heading.prefs.toolbaractions" +msgstr "Akce panelu nástrojů" + +msgid "prefs.toolbaraction1" +msgstr "Tlačítko 1" + +msgid "prefs.toolbaraction2" +msgstr "Tlačítko 2" + +msgid "prefs.blink" +msgstr "Blikající kurzor:" + +msgid "keys.upload.edit" +msgstr "Upravit pomocí Titanu" -- cgit v1.2.3 From a876e38d3d67c69b4772a24819d3e9b56d0d31cb Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 16 Jan 2022 14:46:39 +0200 Subject: Updated language strings --- res/lang/cs.bin | Bin 31885 -> 32034 bytes res/lang/sr.bin | Bin 44885 -> 44881 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/res/lang/cs.bin b/res/lang/cs.bin index 1d650fa4..af77f8b2 100644 Binary files a/res/lang/cs.bin and b/res/lang/cs.bin differ diff --git a/res/lang/sr.bin b/res/lang/sr.bin index ca45035d..cb3da72a 100644 Binary files a/res/lang/sr.bin and b/res/lang/sr.bin differ -- cgit v1.2.3 From c5ae02ce788d5e510ac24a3982bd227eb0784d3b Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 16 Jan 2022 14:47:23 +0200 Subject: Updated AUTHORS --- AUTHORS.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index 99e13875..1555ab65 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -5,17 +5,17 @@ Lagrange was created by Jaakko Keränen () in July 2020. Legend: `C` code, `T` translation ``` -C 2975 Jaakko Keränen +C 3009 Jaakko Keränen CT 43 Nikolay Korotkiy + T 42 Alyssa Liddell T 41 Olga Smirnova - T 39 Alyssa Liddell - T 29 Anna “CyberTailor” + T 30 Anna “CyberTailor” + T 27 Страхиња Радић T 26 Shibo Lyu - T 25 Страхиња Радић + T 19 Wally Hackenslacker T 18 MCMic - T 18 Wally Hackenslacker - T 13 Xosé M - T 12 Tadeáš Erban + T 14 Xosé M + T 13 Tadeáš Erban T 9 Aaron Fischer T 8 El Mau T 7 Waterrail @@ -36,6 +36,7 @@ C 1 Adam Mizerski C 1 Charles C 1 Dario Vladovic C 1 David Gillies + T 1 Emir SARI T 1 Eric T 1 Marek Ľach C 1 Raph M -- cgit v1.2.3 From 73a506239e4dc8be07313147cde4046a3bd848e1 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 16 Jan 2022 17:40:32 +0200 Subject: Navbar menu placement Popup menus opened from navbar buttons should open below the button. IssueID #436 --- src/ui/root.c | 8 ++++++-- src/ui/util.c | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/ui/root.c b/src/ui/root.c index a3c2e0ae..64043fd3 100644 --- a/src/ui/root.c +++ b/src/ui/root.c @@ -318,9 +318,12 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) { if (equal_Command(cmd, "menu.open")) { iWidget *button = pointer_Command(cmd); iWidget *menu = findChild_Widget(button, "menu"); + const iBool isPlacedUnder = argLabel_Command(cmd, "under"); iAssert(menu); if (!isVisible_Widget(menu)) { - openMenu_Widget(menu, topLeft_Rect(bounds_Widget(button))); + openMenu_Widget(menu, + isPlacedUnder ? bottomLeft_Rect(bounds_Widget(button)) + : topLeft_Rect(bounds_Widget(button))); } else { closeMenu_Widget(menu); @@ -416,7 +419,7 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) { } iWidget *menu = makeMenu_Widget(button, constData_Array(&items), size_Array(&items)); - openMenu_Widget(menu, topLeft_Rect(bounds_Widget(button))); + openMenu_Widget(menu, bottomLeft_Rect(bounds_Widget(button))); deinit_Array(&items); return iTrue; } @@ -1484,6 +1487,7 @@ void createUserInterface_Root(iRoot *d) { iLabelWidget *navMenu = makeMenuButton_LabelWidget(menu_Icon, navMenuItems_, iElemCount(navMenuItems_)); # endif + setCommand_LabelWidget(navMenu, collectNewCStr_String("menu.open under:1")); setAlignVisually_LabelWidget(navMenu, iTrue); setId_Widget(addChildFlags_Widget(navBar, iClob(navMenu), collapse_WidgetFlag), "navbar.menu"); #endif diff --git a/src/ui/util.c b/src/ui/util.c index 41e645de..31907721 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -680,7 +680,7 @@ static iBool isCommandIgnoredByMenus_(const char *cmd) { static iLabelWidget *parentMenuButton_(const iWidget *menu) { if (isInstance_Object(menu->parent, &Class_LabelWidget)) { iLabelWidget *button = (iLabelWidget *) menu->parent; - if (!cmp_String(command_LabelWidget(button), "menu.open")) { + if (equal_Command(cstr_String(command_LabelWidget(button)), "menu.open")) { return button; } } -- cgit v1.2.3 From a777e1a26a9e9ec97bc636e82349144912ce4a6b Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 16 Jan 2022 19:31:11 +0200 Subject: Added menu string for customizing toolbar --- po/en.po | 4 ++++ res/lang/cs.bin | Bin 32034 -> 32069 bytes res/lang/de.bin | Bin 30861 -> 30896 bytes res/lang/en.bin | Bin 26930 -> 26965 bytes res/lang/eo.bin | Bin 25900 -> 25935 bytes res/lang/es.bin | Bin 30749 -> 30784 bytes res/lang/es_MX.bin | Bin 28000 -> 28035 bytes res/lang/fi.bin | Bin 30559 -> 30594 bytes res/lang/fr.bin | Bin 31658 -> 31693 bytes res/lang/gl.bin | Bin 29917 -> 29952 bytes res/lang/hu.bin | Bin 31683 -> 31718 bytes res/lang/ia.bin | Bin 29713 -> 29748 bytes res/lang/ie.bin | Bin 29620 -> 29655 bytes res/lang/isv.bin | Bin 25651 -> 25686 bytes res/lang/nl.bin | Bin 29040 -> 29075 bytes res/lang/pl.bin | Bin 30286 -> 30321 bytes res/lang/ru.bin | Bin 45571 -> 45606 bytes res/lang/sk.bin | Bin 25987 -> 26022 bytes res/lang/sr.bin | Bin 44881 -> 44916 bytes res/lang/tok.bin | Bin 27766 -> 27801 bytes res/lang/tr.bin | Bin 29867 -> 29902 bytes res/lang/uk.bin | Bin 44956 -> 44991 bytes res/lang/zh_Hans.bin | Bin 25895 -> 25930 bytes res/lang/zh_Hant.bin | Bin 26293 -> 26328 bytes src/ui/root.c | 2 ++ 25 files changed, 6 insertions(+) diff --git a/po/en.po b/po/en.po index 36f5d0c3..e0e086e3 100644 --- a/po/en.po +++ b/po/en.po @@ -407,6 +407,10 @@ msgstr "Manage Identities" msgid "menu.identity.notactive" msgstr "No Active Identity" +# Menu heading shown when customizing navbar button actions. +msgid "menu.toolbar.setaction" +msgstr "Set Action:" + msgid "sidebar.close" msgstr "Done" diff --git a/res/lang/cs.bin b/res/lang/cs.bin index af77f8b2..eb59f65d 100644 Binary files a/res/lang/cs.bin and b/res/lang/cs.bin differ diff --git a/res/lang/de.bin b/res/lang/de.bin index bf857a70..9c3d4541 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 29608b71..5f649846 100644 Binary files a/res/lang/en.bin and b/res/lang/en.bin differ diff --git a/res/lang/eo.bin b/res/lang/eo.bin index d1d9dba3..beeeb21e 100644 Binary files a/res/lang/eo.bin and b/res/lang/eo.bin differ diff --git a/res/lang/es.bin b/res/lang/es.bin index a434cab4..14c1b843 100644 Binary files a/res/lang/es.bin and b/res/lang/es.bin differ diff --git a/res/lang/es_MX.bin b/res/lang/es_MX.bin index fd3f4e1f..dfcd6306 100644 Binary files a/res/lang/es_MX.bin and b/res/lang/es_MX.bin differ diff --git a/res/lang/fi.bin b/res/lang/fi.bin index 1d1a5185..6e5e3ee6 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 1cb5ddb1..ad16b2e7 100644 Binary files a/res/lang/fr.bin and b/res/lang/fr.bin differ diff --git a/res/lang/gl.bin b/res/lang/gl.bin index ba86c15d..f1501b4a 100644 Binary files a/res/lang/gl.bin and b/res/lang/gl.bin differ diff --git a/res/lang/hu.bin b/res/lang/hu.bin index 8dba97f9..c070dd49 100644 Binary files a/res/lang/hu.bin and b/res/lang/hu.bin differ diff --git a/res/lang/ia.bin b/res/lang/ia.bin index c3304a24..34633dd1 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 8988972e..722ee20d 100644 Binary files a/res/lang/ie.bin and b/res/lang/ie.bin differ diff --git a/res/lang/isv.bin b/res/lang/isv.bin index b14235a4..3be01643 100644 Binary files a/res/lang/isv.bin and b/res/lang/isv.bin differ diff --git a/res/lang/nl.bin b/res/lang/nl.bin index 8c40c833..dcc9fe97 100644 Binary files a/res/lang/nl.bin and b/res/lang/nl.bin differ diff --git a/res/lang/pl.bin b/res/lang/pl.bin index 4018a495..a32e9d10 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 2c0ddc73..11764fc0 100644 Binary files a/res/lang/ru.bin and b/res/lang/ru.bin differ diff --git a/res/lang/sk.bin b/res/lang/sk.bin index 259529e2..833872c9 100644 Binary files a/res/lang/sk.bin and b/res/lang/sk.bin differ diff --git a/res/lang/sr.bin b/res/lang/sr.bin index cb3da72a..fde7227f 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 3a3f1ee0..d17f075e 100644 Binary files a/res/lang/tok.bin and b/res/lang/tok.bin differ diff --git a/res/lang/tr.bin b/res/lang/tr.bin index 3ad7e349..06fc22e5 100644 Binary files a/res/lang/tr.bin and b/res/lang/tr.bin differ diff --git a/res/lang/uk.bin b/res/lang/uk.bin index 7a9cbd28..4f2a0085 100644 Binary files a/res/lang/uk.bin and b/res/lang/uk.bin differ diff --git a/res/lang/zh_Hans.bin b/res/lang/zh_Hans.bin index 63f4040c..246b7c42 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 23cfa0e7..0e94f66a 100644 Binary files a/res/lang/zh_Hant.bin and b/res/lang/zh_Hant.bin differ diff --git a/src/ui/root.c b/src/ui/root.c index 64043fd3..5c4296cf 100644 --- a/src/ui/root.c +++ b/src/ui/root.c @@ -872,6 +872,7 @@ static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) { const int buttonIndex = id.end[-1] - '1'; iArray items; init_Array(&items, sizeof(iMenuItem)); + pushBack_Array(&items, &(iMenuItem){ "```${menu.toolbar.setaction}" }); for (size_t i = 0; i < max_ToolbarAction; i++) { pushBack_Array( &items, @@ -886,6 +887,7 @@ static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) { makeMenu_Widget(get_Root()->widget, constData_Array(&items), size_Array(&items)), coord_Command(cmd)); deinit_Array(&items); + return iTrue; } return iFalse; } -- cgit v1.2.3 From 9b290f35ff00076d825bcc57f8590e7f0cd23cd8 Mon Sep 17 00:00:00 2001 From: Alyssa Liddell Date: Sun, 16 Jan 2022 15:39:05 +0000 Subject: Translated using Weblate (Russian) Currently translated at 100.0% (645 of 645 strings) Translation: Lagrange/User Interface Translate-URL: http://weblate.skyjake.fi/projects/lagrange/ui/ru/ --- po/ru.po | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/po/ru.po b/po/ru.po index f2fd4591..b5def1af 100644 --- a/po/ru.po +++ b/po/ru.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" "POT-Creation-Date: 2021-03-23 19:02+0000\n" -"PO-Revision-Date: 2022-01-08 14:50+0000\n" +"PO-Revision-Date: 2022-01-16 17:31+0000\n" "Last-Translator: Alyssa Liddell \n" "Language-Team: Russian \n" "Language: ru\n" @@ -2230,3 +2230,7 @@ msgstr "Кнопка 2" msgid "keys.upload.edit" msgstr "Редактировать страницу (Titan)" + +# Shows where a local file is using the File Manager. +msgid "menu.reveal.filemgr" +msgstr "Показать в файловом менеджере" -- cgit v1.2.3 From 6d0f54c93a28ba590711e4e88cc0349eba7d14ac Mon Sep 17 00:00:00 2001 From: Nikolay Korotkiy Date: Sun, 16 Jan 2022 16:23:08 +0000 Subject: Translated using Weblate (Finnish) Currently translated at 100.0% (645 of 645 strings) Translation: Lagrange/User Interface Translate-URL: http://weblate.skyjake.fi/projects/lagrange/ui/fi/ --- po/fi.po | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/po/fi.po b/po/fi.po index 2310fb14..c61d974b 100644 --- a/po/fi.po +++ b/po/fi.po @@ -3,8 +3,8 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" "POT-Creation-Date: 2021-03-23 09:09+0000\n" -"PO-Revision-Date: 2022-01-08 14:50+0000\n" -"Last-Translator: Weblate Admin \n" +"PO-Revision-Date: 2022-01-16 17:31+0000\n" +"Last-Translator: Nikolay Korotkiy \n" "Language-Team: Finnish \n" "Language: fi\n" "MIME-Version: 1.0\n" @@ -2228,3 +2228,7 @@ msgstr "Näytä Finderissa" msgid "menu.share" msgstr "Jaa" + +# Shows where a local file is using the File Manager. +msgid "menu.reveal.filemgr" +msgstr "Näytä tiedostonhallinnassa" -- cgit v1.2.3 From 6df8aec784377109805f403b9b1e13cb84e027e5 Mon Sep 17 00:00:00 2001 From: Nikolay Korotkiy Date: Sun, 16 Jan 2022 16:34:05 +0000 Subject: Translated using Weblate (Esperanto) Currently translated at 36.1% (233 of 645 strings) Translation: Lagrange/User Interface Translate-URL: http://weblate.skyjake.fi/projects/lagrange/ui/eo/ --- po/eo.po | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 72 insertions(+), 15 deletions(-) diff --git a/po/eo.po b/po/eo.po index 5bbf56c5..3d770531 100644 --- a/po/eo.po +++ b/po/eo.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" -"PO-Revision-Date: 2021-12-08 14:50+0000\n" +"PO-Revision-Date: 2022-01-16 17:31+0000\n" "Last-Translator: Nikolay Korotkiy \n" "Language-Team: Esperanto " "\n" @@ -227,7 +227,7 @@ msgid "feeds.entry.markunread" msgstr "Marki nelegita" msgid "heading.history.clear" -msgstr "MALPLENIGI HISTORION" +msgstr "Malplenigi historion" msgid "ident.delete" msgstr "Forigi identecon…" @@ -236,7 +236,7 @@ msgid "ident.fingerprint" msgstr "Kopii fingropremaĵon" msgid "heading.ident.use" -msgstr "IDENTECA UZADO" +msgstr "Identeca uzado" msgid "menu.edit.notes" msgstr "Redakti notojn…" @@ -259,10 +259,10 @@ msgid "dlg.input.send" msgstr "Sendi" msgid "heading.unsub" -msgstr "MALABONO" +msgstr "Malabono" msgid "heading.pageinfo" -msgstr "PAĜINFORMOJ" +msgstr "Paĝinformoj" msgid "dlg.cert.fingerprint" msgstr "Kopii fingropremaĵon" @@ -301,10 +301,10 @@ msgstr[0] "%d horo" msgstr[1] "%d horoj" msgid "heading.openlink" -msgstr "MALFERMI LIGILON" +msgstr "Malfermi ligilon" msgid "heading.certimport" -msgstr "IMPORTI IDENTECON" +msgstr "Importi identecon" msgid "hint.certimport.description" msgstr "priskribo" @@ -332,7 +332,7 @@ msgid "link.hint.audio" msgstr "Ludi la sondosieron" msgid "link.hint.image" -msgstr "Montri la bildon" +msgstr "Bildo" msgid "lang.ar" msgstr "Araba" @@ -380,7 +380,7 @@ msgid "dlg.newident.country" msgstr "Lando:" msgid "heading.newident" -msgstr "NOVA IDENTECO" +msgstr "Nova identeco" msgid "dlg.newident.domain" msgstr "Domajno:" @@ -411,7 +411,7 @@ msgid "dlg.bookmark.icon" msgstr "Bildsimbolo:" msgid "heading.prefs" -msgstr "AGORDOJ" +msgstr "Agordoj" msgid "heading.prefs.fonts" msgstr "Tiparoj" @@ -421,10 +421,10 @@ msgid "heading.prefs.network" msgstr "Reto" msgid "heading.prefs.paragraph" -msgstr "ALINEO" +msgstr "Alineo" msgid "heading.prefs.scrolling" -msgstr "RULUMADO" +msgstr "Rulumado" msgid "prefs.theme" msgstr "Temo:" @@ -559,7 +559,7 @@ msgid "dlg.newident.temp" msgstr "Provizora:" msgid "heading.prefs.certs" -msgstr "ATESTILOJ" +msgstr "Atestiloj" #, c-format msgid "num.minutes" @@ -580,7 +580,7 @@ msgid "keys.fullscreen" msgstr "Baskuligi plenekranan reĝimon" msgid "heading.import.bookmarks" -msgstr "IMPORTI LEGOSIGNOJN" +msgstr "Importi legosignojn" msgid "lang.sr" msgstr "Serba" @@ -679,7 +679,7 @@ msgid "heading.prefs.interface" msgstr "Fasado" msgid "heading.prefs.proxies" -msgstr "PROKURILOJ" +msgstr "Prokuriloj" msgid "gempub.meta.lang" msgstr "Lingvo" @@ -746,3 +746,60 @@ msgstr "Tiparoj" msgid "dlg.dismiss.warning" msgstr "Ignori averton" + +msgid "menu.newfolder" +msgstr "Nova dosierujo…" + +# keep this short (3x1 horiz layout) +msgid "menu.selectall" +msgstr "Elekti ĉiujn" + +msgid "menu.copyurl" +msgstr "Kopii URL" + +#, c-format +msgid "minutes.ago" +msgid_plural "minutes.ago.n" +msgstr[0] "antaŭ %d minuto" +msgstr[1] "antaŭ %d minutoj" + +#, c-format +msgid "hours.ago" +msgid_plural "hours.ago.n" +msgstr[0] "antaŭ %d horo" +msgstr[1] "antaŭ %d horoj" + +# used for Preferences on mobile +msgid "menu.settings" +msgstr "Agordoj" + +# keep this short (3x1 horiz layout) +msgid "menu.delete" +msgstr "Forigi" + +msgid "sidebar.action.bookmarks.newfolder" +msgstr "Nova dosierujo" + +msgid "sidebar.action.bookmarks.edit" +msgstr "Redakti" + +msgid "num.files" +msgid_plural "num.files.n" +msgstr[0] "%u dosiero" +msgstr[1] "%u dosieroj" + +msgid "ident.export" +msgstr "Eksporti" + +#, c-format +msgid "days.ago" +msgid_plural "days.ago.n" +msgstr[0] "antaŭ %d tago" +msgstr[1] "antaŭ %d tagoj" + +# keep this short (3x1 horiz layout) +msgid "menu.undo" +msgstr "Malfari" + +msgid "sidebar.action.show" +msgstr "Montri:" -- cgit v1.2.3 From 12c9964e465e4522e03fd3414902624088ae232a Mon Sep 17 00:00:00 2001 From: Страхиња Радић Date: Sun, 16 Jan 2022 13:42:58 +0000 Subject: Translated using Weblate (Serbian) Currently translated at 100.0% (645 of 645 strings) Translation: Lagrange/User Interface Translate-URL: http://weblate.skyjake.fi/projects/lagrange/ui/sr/ --- po/sr.po | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/po/sr.po b/po/sr.po index e9b31570..e6cef348 100644 --- a/po/sr.po +++ b/po/sr.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" -"PO-Revision-Date: 2022-01-16 09:50+0000\n" +"PO-Revision-Date: 2022-01-16 17:31+0000\n" "Last-Translator: Страхиња Радић \n" "Language-Team: Serbian \n" "Language: sr\n" @@ -2277,3 +2277,7 @@ msgstr "Трепћући курсор:" msgid "keys.upload.edit" msgstr "Уређивање странице преко Титана" + +# Shows where a local file is using the File Manager. +msgid "menu.reveal.filemgr" +msgstr "Прикажи у Менаџеру датотека" -- cgit v1.2.3 From 786e9a5bc0601449143c1c3e2fbfae76e3ea8071 Mon Sep 17 00:00:00 2001 From: Alyssa Liddell Date: Sun, 16 Jan 2022 15:38:19 +0000 Subject: Translated using Weblate (Ukrainian) Currently translated at 100.0% (645 of 645 strings) Translation: Lagrange/User Interface Translate-URL: http://weblate.skyjake.fi/projects/lagrange/ui/uk/ --- po/uk.po | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/po/uk.po b/po/uk.po index 9b675221..1015097c 100644 --- a/po/uk.po +++ b/po/uk.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" -"PO-Revision-Date: 2022-01-08 14:50+0000\n" +"PO-Revision-Date: 2022-01-16 17:31+0000\n" "Last-Translator: Alyssa Liddell \n" "Language-Team: Ukrainian " "\n" @@ -2258,3 +2258,7 @@ msgstr "Шлях URL" msgid "hint.upload.token.long" msgstr "токен — див. вказівки сервера" + +# Shows where a local file is using the File Manager. +msgid "menu.reveal.filemgr" +msgstr "Показати у файловому менеджері" -- cgit v1.2.3 From fb3889c6021013b6dc028cae0f4392bc0bcf0eae Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Mon, 17 Jan 2022 07:12:35 +0200 Subject: macOS: Scroll glitch prevention (Monterey 12.1) --- src/macos.m | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/macos.m b/src/macos.m index 911f5e3a..4ad267c1 100644 --- a/src/macos.m +++ b/src/macos.m @@ -429,12 +429,48 @@ static void trackSwipe_(NSEvent *event) { #endif static int swipeDir_ = 0; +static int preventTapGlitch_ = 0; static iBool processScrollWheelEvent_(NSEvent *event) { const iBool isPerPixel = (event.hasPreciseScrollingDeltas != 0); const iBool isInertia = (event.momentumPhase & (NSEventPhaseBegan | NSEventPhaseChanged)) != 0; const iBool isEnded = event.scrollingDeltaX == 0.0f && event.scrollingDeltaY == 0.0f && !isInertia; const iWindow *win = &get_MainWindow()->base; + if (isPerPixel) { + /* On macOS 12.1, stopping ongoing inertia scroll with a tap seems to sometimes produce + spurious large scroll events. */ + switch (preventTapGlitch_) { + case 0: + if (isInertia && event.momentumPhase == NSEventPhaseChanged) { + preventTapGlitch_++; + } + else { + preventTapGlitch_ = 0; + } + break; + case 1: + if (event.scrollingDeltaY == 0 && event.momentumPhase == NSEventPhaseEnded) { + preventTapGlitch_++; + } + break; + case 2: + if (event.scrollingDeltaY == 0 && event.momentumPhase == 0 && isEnded) { + preventTapGlitch_++; + } + else { + preventTapGlitch_ = 0; + } + break; + case 3: + if (event.scrollingDeltaY != 0 && event.momentumPhase == 0 && !isInertia) { + preventTapGlitch_ = 0; + // printf("SPURIOUS\n"); fflush(stdout); + return iTrue; + } + preventTapGlitch_ = 0; + break; + } + } /* Post corresponding MOUSEWHEEL events. */ SDL_MouseWheelEvent e = { .type = SDL_MOUSEWHEEL }; e.timestamp = SDL_GetTicks(); @@ -464,7 +500,8 @@ static iBool processScrollWheelEvent_(NSEvent *event) { e.x = -event.scrollingDeltaX; e.y = iSign(event.scrollingDeltaY); } -// printf("#### dx:%d dy:%d phase:%ld end:%d\n", e.x, e.y, (long) event.momentumPhase, isEnded); fflush(stdout); + // printf("#### [%d] dx:%d dy:%d phase:%ld inertia:%d end:%d\n", preventTapGlitch_, e.x, e.y, (long) event.momentumPhase, + // isInertia, isEnded); fflush(stdout); SDL_PushEvent((SDL_Event *) &e); #if 0 /* On macOS, we handle both trackpad and mouse events. We expect SDL to identify -- cgit v1.2.3 From 5a204c06ca29c504ca4b8ff08cae3dbd5a71ee63 Mon Sep 17 00:00:00 2001 From: Weblate Admin Date: Sun, 16 Jan 2022 17:40:01 +0000 Subject: Translated using Weblate (Finnish) Currently translated at 100.0% (646 of 646 strings) Translation: Lagrange/User Interface Translate-URL: http://weblate.skyjake.fi/projects/lagrange/ui/fi/ --- po/fi.po | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/po/fi.po b/po/fi.po index c61d974b..6ed46bb8 100644 --- a/po/fi.po +++ b/po/fi.po @@ -3,8 +3,8 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" "POT-Creation-Date: 2021-03-23 09:09+0000\n" -"PO-Revision-Date: 2022-01-16 17:31+0000\n" -"Last-Translator: Nikolay Korotkiy \n" +"PO-Revision-Date: 2022-01-17 09:43+0000\n" +"Last-Translator: Weblate Admin \n" "Language-Team: Finnish \n" "Language: fi\n" "MIME-Version: 1.0\n" @@ -2232,3 +2232,7 @@ msgstr "Jaa" # Shows where a local file is using the File Manager. msgid "menu.reveal.filemgr" msgstr "Näytä tiedostonhallinnassa" + +# Menu heading shown when customizing navbar button actions. +msgid "menu.toolbar.setaction" +msgstr "Aseta toiminto:" -- cgit v1.2.3 From 511c49e36b63fa448afb07bc14ecb8d5cafb0f6d Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Mon, 17 Jan 2022 11:44:10 +0200 Subject: Updated language strings --- res/lang/eo.bin | Bin 25935 -> 26163 bytes res/lang/fi.bin | Bin 30594 -> 30606 bytes res/lang/ru.bin | Bin 45606 -> 45641 bytes res/lang/sr.bin | Bin 44916 -> 44947 bytes res/lang/uk.bin | Bin 44991 -> 45028 bytes 5 files changed, 0 insertions(+), 0 deletions(-) diff --git a/res/lang/eo.bin b/res/lang/eo.bin index beeeb21e..ace592df 100644 Binary files a/res/lang/eo.bin and b/res/lang/eo.bin differ diff --git a/res/lang/fi.bin b/res/lang/fi.bin index 6e5e3ee6..44d2741d 100644 Binary files a/res/lang/fi.bin and b/res/lang/fi.bin differ diff --git a/res/lang/ru.bin b/res/lang/ru.bin index 11764fc0..e434971b 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 fde7227f..43a6deda 100644 Binary files a/res/lang/sr.bin and b/res/lang/sr.bin differ diff --git a/res/lang/uk.bin b/res/lang/uk.bin index 4f2a0085..23329907 100644 Binary files a/res/lang/uk.bin and b/res/lang/uk.bin differ -- cgit v1.2.3 From 5d2e8f5acc6448c17a4532bea692dcb9d92d3dc9 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Mon, 17 Jan 2022 12:38:07 +0200 Subject: macOS: Deploy the app icon --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1bb50791..4ac00ad3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -294,6 +294,7 @@ elseif (APPLE) add_definitions (-DiPlatformAppleDesktop=1) list (APPEND SOURCES src/macos.m src/macos.h) list (APPEND RESOURCES "res/Lagrange.icns") + set_source_files_properties ("res/Lagrange.icns" PROPERTIES MACOSX_PACKAGE_LOCATION Resources) endif () if (MSYS) set (WINRC_FILE_VERSION ${PROJECT_VERSION_MAJOR},${PROJECT_VERSION_MINOR},${PROJECT_VERSION_PATCH},0) @@ -301,7 +302,6 @@ if (MSYS) configure_file (res/lagrange.rc.in ${CMAKE_CURRENT_BINARY_DIR}/lagrange.rc NEWLINE_STYLE WIN32) list (APPEND SOURCES src/win32.c src/win32.h ${CMAKE_CURRENT_BINARY_DIR}/lagrange.rc) endif () -#set_source_files_properties (${RESOURCES} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) if (MSYS OR (APPLE AND NOT MOBILE) OR (UNIX AND NOT MOBILE)) add_definitions (-DiPlatformPcDesktop=1) endif () -- cgit v1.2.3 From 25ef7c7ce7f74e2c509251145afb25e7ae2f7d1e Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Mon, 17 Jan 2022 19:12:43 +0200 Subject: Bumped version number to 1.10.1 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4ac00ad3..97d5d279 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,7 +18,7 @@ cmake_minimum_required (VERSION 3.9) project (Lagrange - VERSION 1.10.0 + VERSION 1.10.1 DESCRIPTION "A Beautiful Gemini Client" LANGUAGES C ) -- cgit v1.2.3 From 83a6847babf282f1d334039f13e003773fd70cfc Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Mon, 17 Jan 2022 19:13:16 +0200 Subject: SidebarWidget: Fixed actions getting hidden on Feeds tab --- res/about/version.gmi | 3 +++ src/ui/sidebarwidget.c | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/res/about/version.gmi b/res/about/version.gmi index 9a8f5a21..b2700598 100644 --- a/res/about/version.gmi +++ b/res/about/version.gmi @@ -6,6 +6,9 @@ ``` # Release notes +## 1.10.1 +* Fixed bottom actions of the Feeds sidebar getting hidden when all entries are read. This prevented switching between Unread/All filter modes. + ## 1.10 New features: * macOS: Trackpad swipe navigation. diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index f5beb785..16677f9e 100644 --- a/src/ui/sidebarwidget.c +++ b/src/ui/sidebarwidget.c @@ -286,7 +286,8 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct iZap(on); size_t numItems = 0; isEmpty = iTrue; - iConstForEach(PtrArray, i, listEntries_Feeds()) { + const iPtrArray *feedEntries = listEntries_Feeds(); + iConstForEach(PtrArray, i, feedEntries) { const iFeedEntry *entry = i.ptr; if (isHidden_FeedEntry(entry)) { continue; /* A hidden entry. */ @@ -350,7 +351,7 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct } /* Actions. */ if (!isMobile) { - if (!keepActions && !isEmpty) { + if (!keepActions && !isEmpty_PtrArray(feedEntries)) { addActionButton_SidebarWidget_(d, check_Icon " ${sidebar.action.feeds.markallread}", -- cgit v1.2.3 From de9201c809ec7777b0fcfb3567442dbd6a2ab5ae Mon Sep 17 00:00:00 2001 From: Nikolay Korotkiy Date: Mon, 17 Jan 2022 11:28:04 +0000 Subject: Translated using Weblate (Russian) Currently translated at 100.0% (646 of 646 strings) Translation: Lagrange/User Interface Translate-URL: http://weblate.skyjake.fi/projects/lagrange/ui/ru/ --- po/ru.po | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/po/ru.po b/po/ru.po index b5def1af..d8306f17 100644 --- a/po/ru.po +++ b/po/ru.po @@ -3,8 +3,8 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" "POT-Creation-Date: 2021-03-23 19:02+0000\n" -"PO-Revision-Date: 2022-01-16 17:31+0000\n" -"Last-Translator: Alyssa Liddell \n" +"PO-Revision-Date: 2022-01-18 17:50+0000\n" +"Last-Translator: Nikolay Korotkiy \n" "Language-Team: Russian \n" "Language: ru\n" "MIME-Version: 1.0\n" @@ -2234,3 +2234,7 @@ msgstr "Редактировать страницу (Titan)" # Shows where a local file is using the File Manager. msgid "menu.reveal.filemgr" msgstr "Показать в файловом менеджере" + +# Menu heading shown when customizing navbar button actions. +msgid "menu.toolbar.setaction" +msgstr "Установить действие:" -- cgit v1.2.3 From d55a1ed06ad2f63be3dd86cdf51d588e2bf9d76d Mon Sep 17 00:00:00 2001 From: Wally Hackenslacker Date: Mon, 17 Jan 2022 13:24:10 +0000 Subject: Translated using Weblate (Spanish) Currently translated at 100.0% (646 of 646 strings) Translation: Lagrange/User Interface Translate-URL: http://weblate.skyjake.fi/projects/lagrange/ui/es/ --- po/es.po | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/po/es.po b/po/es.po index bf2c84de..b84622ab 100644 --- a/po/es.po +++ b/po/es.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" -"PO-Revision-Date: 2022-01-11 02:50+0000\n" +"PO-Revision-Date: 2022-01-18 17:50+0000\n" "Last-Translator: Wally Hackenslacker \n" "Language-Team: Spanish \n" "Language: es\n" @@ -2243,3 +2243,11 @@ msgstr "Cursor parpadeante:" msgid "keys.upload.edit" msgstr "Editar Página con Titan" + +# Menu heading shown when customizing navbar button actions. +msgid "menu.toolbar.setaction" +msgstr "Establecer Acción:" + +# Shows where a local file is using the File Manager. +msgid "menu.reveal.filemgr" +msgstr "Mostrar en Administrador de Archivos" -- cgit v1.2.3 From 4859ebb237e6d09a26b86157524a2c01b4454ba2 Mon Sep 17 00:00:00 2001 From: Страхиња Радић Date: Mon, 17 Jan 2022 17:05:59 +0000 Subject: Translated using Weblate (Serbian) Currently translated at 100.0% (646 of 646 strings) Translation: Lagrange/User Interface Translate-URL: http://weblate.skyjake.fi/projects/lagrange/ui/sr/ --- po/sr.po | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/po/sr.po b/po/sr.po index e6cef348..02b35316 100644 --- a/po/sr.po +++ b/po/sr.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" -"PO-Revision-Date: 2022-01-16 17:31+0000\n" +"PO-Revision-Date: 2022-01-18 17:50+0000\n" "Last-Translator: Страхиња Радић \n" "Language-Team: Serbian \n" "Language: sr\n" @@ -2281,3 +2281,7 @@ msgstr "Уређивање странице преко Титана" # Shows where a local file is using the File Manager. msgid "menu.reveal.filemgr" msgstr "Прикажи у Менаџеру датотека" + +# Menu heading shown when customizing navbar button actions. +msgid "menu.toolbar.setaction" +msgstr "Постави акцију:" -- cgit v1.2.3 From 98a26054e930eac7b165d27641a8d3f5fb5be048 Mon Sep 17 00:00:00 2001 From: Alyssa Liddell Date: Mon, 17 Jan 2022 11:32:37 +0000 Subject: Translated using Weblate (Ukrainian) Currently translated at 100.0% (646 of 646 strings) Translation: Lagrange/User Interface Translate-URL: http://weblate.skyjake.fi/projects/lagrange/ui/uk/ --- po/uk.po | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/po/uk.po b/po/uk.po index 1015097c..2d86f54d 100644 --- a/po/uk.po +++ b/po/uk.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" -"PO-Revision-Date: 2022-01-16 17:31+0000\n" +"PO-Revision-Date: 2022-01-18 17:50+0000\n" "Last-Translator: Alyssa Liddell \n" "Language-Team: Ukrainian " "\n" @@ -2262,3 +2262,7 @@ msgstr "токен — див. вказівки сервера" # Shows where a local file is using the File Manager. msgid "menu.reveal.filemgr" msgstr "Показати у файловому менеджері" + +# Menu heading shown when customizing navbar button actions. +msgid "menu.toolbar.setaction" +msgstr "Призначити дію:" -- cgit v1.2.3 From 11660029174f1874d5fff76026df4f5d00471b6c Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Wed, 19 Jan 2022 17:45:43 +0200 Subject: Event processing workaround (2.0.18+) This is most likely not needed for older SDL versions. IssueID #436 IssueID #438 --- src/app.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.c b/src/app.c index 4045610e..30221582 100644 --- a/src/app.c +++ b/src/app.c @@ -1268,7 +1268,7 @@ static iBool nextEvent_App_(iApp *d, enum iAppEventMode eventMode, SDL_Event *ev /* SDL regression circa 2.0.18? SDL_PollEvent() doesn't always return events posted immediately beforehand. Waiting with a very short timeout seems to work better. */ -#if defined (iPlatformLinux) +#if defined (iPlatformLinux) && SDL_VERSION_ATLEAST(2, 0, 18) return SDL_WaitEventTimeout(event, 1); #else return SDL_PollEvent(event); -- cgit v1.2.3 From f9fbd32e745e3c82152a964887f9d301a0954f1d Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 20 Jan 2022 06:03:05 +0200 Subject: DocumentWidget: Fixed crash when downloading a large file --- src/ui/documentwidget.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 6a535882..fdb55232 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -2756,6 +2756,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, } d->flags |= drawDownloadCounter_DocumentWidgetFlag; clear_PtrSet(d->view.invalidRuns); + documentRunsInvalidated_DocumentWidget_(d); deinit_String(&str); return; } -- cgit v1.2.3 From 9390d3ed4c684cd9749dd72f064d4217040a38b2 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 20 Jan 2022 06:04:34 +0200 Subject: Updated release notes --- res/about/version.gmi | 2 ++ 1 file changed, 2 insertions(+) diff --git a/res/about/version.gmi b/res/about/version.gmi index b2700598..0e6ec4a4 100644 --- a/res/about/version.gmi +++ b/res/about/version.gmi @@ -8,6 +8,8 @@ ## 1.10.1 * Fixed bottom actions of the Feeds sidebar getting hidden when all entries are read. This prevented switching between Unread/All filter modes. +* Fixed potential crash when downloading a large file (e.g., a fontpack). +* Linux: SDL event handling workaround adjusted to only apply to 2.0.18+. ## 1.10 New features: -- cgit v1.2.3 From 95c0646415b706d66d5fa61b4545d5d254026034 Mon Sep 17 00:00:00 2001 From: Tadeáš Erban Date: Wed, 19 Jan 2022 14:04:48 +0000 Subject: Translated using Weblate (Czech) Currently translated at 100.0% (646 of 646 strings) Translation: Lagrange/User Interface Translate-URL: http://weblate.skyjake.fi/projects/lagrange/ui/cs/ --- po/cs.po | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/po/cs.po b/po/cs.po index 034286fa..2b3eab30 100644 --- a/po/cs.po +++ b/po/cs.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" -"PO-Revision-Date: 2022-01-16 09:50+0000\n" +"PO-Revision-Date: 2022-01-20 04:05+0000\n" "Last-Translator: Tadeáš Erban \n" "Language-Team: Czech \n" "Language: cs\n" @@ -2240,3 +2240,11 @@ msgstr "Blikající kurzor:" msgid "keys.upload.edit" msgstr "Upravit pomocí Titanu" + +# Menu heading shown when customizing navbar button actions. +msgid "menu.toolbar.setaction" +msgstr "Nastavit akci:" + +# Shows where a local file is using the File Manager. +msgid "menu.reveal.filemgr" +msgstr "Otevřít v průzkumníku souborů" -- cgit v1.2.3 From 8de556928d509637996350fd8dd810ec557e86ec Mon Sep 17 00:00:00 2001 From: Emir SARI Date: Wed, 19 Jan 2022 15:50:43 +0000 Subject: Translated using Weblate (Turkish) Currently translated at 100.0% (646 of 646 strings) Translation: Lagrange/User Interface Translate-URL: http://weblate.skyjake.fi/projects/lagrange/ui/tr/ --- po/tr.po | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/po/tr.po b/po/tr.po index 5cff07d1..c0811109 100644 --- a/po/tr.po +++ b/po/tr.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" -"PO-Revision-Date: 2022-01-08 14:50+0000\n" +"PO-Revision-Date: 2022-01-20 04:05+0000\n" "Last-Translator: Emir SARI \n" "Language-Team: Turkish \n" "Language: tr\n" @@ -2182,3 +2182,11 @@ msgstr "jeton — sunucunun yönergelerine bakın" msgid "heading.prefs.toolbaractions" msgstr "Araç çubuğu eylemleri" + +# Menu heading shown when customizing navbar button actions. +msgid "menu.toolbar.setaction" +msgstr "Eylem Ayarla:" + +# Shows where a local file is using the File Manager. +msgid "menu.reveal.filemgr" +msgstr "Dosya Yöneticisinde Göster" -- cgit v1.2.3 From d60741bf51d9742e6d0f57835fbc36f726f8e4f1 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 20 Jan 2022 06:06:02 +0200 Subject: Updated language strings --- res/about/version.gmi | 1 + res/lang/cs.bin | Bin 32069 -> 32086 bytes res/lang/es.bin | Bin 30784 -> 30808 bytes res/lang/ru.bin | Bin 45641 -> 45668 bytes res/lang/sr.bin | Bin 44947 -> 44964 bytes res/lang/tr.bin | Bin 29902 -> 29912 bytes res/lang/uk.bin | Bin 45028 -> 45045 bytes 7 files changed, 1 insertion(+) diff --git a/res/about/version.gmi b/res/about/version.gmi index 0e6ec4a4..ea420916 100644 --- a/res/about/version.gmi +++ b/res/about/version.gmi @@ -10,6 +10,7 @@ * Fixed bottom actions of the Feeds sidebar getting hidden when all entries are read. This prevented switching between Unread/All filter modes. * Fixed potential crash when downloading a large file (e.g., a fontpack). * Linux: SDL event handling workaround adjusted to only apply to 2.0.18+. +* Updated UI translations. ## 1.10 New features: diff --git a/res/lang/cs.bin b/res/lang/cs.bin index eb59f65d..6624fbd4 100644 Binary files a/res/lang/cs.bin and b/res/lang/cs.bin differ diff --git a/res/lang/es.bin b/res/lang/es.bin index 14c1b843..09b7151b 100644 Binary files a/res/lang/es.bin and b/res/lang/es.bin differ diff --git a/res/lang/ru.bin b/res/lang/ru.bin index e434971b..6a9b8e67 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 43a6deda..a1e87e95 100644 Binary files a/res/lang/sr.bin and b/res/lang/sr.bin differ diff --git a/res/lang/tr.bin b/res/lang/tr.bin index 06fc22e5..981ced04 100644 Binary files a/res/lang/tr.bin and b/res/lang/tr.bin differ diff --git a/res/lang/uk.bin b/res/lang/uk.bin index 23329907..d5e78f6f 100644 Binary files a/res/lang/uk.bin and b/res/lang/uk.bin differ -- cgit v1.2.3 From 6f063e986bf572cc832d2a74e5b3f973cfb7722f Mon Sep 17 00:00:00 2001 From: Xosé M Date: Thu, 20 Jan 2022 05:29:26 +0000 Subject: Translated using Weblate (Galician) Currently translated at 100.0% (646 of 646 strings) Translation: Lagrange/User Interface Translate-URL: http://weblate.skyjake.fi/projects/lagrange/ui/gl/ --- po/gl.po | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/po/gl.po b/po/gl.po index 6d9f7e91..ac2ef700 100644 --- a/po/gl.po +++ b/po/gl.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" -"PO-Revision-Date: 2022-01-08 14:50+0000\n" +"PO-Revision-Date: 2022-01-20 06:20+0000\n" "Last-Translator: Xosé M. \n" "Language-Team: Galician " "\n" @@ -2233,3 +2233,11 @@ msgstr "Cursor intermitente:" msgid "keys.upload.edit" msgstr "Editar páxina con Titan" + +# Shows where a local file is using the File Manager. +msgid "menu.reveal.filemgr" +msgstr "Mostrar no Xestor de ficheiros" + +# Menu heading shown when customizing navbar button actions. +msgid "menu.toolbar.setaction" +msgstr "Establece acción:" -- cgit v1.2.3 From 33816278c84fd7ac7e895f4111229c4ff4436b53 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 20 Jan 2022 08:21:10 +0200 Subject: Updated language strings --- res/lang/gl.bin | Bin 29952 -> 29969 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/res/lang/gl.bin b/res/lang/gl.bin index f1501b4a..925994f9 100644 Binary files a/res/lang/gl.bin and b/res/lang/gl.bin differ -- cgit v1.2.3