diff --git a/res/about/version.gmi b/res/about/version.gmi index 3679a465..f3d40e76 100644 --- a/res/about/version.gmi +++ b/res/about/version.gmi @@ -7,6 +7,7 @@ # Release notes  ## 0.10 +* Added option to load inline images when pressing Space or ↓. If an image link is visible, the image will be loaded instead of scrolling. This option is disabled by default. * Added an option to use a proxy server for Gemini requests. * Added a keybinding to activate keyboard link navigation mode (default is "F"). * Clearing and resetting keybindings via a context menu. diff --git a/src/app.c b/src/app.c index f2741ee6..b53666c8 100644 --- a/src/app.c +++ b/src/app.c @@ -183,6 +183,7 @@ static iString *serializePrefs_App_(const iApp *d) { appendFormat_String(str, "prefs.mono.gopher.changed arg:%d\n", d->prefs.monospaceGopher); appendFormat_String(str, "zoom.set arg:%d\n", d->prefs.zoomPercent); appendFormat_String(str, "smoothscroll arg:%d\n", d->prefs.smoothScrolling); + appendFormat_String(str, "imageloadscroll arg:%d\n", d->prefs.loadImageInsteadOfScrolling); appendFormat_String(str, "linewidth.set arg:%d\n", d->prefs.lineWidth); appendFormat_String(str, "prefs.biglede.changed arg:%d\n", d->prefs.bigFirstParagraph); appendFormat_String(str, "prefs.sideicon.changed arg:%d\n", d->prefs.sideIcon); @@ -746,6 +747,8 @@ static iBool handlePrefsCommands_(iWidget *d, const char *cmd) { isSelected_Widget(findChild_Widget(d, "prefs.retainwindow"))); postCommandf_App("smoothscroll arg:%d", isSelected_Widget(findChild_Widget(d, "prefs.smoothscroll"))); + postCommandf_App("imageloadscroll arg:%d", + isSelected_Widget(findChild_Widget(d, "prefs.imageloadscroll"))); postCommandf_App("ostheme arg:%d", isSelected_Widget(findChild_Widget(d, "prefs.ostheme"))); postCommandf_App("proxy.gemini address:%s", @@ -956,6 +959,10 @@ iBool handleCommand_App(const char *cmd) { d->prefs.smoothScrolling = arg_Command(cmd); return iTrue; } + else if (equal_Command(cmd, "imageloadscroll")) { + d->prefs.loadImageInsteadOfScrolling = arg_Command(cmd); + return iTrue; + } else if (equal_Command(cmd, "forcewrap.toggle")) { d->prefs.forceLineWrap = !d->prefs.forceLineWrap; updateSize_DocumentWidget(document_App()); @@ -1153,6 +1160,7 @@ iBool handleCommand_App(const char *cmd) { setText_InputWidget(findChild_Widget(dlg, "prefs.downloads"), &d->prefs.downloadDir); setToggle_Widget(findChild_Widget(dlg, "prefs.hoveroutline"), d->prefs.hoverOutline); setToggle_Widget(findChild_Widget(dlg, "prefs.smoothscroll"), d->prefs.smoothScrolling); + setToggle_Widget(findChild_Widget(dlg, "prefs.imageloadscroll"), d->prefs.loadImageInsteadOfScrolling); setToggle_Widget(findChild_Widget(dlg, "prefs.ostheme"), d->prefs.useSystemTheme); setToggle_Widget(findChild_Widget(dlg, "prefs.retainwindow"), d->prefs.retainWindowSize); setText_InputWidget(findChild_Widget(dlg, "prefs.uiscale"), diff --git a/src/gmdocument.c b/src/gmdocument.c index 923c20c9..62a0ba55 100644 --- a/src/gmdocument.c +++ b/src/gmdocument.c @@ -1366,8 +1366,14 @@ enum iColorId linkColor_GmDocument(const iGmDocument *d, iGmLinkId linkId, enum }  iBool isMediaLink_GmDocument(const iGmDocument *d, iGmLinkId linkId) { - return (linkFlags_GmDocument(d, linkId) & - (imageFileExtension_GmLinkFlag | audioFileExtension_GmLinkFlag)) != 0; + const iString *dstUrl = absoluteUrl_String(&d->url, linkUrl_GmDocument(d, linkId)); + const iRangecc scheme = urlScheme_String(dstUrl); + if (equalCase_Rangecc(scheme, "gemini") || equalCase_Rangecc(scheme, "gopher") || + willUseProxy_App(scheme)) { + return (linkFlags_GmDocument(d, linkId) & + (imageFileExtension_GmLinkFlag | audioFileExtension_GmLinkFlag)) != 0; + } + return iFalse; }  const iString *title_GmDocument(const iGmDocument *d) { diff --git a/src/prefs.c b/src/prefs.c index 80b11c30..dc2bd601 100644 --- a/src/prefs.c +++ b/src/prefs.c @@ -24,21 +24,23 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */  void init_Prefs(iPrefs *d) { d->dialogTab = 0; - d->theme = dark_ColorTheme; d->useSystemTheme = iTrue; + d->theme = dark_ColorTheme; d->retainWindowSize = iTrue; + d->uiScale = 1.0f; /* default set elsewhere */ d->zoomPercent = 100; + d->sideIcon = iTrue; + d->hoverOutline = iFalse; d->smoothScrolling = iTrue; - d->forceLineWrap = iFalse; - d->quoteIcon = iTrue; + d->loadImageInsteadOfScrolling = iFalse; d->font = nunito_TextFont; d->headingFont = nunito_TextFont; d->monospaceGemini = iFalse; d->monospaceGopher = iFalse; d->lineWidth = 40; d->bigFirstParagraph = iTrue; - d->sideIcon = iTrue; - d->hoverOutline = iFalse; + d->forceLineWrap = iFalse; + d->quoteIcon = iTrue; d->docThemeDark = colorfulDark_GmDocumentTheme; d->docThemeLight = white_GmDocumentTheme; d->saturation = 1.0f; diff --git a/src/prefs.h b/src/prefs.h index 33ce8b41..a3993629 100644 --- a/src/prefs.h +++ b/src/prefs.h @@ -33,31 +33,37 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ iDeclareType(Prefs)  struct Impl_Prefs { - int dialogTab; - iBool retainWindowSize; - float uiScale; - int zoomPercent; - iBool smoothScrolling; - iBool useSystemTheme; + /* UI state */ + int dialogTab; + /* Window */ + iBool useSystemTheme; enum iColorTheme theme; - iString geminiProxy; - iString gopherProxy; - iString httpProxy; - iString downloadDir; - /* Content */ - enum iTextFont font; - enum iTextFont headingFont; - iBool monospaceGemini; - iBool monospaceGopher; - int lineWidth; - iBool bigFirstParagraph; - iBool forceLineWrap; - iBool quoteIcon; - iBool sideIcon; - iBool hoverOutline; + iBool retainWindowSize; + float uiScale; + int zoomPercent; + iBool sideIcon; + /* Behavior */ + iString downloadDir; + iBool hoverOutline; + iBool smoothScrolling; + iBool loadImageInsteadOfScrolling; + /* Network */ + iString geminiProxy; + iString gopherProxy; + iString httpProxy; + /* Style */ + enum iTextFont font; + enum iTextFont headingFont; + iBool monospaceGemini; + iBool monospaceGopher; + int lineWidth; + iBool bigFirstParagraph; + iBool forceLineWrap; + iBool quoteIcon; + /* Colors */ enum iGmDocumentTheme docThemeDark; enum iGmDocumentTheme docThemeLight; - float saturation; + float saturation; };  iDeclareTypeConstruction(Prefs) diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 59f5a92c..0fc969ba 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -1151,10 +1151,8 @@ static iMediaRequest *findMediaRequest_DocumentWidget_(const iDocumentWidget *d,  static iBool requestMedia_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId) { if (!findMediaRequest_DocumentWidget_(d, linkId)) { - pushBack_ObjectList( - d->media, - iClob(new_MediaRequest( - d, linkId, absoluteUrl_String(d->mod.url, linkUrl_GmDocument(d->doc, linkId))))); + const iString *imageUrl = absoluteUrl_String(d->mod.url, linkUrl_GmDocument(d->doc, linkId)); + pushBack_ObjectList(d->media, iClob(new_MediaRequest(d, linkId, imageUrl))); invalidate_DocumentWidget_(d); return iTrue; } @@ -1235,6 +1233,22 @@ static void allocVisBuffer_DocumentWidget_(const iDocumentWidget *d) { } }  +static iBool fetchNextUnfetchedImage_DocumentWidget_(iDocumentWidget *d) { + iConstForEach(PtrArray, i, &d->visibleLinks) { + const iGmRun *run = i.ptr; + if (run->linkId && !run->imageId && ~run->flags & decoration_GmRunFlag) { + const int linkFlags = linkFlags_GmDocument(d->doc, run->linkId); + if (isMediaLink_GmDocument(d->doc, run->linkId) && + ~linkFlags & content_GmLinkFlag && ~linkFlags & permanent_GmLinkFlag ) { + if (requestMedia_DocumentWidget_(d, run->linkId)) { + return iTrue; + } + } + } + } + return iFalse; +} + static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) { iWidget *w = as_Widget(d); if (equal_Command(cmd, "window.resized") || equal_Command(cmd, "font.changed")) { @@ -1572,13 +1586,16 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) return iTrue; } else if (equal_Command(cmd, "scroll.page") && document_App() == d) { - if (argLabel_Command(cmd, "repeat")) { - /* TODO: Adjust scroll animation to be linear during repeated scroll? */ + const int dir = arg_Command(cmd); + if (!argLabel_Command(cmd, "repeat") && prefs_App()->loadImageInsteadOfScrolling && + dir > 0) { + if (fetchNextUnfetchedImage_DocumentWidget_(d)) { + return iTrue; + } } smoothScroll_DocumentWidget_(d, - arg_Command(cmd) * - (0.5f * height_Rect(documentBounds_DocumentWidget_(d)) - - 0 * lineHeight_Text(paragraph_FontId)), + dir * (0.5f * height_Rect(documentBounds_DocumentWidget_(d)) - + 0 * lineHeight_Text(paragraph_FontId)), smoothDuration_DocumentWidget_); return iTrue; } @@ -1599,8 +1616,15 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) return iTrue; } else if (equal_Command(cmd, "scroll.step") && document_App() == d) { + const int dir = arg_Command(cmd); + if (!argLabel_Command(cmd, "repeat") && prefs_App()->loadImageInsteadOfScrolling && + dir > 0) { + if (fetchNextUnfetchedImage_DocumentWidget_(d)) { + return iTrue; + } + } smoothScroll_DocumentWidget_(d, - 3 * lineHeight_Text(paragraph_FontId) * arg_Command(cmd), + 3 * lineHeight_Text(paragraph_FontId) * dir, smoothDuration_DocumentWidget_); return iTrue; } diff --git a/src/ui/keys.c b/src/ui/keys.c index 5b88bfcf..ea874343 100644 --- a/src/ui/keys.c +++ b/src/ui/keys.c @@ -56,25 +56,46 @@ static void clear_Keys_(iKeys *d) { } }  +enum iBindFlag { + argRepeat_BindFlag = iBit(1), +}; + /* TODO: This indirection could be used for localization, although all UI strings would need to be similarly handled. */ -static const struct { int id; iMenuItem bind; } defaultBindings_[] = { - { 1, { "Jump to top", SDLK_HOME, 0, "scroll.top" } }, - { 2, { "Jump to bottom", SDLK_END, 0, "scroll.bottom" } }, - { 10, { "Scroll up", SDLK_UP, 0, "scroll.step arg:-1" } }, - { 11, { "Scroll down", SDLK_DOWN, 0, "scroll.step arg:1" } }, - { 20, { "Scroll up half a page", SDLK_PAGEUP, 0, "scroll.page arg:-1" } }, - { 21, { "Scroll down half a page", SDLK_PAGEDOWN, 0, "scroll.page arg:1" } }, - { 30, { "Go back", navigateBack_KeyShortcut, "navigate.back" } }, - { 31, { "Go forward", navigateForward_KeyShortcut, "navigate.forward" } }, - { 32, { "Go to parent directory", navigateParent_KeyShortcut, "navigate.parent" } }, - { 33, { "Go to site root", navigateRoot_KeyShortcut, "navigate.root" } }, - { 40, { "Open link via keyboard", 'f', 0, "document.linkkeys"} }, +static const struct { int id; iMenuItem bind; int flags; } defaultBindings_[] = { + { 1, { "Jump to top", SDLK_HOME, 0, "scroll.top" }, 0 }, + { 2, { "Jump to bottom", SDLK_END, 0, "scroll.bottom" }, 0 }, + { 10, { "Scroll up", SDLK_UP, 0, "scroll.step arg:-1" }, argRepeat_BindFlag }, + { 11, { "Scroll down", SDLK_DOWN, 0, "scroll.step arg:1" }, argRepeat_BindFlag }, + { 20, { "Scroll up half a page", SDLK_PAGEUP, 0, "scroll.page arg:-1" }, argRepeat_BindFlag }, + { 21, { "Scroll down half a page", SDLK_PAGEDOWN, 0, "scroll.page arg:1" }, argRepeat_BindFlag }, + { 30, { "Go back", navigateBack_KeyShortcut, "navigate.back" }, 0 }, + { 31, { "Go forward", navigateForward_KeyShortcut, "navigate.forward" }, 0 }, + { 32, { "Go to parent directory", navigateParent_KeyShortcut, "navigate.parent" }, 0 }, + { 33, { "Go to site root", navigateRoot_KeyShortcut, "navigate.root" }, 0 }, + { 40, { "Open link via keyboard", 'f', 0, "document.linkkeys" }, 0 }, /* The following cannot currently be changed (built-in duplicates). */ - { 1000, { NULL, SDLK_SPACE, KMOD_SHIFT, "scroll.page arg:-1" } }, - { 1001, { NULL, SDLK_SPACE, 0, "scroll.page arg:1" } }, + { 1000, { NULL, SDLK_SPACE, KMOD_SHIFT, "scroll.page arg:-1" }, argRepeat_BindFlag }, + { 1001, { NULL, SDLK_SPACE, 0, "scroll.page arg:1" }, argRepeat_BindFlag }, };  +static iBinding *findId_Keys_(iKeys *d, int id) { + iForEach(Array, i, &d->bindings) { + iBinding *bind = i.value; + if (bind->id == id) { + return bind; + } + } + return NULL; +} + +static void setFlags_Keys_(int id, int bindFlags) { + iBinding *bind = findId_Keys_(&keys_, id); + if (bind) { + bind->flags = bindFlags; + } +} + static void bindDefaults_(void) { iForIndices(i, defaultBindings_) { const int id = defaultBindings_[i].id; @@ -83,6 +104,7 @@ static void bindDefaults_(void) { if (bind.label) { setLabel_Keys(id, bind.label); } + setFlags_Keys_(id, defaultBindings_[i].flags); } }  @@ -95,16 +117,6 @@ static iBinding *find_Keys_(iKeys *d, int key, int mods) { return NULL; }  -static iBinding *findId_Keys_(iKeys *d, int id) { - iForEach(Array, i, &d->bindings) { - iBinding *bind = i.value; - if (bind->id == id) { - return bind; - } - } - return NULL; -} - static iBinding *findCommand_Keys_(iKeys *d, const char *command) { /* Note: O(n) */ iForEach(Array, i, &d->bindings) { @@ -259,7 +271,12 @@ iBool processEvent_Keys(const SDL_Event *ev) { if (ev->type == SDL_KEYDOWN) { const iBinding *bind = find_Keys_(d, ev->key.keysym.sym, keyMods_Sym(ev->key.keysym.mod)); if (bind) { - postCommandString_App(&bind->command); + if (ev->key.repeat && (bind->flags & argRepeat_BindFlag)) { + postCommandf_App("%s repeat:1", cstr_String(&bind->command)); + } + else { + postCommandString_App(&bind->command); + } return iTrue; } } diff --git a/src/ui/keys.h b/src/ui/keys.h index 0cd97e2a..1e676c59 100644 --- a/src/ui/keys.h +++ b/src/ui/keys.h @@ -52,6 +52,7 @@ iDeclareType(Binding)  struct Impl_Binding { int id; + int flags; int key; int mods; iString command; diff --git a/src/ui/util.c b/src/ui/util.c index 85d3562f..559c5381 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -1013,6 +1013,8 @@ iWidget *makePreferences_Widget(void) { addChild_Widget(values, iClob(makeToggle_Widget("prefs.hoveroutline"))); addChild_Widget(headings, iClob(makeHeading_Widget("Smooth scrolling:"))); addChild_Widget(values, iClob(makeToggle_Widget("prefs.smoothscroll"))); + addChild_Widget(headings, iClob(makeHeading_Widget("Load image on scroll:"))); + addChild_Widget(values, iClob(makeToggle_Widget("prefs.imageloadscroll"))); } /* Window. */ { appendTwoColumnPage_(tabs, "Window", '2', &headings, &values);