summaryrefslogtreecommitdiff
path: root/src/ui/documentwidget.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui/documentwidget.c')
-rw-r--r--src/ui/documentwidget.c429
1 files changed, 336 insertions, 93 deletions
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index 94337f8d..29e264e8 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -222,6 +222,7 @@ enum iDocumentWidgetFlag {
222 movingSelectMarkStart_DocumentWidgetFlag = iBit(10), 222 movingSelectMarkStart_DocumentWidgetFlag = iBit(10),
223 movingSelectMarkEnd_DocumentWidgetFlag = iBit(11), 223 movingSelectMarkEnd_DocumentWidgetFlag = iBit(11),
224 otherRootByDefault_DocumentWidgetFlag = iBit(12), /* links open to other root by default */ 224 otherRootByDefault_DocumentWidgetFlag = iBit(12), /* links open to other root by default */
225 urlChanged_DocumentWidgetFlag = iBit(13),
225}; 226};
226 227
227enum iDocumentLinkOrdinalMode { 228enum iDocumentLinkOrdinalMode {
@@ -231,15 +232,41 @@ enum iDocumentLinkOrdinalMode {
231 232
232struct Impl_DocumentWidget { 233struct Impl_DocumentWidget {
233 iWidget widget; 234 iWidget widget;
234 enum iRequestState state;
235 iPersistentDocumentState mod;
236 int flags; 235 int flags;
236
237 /* User interface: */
237 enum iDocumentLinkOrdinalMode ordinalMode; 238 enum iDocumentLinkOrdinalMode ordinalMode;
238 size_t ordinalBase; 239 size_t ordinalBase;
239 iString * titleUser; 240 iRangecc selectMark;
241 iRangecc initialSelectMark; /* for word/line selection */
242 iRangecc foundMark;
243 const iGmRun * grabbedPlayer; /* currently adjusting volume in a player */
244 float grabbedStartVolume;
245 int mediaTimer;
246 const iGmRun * hoverPre; /* for clicking */
247 const iGmRun * hoverAltPre; /* for drawing alt text */
248 const iGmRun * hoverLink;
249 const iGmRun * contextLink;
250 iClick click;
251 iInt2 contextPos; /* coordinates of latest right click */
252 int pinchZoomInitial;
253 int pinchZoomPosted;
254 iString pendingGotoHeading;
255
256 /* Network request: */
257 enum iRequestState state;
240 iGmRequest * request; 258 iGmRequest * request;
241 iAtomicInt isRequestUpdated; /* request has new content, need to parse it */ 259 iAtomicInt isRequestUpdated; /* request has new content, need to parse it */
242 iObjectList * media; 260 int certFlags;
261 iBlock * certFingerprint;
262 iDate certExpiry;
263 iString * certSubject;
264 int redirectCount;
265 iObjectList * media; /* inline media requests */
266
267 /* Document: */
268 iPersistentDocumentState mod;
269 iString * titleUser;
243 enum iGmStatusCode sourceStatus; 270 enum iGmStatusCode sourceStatus;
244 iString sourceHeader; 271 iString sourceHeader;
245 iString sourceMime; 272 iString sourceMime;
@@ -247,53 +274,36 @@ struct Impl_DocumentWidget {
247 iTime sourceTime; 274 iTime sourceTime;
248 iGempub * sourceGempub; /* NULL unless the page is Gempub content */ 275 iGempub * sourceGempub; /* NULL unless the page is Gempub content */
249 iGmDocument * doc; 276 iGmDocument * doc;
250 int certFlags; 277
251 iBlock * certFingerprint; 278 /* Rendering: */
252 iDate certExpiry;
253 iString * certSubject;
254 int redirectCount;
255 iRangecc selectMark;
256 iRangecc initialSelectMark; /* for word/line selection */
257 iRangecc foundMark;
258 int pageMargin; 279 int pageMargin;
280 float initNormScrollY;
281 iSmoothScroll scrollY;
282 iAnim sideOpacity;
283 iAnim altTextOpacity;
284 iGmRunRange visibleRuns;
259 iPtrArray visibleLinks; 285 iPtrArray visibleLinks;
260 iPtrArray visiblePre; 286 iPtrArray visiblePre;
287 iPtrArray visibleMedia; /* currently playing audio / ongoing downloads */
261 iPtrArray visibleWideRuns; /* scrollable blocks; TODO: merge into `visiblePre` */ 288 iPtrArray visibleWideRuns; /* scrollable blocks; TODO: merge into `visiblePre` */
262 iArray wideRunOffsets; 289 iArray wideRunOffsets;
263 iAnim animWideRunOffset; 290 iAnim animWideRunOffset;
264 uint16_t animWideRunId; 291 uint16_t animWideRunId;
265 iGmRunRange animWideRunRange; 292 iGmRunRange animWideRunRange;
266 iPtrArray visibleMedia; /* currently playing audio / ongoing downloads */ 293 iDrawBufs * drawBufs; /* dynamic state for drawing */
267 const iGmRun * grabbedPlayer; /* currently adjusting volume in a player */ 294 iVisBuf * visBuf;
268 float grabbedStartVolume; 295 iVisBufMeta * visBufMeta;
269 int mediaTimer;
270 const iGmRun * hoverPre; /* for clicking */
271 const iGmRun * hoverAltPre; /* for drawing alt text */
272 const iGmRun * hoverLink;
273 const iGmRun * contextLink;
274 iGmRunRange visibleRuns;
275 iGmRunRange renderRuns; 296 iGmRunRange renderRuns;
276 iClick click; 297 iPtrSet * invalidRuns;
277 iInt2 contextPos; /* coordinates of latest right click */ 298
278 iString pendingGotoHeading; 299 /* Widget structure: */
279 float initNormScrollY;
280// iAnim scrollY;
281// int overscroll;
282 iSmoothScroll scrollY;
283 iAnim sideOpacity;
284 iAnim altTextOpacity;
285 iScrollWidget *scroll; 300 iScrollWidget *scroll;
301 iWidget * footerButtons;
286 iWidget * menu; 302 iWidget * menu;
287 iWidget * playerMenu; 303 iWidget * playerMenu;
288 iWidget * copyMenu; 304 iWidget * copyMenu;
289 iVisBuf * visBuf;
290 iVisBufMeta * visBufMeta;
291 iPtrSet * invalidRuns;
292 iDrawBufs * drawBufs; /* dynamic state for drawing */
293 iTranslation * translation; 305 iTranslation * translation;
294 iWidget * phoneToolbar; 306 iWidget * phoneToolbar;
295 int pinchZoomInitial;
296 int pinchZoomPosted;
297}; 307};
298 308
299iDefineObjectConstruction(DocumentWidget) 309iDefineObjectConstruction(DocumentWidget)
@@ -308,6 +318,7 @@ void init_DocumentWidget(iDocumentWidget *d) {
308 init_PersistentDocumentState(&d->mod); 318 init_PersistentDocumentState(&d->mod);
309 d->flags = 0; 319 d->flags = 0;
310 d->phoneToolbar = NULL; 320 d->phoneToolbar = NULL;
321 d->footerButtons = NULL;
311 iZap(d->certExpiry); 322 iZap(d->certExpiry);
312 d->certFingerprint = new_Block(0); 323 d->certFingerprint = new_Block(0);
313 d->certFlags = 0; 324 d->certFlags = 0;
@@ -321,7 +332,6 @@ void init_DocumentWidget(iDocumentWidget *d) {
321 d->redirectCount = 0; 332 d->redirectCount = 0;
322 d->ordinalBase = 0; 333 d->ordinalBase = 0;
323 d->initNormScrollY = 0; 334 d->initNormScrollY = 0;
324 //init_Anim(&d->scrollY, 0);
325 init_SmoothScroll(&d->scrollY, w, scrollBegan_DocumentWidget_); 335 init_SmoothScroll(&d->scrollY, w, scrollBegan_DocumentWidget_);
326 d->animWideRunId = 0; 336 d->animWideRunId = 0;
327 init_Anim(&d->animWideRunOffset, 0); 337 init_Anim(&d->animWideRunOffset, 0);
@@ -573,7 +583,8 @@ static float normScrollPos_DocumentWidget_(const iDocumentWidget *d) {
573static int scrollMax_DocumentWidget_(const iDocumentWidget *d) { 583static int scrollMax_DocumentWidget_(const iDocumentWidget *d) {
574 const iWidget *w = constAs_Widget(d); 584 const iWidget *w = constAs_Widget(d);
575 int sm = size_GmDocument(d->doc).y - height_Rect(bounds_Widget(w)) + 585 int sm = size_GmDocument(d->doc).y - height_Rect(bounds_Widget(w)) +
576 (hasSiteBanner_GmDocument(d->doc) ? 1 : 2) * d->pageMargin * gap_UI; 586 (hasSiteBanner_GmDocument(d->doc) ? 1 : 2) * d->pageMargin * gap_UI +
587 height_Widget(d->footerButtons);
577 if (d->phoneToolbar) { 588 if (d->phoneToolbar) {
578 sm += size_Root(w->root).y - 589 sm += size_Root(w->root).y -
579 top_Rect(boundsWithoutVisualOffset_Widget(d->phoneToolbar)); 590 top_Rect(boundsWithoutVisualOffset_Widget(d->phoneToolbar));
@@ -809,6 +820,13 @@ static iRangecc currentHeading_DocumentWidget_(const iDocumentWidget *d) {
809 return heading; 820 return heading;
810} 821}
811 822
823static int updateScrollMax_DocumentWidget_(iDocumentWidget *d) {
824 arrange_Widget(d->footerButtons); /* scrollMax depends on footer height */
825 const int scrollMax = scrollMax_DocumentWidget_(d);
826 setMax_SmoothScroll(&d->scrollY, scrollMax);
827 return scrollMax;
828}
829
812static void updateVisible_DocumentWidget_(iDocumentWidget *d) { 830static void updateVisible_DocumentWidget_(iDocumentWidget *d) {
813 iChangeFlags(d->flags, 831 iChangeFlags(d->flags,
814 centerVertically_DocumentWidgetFlag, 832 centerVertically_DocumentWidgetFlag,
@@ -816,8 +834,26 @@ static void updateVisible_DocumentWidget_(iDocumentWidget *d) {
816 !isSuccess_GmStatusCode(d->sourceStatus)); 834 !isSuccess_GmStatusCode(d->sourceStatus));
817 const iRangei visRange = visibleRange_DocumentWidget_(d); 835 const iRangei visRange = visibleRange_DocumentWidget_(d);
818 const iRect bounds = bounds_Widget(as_Widget(d)); 836 const iRect bounds = bounds_Widget(as_Widget(d));
819 const int scrollMax = scrollMax_DocumentWidget_(d); 837 const int scrollMax = updateScrollMax_DocumentWidget_(d);
820 setMax_SmoothScroll(&d->scrollY, scrollMax); 838 /* Reposition the footer buttons as appropriate. */
839 /* TODO: You can just position `footerButtons` here completely without having to get
840 `Widget` involved with the offset in any way. */
841 if (d->footerButtons) {
842 const iRect bounds = bounds_Widget(as_Widget(d));
843 const iRect docBounds = documentBounds_DocumentWidget_(d);
844 const int hPad = (width_Rect(bounds) - iMin(120 * gap_UI, width_Rect(docBounds))) / 2;
845 const int vPad = 3 * gap_UI;
846 setPadding_Widget(d->footerButtons, hPad, vPad, hPad, vPad);
847 d->footerButtons->animOffsetRef = (scrollMax > 0 ? &d->scrollY.pos : NULL);
848 if (scrollMax <= 0) {
849 d->footerButtons->animOffsetRef = NULL;
850 d->footerButtons->rect.pos.y = height_Rect(bounds) - height_Widget(d->footerButtons);
851 }
852 else {
853 d->footerButtons->animOffsetRef = &d->scrollY.pos;
854 d->footerButtons->rect.pos.y = size_GmDocument(d->doc).y + 2 * gap_UI * d->pageMargin;
855 }
856 }
821 setRange_ScrollWidget(d->scroll, (iRangei){ 0, scrollMax }); 857 setRange_ScrollWidget(d->scroll, (iRangei){ 0, scrollMax });
822 const int docSize = size_GmDocument(d->doc).y; 858 const int docSize = size_GmDocument(d->doc).y;
823 setThumb_ScrollWidget(d->scroll, 859 setThumb_ScrollWidget(d->scroll,
@@ -1036,6 +1072,35 @@ static enum iGmDocumentBanner bannerType_DocumentWidget_(const iDocumentWidget *
1036 return siteDomain_GmDocumentBanner; 1072 return siteDomain_GmDocumentBanner;
1037} 1073}
1038 1074
1075static void makeFooterButtons_DocumentWidget_(iDocumentWidget *d, const iMenuItem *items, size_t count) {
1076 iWidget *w = as_Widget(d);
1077 destroy_Widget(d->footerButtons);
1078 d->footerButtons = NULL;
1079 if (count == 0) {
1080 return;
1081 }
1082 d->footerButtons = new_Widget();
1083 setFlags_Widget(d->footerButtons,
1084 unhittable_WidgetFlag | arrangeVertical_WidgetFlag |
1085 resizeWidthOfChildren_WidgetFlag | arrangeHeight_WidgetFlag |
1086 fixedPosition_WidgetFlag | resizeToParentWidth_WidgetFlag,
1087 iTrue);
1088 //setBackgroundColor_Widget(d->footerButtons, tmBackground_ColorId);
1089 for (size_t i = 0; i < count; ++i) {
1090 iLabelWidget *button = addChildFlags_Widget(
1091 d->footerButtons,
1092 iClob(newKeyMods_LabelWidget(
1093 items[i].label, items[i].key, items[i].kmods, items[i].command)),
1094 alignLeft_WidgetFlag | drawKey_WidgetFlag);
1095 checkIcon_LabelWidget(button);
1096 setFont_LabelWidget(button, uiContent_FontId);
1097 }
1098 addChild_Widget(as_Widget(d), iClob(d->footerButtons));
1099 arrange_Widget(d->footerButtons);
1100 arrange_Widget(w);
1101 updateVisible_DocumentWidget_(d); /* final placement for the buttons */
1102}
1103
1039static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode code, 1104static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode code,
1040 const iString *meta) { 1105 const iString *meta) {
1041 iString *src = collectNewCStr_String("# "); 1106 iString *src = collectNewCStr_String("# ");
@@ -1061,10 +1126,17 @@ static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode
1061 iString *key = collectNew_String(); 1126 iString *key = collectNew_String();
1062 toString_Sym(SDLK_s, KMOD_PRIMARY, key); 1127 toString_Sym(SDLK_s, KMOD_PRIMARY, key);
1063 appendFormat_String(src, "\n```\n%s\n```\n", cstr_String(meta)); 1128 appendFormat_String(src, "\n```\n%s\n```\n", cstr_String(meta));
1064 appendFormat_String(src, 1129// appendFormat_String(src,
1065 cstr_Lang("error.unsupported.suggestsave"), 1130// cstr_Lang("error.unsupported.suggestsave"),
1066 cstr_String(key), 1131// cstr_String(key),
1067 saveToDownloads_Label); 1132// saveToDownloads_Label);
1133 makeFooterButtons_DocumentWidget_(
1134 d,
1135 (iMenuItem[]){ { translateCStr_Lang(download_Icon " " saveToDownloads_Label),
1136 0,
1137 0,
1138 "document.save" } },
1139 1);
1068 break; 1140 break;
1069 } 1141 }
1070 case slowDown_GmStatusCode: 1142 case slowDown_GmStatusCode:
@@ -1072,9 +1144,19 @@ static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode
1072 cstr_String(meta)); 1144 cstr_String(meta));
1073 break; 1145 break;
1074 default: 1146 default:
1147 if (!isEmpty_String(meta)) {
1148 appendFormat_String(src, "\n\n${error.server.msg}\n> %s", cstr_String(meta));
1149 }
1075 break; 1150 break;
1076 } 1151 }
1077 } 1152 }
1153 if (category_GmStatusCode(code) == categoryClientCertificate_GmStatus) {
1154 makeFooterButtons_DocumentWidget_(
1155 d,
1156 (iMenuItem[]){ { leftHalf_Icon " ${menu.show.identities}", '4', KMOD_PRIMARY, "sidebar.mode arg:3 show:1" },
1157 { person_Icon " ${menu.identity.new}", newIdentity_KeyShortcut, "ident.new" } },
1158 2);
1159 }
1078 setBanner_GmDocument(d->doc, useBanner ? bannerType_DocumentWidget_(d) : none_GmDocumentBanner); 1160 setBanner_GmDocument(d->doc, useBanner ? bannerType_DocumentWidget_(d) : none_GmDocumentBanner);
1079 setFormat_GmDocument(d->doc, gemini_SourceFormat); 1161 setFormat_GmDocument(d->doc, gemini_SourceFormat);
1080 translate_Lang(src); 1162 translate_Lang(src);
@@ -1159,11 +1241,102 @@ static void postProcessRequestContent_DocumentWidget_(iDocumentWidget *d, iBool
1159 } 1241 }
1160 } 1242 }
1161 if (d->sourceGempub) { 1243 if (d->sourceGempub) {
1162 if (equal_String(d->mod.url, coverPageUrl_Gempub(d->sourceGempub)) && 1244 if (equal_String(d->mod.url, coverPageUrl_Gempub(d->sourceGempub))) {
1163 preloadCoverImage_Gempub(d->sourceGempub, d->doc)) { 1245 if (!isRemote_Gempub(d->sourceGempub)) {
1246 iArray *items = collectNew_Array(sizeof(iMenuItem));
1247 pushBack_Array(
1248 items,
1249 &(iMenuItem){ book_Icon " ${gempub.cover.view}",
1250 0,
1251 0,
1252 format_CStr("!open url:%s",
1253 cstr_String(indexPageUrl_Gempub(d->sourceGempub))) });
1254 if (navSize_Gempub(d->sourceGempub) > 0) {
1255 pushBack_Array(
1256 items,
1257 &(iMenuItem){
1258 format_CStr(forwardArrow_Icon " %s",
1259 cstr_String(navLinkLabel_Gempub(d->sourceGempub, 0))),
1260 SDLK_RIGHT,
1261 0,
1262 format_CStr("!open url:%s",
1263 cstr_String(navLinkUrl_Gempub(d->sourceGempub, 0))) });
1264 }
1265 makeFooterButtons_DocumentWidget_(d, constData_Array(items), size_Array(items));
1266 }
1267 else {
1268 makeFooterButtons_DocumentWidget_(
1269 d,
1270 (iMenuItem[]){ { book_Icon " ${menu.save.downloads.open}",
1271 SDLK_s,
1272 KMOD_PRIMARY | KMOD_SHIFT,
1273 "document.save open:1" },
1274 { download_Icon " " saveToDownloads_Label,
1275 SDLK_s,
1276 KMOD_PRIMARY,
1277 "document.save" } },
1278 2);
1279 }
1280 if (preloadCoverImage_Gempub(d->sourceGempub, d->doc)) {
1164 redoLayout_GmDocument(d->doc); 1281 redoLayout_GmDocument(d->doc);
1165 updateVisible_DocumentWidget_(d); 1282 updateVisible_DocumentWidget_(d);
1166 invalidate_DocumentWidget_(d); 1283 invalidate_DocumentWidget_(d);
1284 }
1285 }
1286 else if (equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {
1287 makeFooterButtons_DocumentWidget_(
1288 d,
1289 (iMenuItem[]){ { format_CStr(book_Icon " %s",
1290 cstr_String(property_Gempub(d->sourceGempub,
1291 title_GempubProperty))),
1292 SDLK_LEFT,
1293 0,
1294 format_CStr("!open url:%s",
1295 cstr_String(coverPageUrl_Gempub(d->sourceGempub))) } },
1296 1);
1297 }
1298 else {
1299 /* Navigation buttons. */
1300 iArray *items = collectNew_Array(sizeof(iMenuItem));
1301 const size_t navIndex = navIndex_Gempub(d->sourceGempub, d->mod.url);
1302 if (navIndex != iInvalidPos) {
1303 if (navIndex < navSize_Gempub(d->sourceGempub) - 1) {
1304 pushBack_Array(
1305 items,
1306 &(iMenuItem){
1307 format_CStr(forwardArrow_Icon " %s",
1308 cstr_String(navLinkLabel_Gempub(d->sourceGempub, navIndex + 1))),
1309 SDLK_RIGHT,
1310 0,
1311 format_CStr("!open url:%s",
1312 cstr_String(navLinkUrl_Gempub(d->sourceGempub, navIndex + 1))) });
1313 }
1314 if (navIndex > 0) {
1315 pushBack_Array(
1316 items,
1317 &(iMenuItem){
1318 format_CStr(backArrow_Icon " %s",
1319 cstr_String(navLinkLabel_Gempub(d->sourceGempub, navIndex - 1))),
1320 SDLK_LEFT,
1321 0,
1322 format_CStr("!open url:%s",
1323 cstr_String(navLinkUrl_Gempub(d->sourceGempub, navIndex - 1))) });
1324 }
1325 else if (!equalCase_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {
1326 pushBack_Array(
1327 items,
1328 &(iMenuItem){
1329 format_CStr(book_Icon " %s",
1330 cstr_String(property_Gempub(d->sourceGempub, title_GempubProperty))),
1331 SDLK_LEFT,
1332 0,
1333 format_CStr("!open url:%s",
1334 cstr_String(coverPageUrl_Gempub(d->sourceGempub))) });
1335 }
1336 }
1337 if (!isEmpty_Array(items)) {
1338 makeFooterButtons_DocumentWidget_(d, constData_Array(items), size_Array(items));
1339 }
1167 } 1340 }
1168 if (!isCached && prefs_App()->pinSplit && 1341 if (!isCached && prefs_App()->pinSplit &&
1169 equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) { 1342 equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {
@@ -1227,7 +1400,9 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d,
1227 setRange_String(&d->sourceMime, param); 1400 setRange_String(&d->sourceMime, param);
1228 } 1401 }
1229 else if (startsWith_Rangecc(param, "text/") || 1402 else if (startsWith_Rangecc(param, "text/") ||
1230 equal_Rangecc(param, "application/json")) { 1403 equal_Rangecc(param, "application/json") ||
1404 equal_Rangecc(param, "application/x-pem-file") ||
1405 equal_Rangecc(param, "application/pem-certificate-chain")) {
1231 docFormat = plainText_SourceFormat; 1406 docFormat = plainText_SourceFormat;
1232 setRange_String(&d->sourceMime, param); 1407 setRange_String(&d->sourceMime, param);
1233 } 1408 }
@@ -1400,44 +1575,58 @@ static void cacheDocumentGlyphs_DocumentWidget_(const iDocumentWidget *d) {
1400 } 1575 }
1401} 1576}
1402 1577
1578static void updateFromCachedResponse_DocumentWidget_(iDocumentWidget *d, float normScrollY,
1579 const iGmResponse *resp, iGmDocument *cachedDoc) {
1580 setLinkNumberMode_DocumentWidget_(d, iFalse);
1581 clear_ObjectList(d->media);
1582 delete_Gempub(d->sourceGempub);
1583 d->sourceGempub = NULL;
1584 iRelease(d->doc);
1585 destroy_Widget(d->footerButtons);
1586 d->footerButtons = NULL;
1587 d->doc = new_GmDocument();
1588 resetWideRuns_DocumentWidget_(d);
1589 d->state = fetching_RequestState;
1590 /* Do the fetch. */ {
1591 d->initNormScrollY = normScrollY;
1592 /* Use the cached response data. */
1593 updateTrust_DocumentWidget_(d, resp);
1594 d->sourceTime = resp->when;
1595 d->sourceStatus = success_GmStatusCode;
1596 format_String(&d->sourceHeader, cstr_Lang("pageinfo.header.cached"));
1597 set_Block(&d->sourceContent, &resp->body);
1598 updateDocument_DocumentWidget_(d, resp, cachedDoc, iTrue);
1599 setCachedDocument_History(d->mod.history, d->doc);
1600 }
1601 d->state = ready_RequestState;
1602 postProcessRequestContent_DocumentWidget_(d, iTrue);
1603 init_Anim(&d->altTextOpacity, 0);
1604 reset_SmoothScroll(&d->scrollY);
1605 init_Anim(&d->scrollY.pos, d->initNormScrollY * size_GmDocument(d->doc).y);
1606 updateSideOpacity_DocumentWidget_(d, iFalse);
1607 updateVisible_DocumentWidget_(d);
1608 moveSpan_SmoothScroll(&d->scrollY, 0, 0); /* clamp position to new max */
1609 cacheDocumentGlyphs_DocumentWidget_(d);
1610 d->drawBufs->flags |= updateTimestampBuf_DrawBufsFlag | updateSideBuf_DrawBufsFlag;
1611 d->flags &= ~urlChanged_DocumentWidgetFlag;
1612 postCommandf_Root(
1613 as_Widget(d)->root, "document.changed doc:%p url:%s", d, cstr_String(d->mod.url));
1614}
1615
1403static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) { 1616static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) {
1404 const iRecentUrl *recent = findUrl_History(d->mod.history, d->mod.url); 1617 const iRecentUrl *recent = findUrl_History(d->mod.history, withSpacesEncoded_String(d->mod.url));
1405 if (recent && recent->cachedResponse) { 1618 if (recent && recent->cachedResponse) {
1406 const iGmResponse *resp = recent->cachedResponse; 1619 updateFromCachedResponse_DocumentWidget_(
1407 clear_ObjectList(d->media); 1620 d, recent->normScrollY, recent->cachedResponse, recent->cachedDoc);
1408 delete_Gempub(d->sourceGempub);
1409 d->sourceGempub = NULL;
1410 iRelease(d->doc);
1411 d->doc = new_GmDocument();
1412 resetWideRuns_DocumentWidget_(d);
1413 d->state = fetching_RequestState;
1414 /* Do the fetch. */ {
1415 d->initNormScrollY = recent->normScrollY;
1416 /* Use the cached response data. */
1417 updateTrust_DocumentWidget_(d, resp);
1418 d->sourceTime = resp->when;
1419 d->sourceStatus = success_GmStatusCode;
1420 format_String(&d->sourceHeader, cstr_Lang("pageinfo.header.cached"));
1421 set_Block(&d->sourceContent, &resp->body);
1422 updateDocument_DocumentWidget_(d, resp, recent->cachedDoc, iTrue);
1423 setCachedDocument_History(d->mod.history, d->doc);
1424 }
1425 d->state = ready_RequestState;
1426 postProcessRequestContent_DocumentWidget_(d, iTrue);
1427 init_Anim(&d->altTextOpacity, 0);
1428 reset_SmoothScroll(&d->scrollY);
1429 init_Anim(&d->scrollY.pos, d->initNormScrollY * size_GmDocument(d->doc).y);
1430 updateSideOpacity_DocumentWidget_(d, iFalse);
1431 updateVisible_DocumentWidget_(d);
1432 moveSpan_SmoothScroll(&d->scrollY, 0, 0); /* clamp position to new max */
1433 cacheDocumentGlyphs_DocumentWidget_(d);
1434 d->drawBufs->flags |= updateTimestampBuf_DrawBufsFlag | updateSideBuf_DrawBufsFlag;
1435 postCommandf_Root(as_Widget(d)->root, "document.changed doc:%p url:%s", d, cstr_String(d->mod.url));
1436 return iTrue; 1621 return iTrue;
1437 } 1622 }
1438 else if (!isEmpty_String(d->mod.url)) { 1623 else if (!isEmpty_String(d->mod.url)) {
1439 fetch_DocumentWidget_(d); 1624 fetch_DocumentWidget_(d);
1440 } 1625 }
1626 if (recent) {
1627 /* Retain scroll position in refetched content as well. */
1628 d->initNormScrollY = recent->normScrollY;
1629 }
1441 return iFalse; 1630 return iFalse;
1442} 1631}
1443 1632
@@ -1674,11 +1863,16 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) {
1674 break; 1863 break;
1675 } 1864 }
1676 case categorySuccess_GmStatusCode: 1865 case categorySuccess_GmStatusCode:
1677 //reset_SmoothScroll(&d->scrollY); 1866 if (d->flags & urlChanged_DocumentWidgetFlag) {
1867 /* Keep scroll position when reloading the same page. */
1868 reset_SmoothScroll(&d->scrollY);
1869 }
1678 iRelease(d->doc); /* new content incoming */ 1870 iRelease(d->doc); /* new content incoming */
1679 d->doc = new_GmDocument(); 1871 d->doc = new_GmDocument();
1680 delete_Gempub(d->sourceGempub); 1872 delete_Gempub(d->sourceGempub);
1681 d->sourceGempub = NULL; 1873 d->sourceGempub = NULL;
1874 destroy_Widget(d->footerButtons);
1875 d->footerButtons = NULL;
1682 resetWideRuns_DocumentWidget_(d); 1876 resetWideRuns_DocumentWidget_(d);
1683 updateDocument_DocumentWidget_(d, resp, NULL, iTrue); 1877 updateDocument_DocumentWidget_(d, resp, NULL, iTrue);
1684 break; 1878 break;
@@ -1902,7 +2096,8 @@ static iBool fetchNextUnfetchedImage_DocumentWidget_(iDocumentWidget *d) {
1902 return iFalse; 2096 return iFalse;
1903} 2097}
1904 2098
1905static void saveToDownloads_(const iString *url, const iString *mime, const iBlock *content) { 2099static const iString *saveToDownloads_(const iString *url, const iString *mime, const iBlock *content,
2100 iBool showDialog) {
1906 const iString *savePath = downloadPathForUrl_App(url, mime); 2101 const iString *savePath = downloadPathForUrl_App(url, mime);
1907 /* Write the file. */ { 2102 /* Write the file. */ {
1908 iFile *f = new_File(savePath); 2103 iFile *f = new_File(savePath);
@@ -1914,17 +2109,22 @@ static void saveToDownloads_(const iString *url, const iString *mime, const iBlo
1914#if defined (iPlatformAppleMobile) 2109#if defined (iPlatformAppleMobile)
1915 exportDownloadedFile_iOS(savePath); 2110 exportDownloadedFile_iOS(savePath);
1916#else 2111#else
2112 if (showDialog) {
1917 const iMenuItem items[2] = { 2113 const iMenuItem items[2] = {
1918 { "${dlg.save.opendownload}", 0, 0, 2114 { "${dlg.save.opendownload}", 0, 0,
1919 format_CStr("!open url:%s", cstrCollect_String(makeFileUrl_String(savePath))) }, 2115 format_CStr("!open url:%s", cstrCollect_String(makeFileUrl_String(savePath))) },
1920 { "${dlg.message.ok}", 0, 0, "message.ok" }, 2116 { "${dlg.message.ok}", 0, 0, "message.ok" },
1921 }; 2117 };
1922 makeMessage_Widget(uiHeading_ColorEscape "${heading.save}", 2118 makeMessage_Widget(uiHeading_ColorEscape "${heading.save}",
1923 format_CStr("%s\n${dlg.save.size} %.3f %s", cstr_String(path_File(f)), 2119 format_CStr("%s\n${dlg.save.size} %.3f %s",
2120 cstr_String(path_File(f)),
1924 isMega ? size / 1.0e6f : (size / 1.0e3f), 2121 isMega ? size / 1.0e6f : (size / 1.0e3f),
1925 isMega ? "${mb}" : "${kb}"), 2122 isMega ? "${mb}" : "${kb}"),
1926 items, iElemCount(items)); 2123 items,
2124 iElemCount(items));
2125 }
1927#endif 2126#endif
2127 return savePath;
1928 } 2128 }
1929 else { 2129 else {
1930 makeSimpleMessage_Widget(uiTextCaution_ColorEscape "${heading.save.error}", 2130 makeSimpleMessage_Widget(uiTextCaution_ColorEscape "${heading.save.error}",
@@ -1932,6 +2132,7 @@ static void saveToDownloads_(const iString *url, const iString *mime, const iBlo
1932 } 2132 }
1933 iRelease(f); 2133 iRelease(f);
1934 } 2134 }
2135 return collectNew_String();
1935} 2136}
1936 2137
1937static void addAllLinks_(void *context, const iGmRun *run) { 2138static void addAllLinks_(void *context, const iGmRun *run) {
@@ -2061,6 +2262,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2061 const iBool keepCenter = equal_Command(cmd, "font.changed"); 2262 const iBool keepCenter = equal_Command(cmd, "font.changed");
2062 updateDocumentWidthRetainingScrollPosition_DocumentWidget_(d, keepCenter); 2263 updateDocumentWidthRetainingScrollPosition_DocumentWidget_(d, keepCenter);
2063 d->drawBufs->flags |= updateSideBuf_DrawBufsFlag; 2264 d->drawBufs->flags |= updateSideBuf_DrawBufsFlag;
2265 updateVisible_DocumentWidget_(d);
2064 invalidate_DocumentWidget_(d); 2266 invalidate_DocumentWidget_(d);
2065 dealloc_VisBuf(d->visBuf); 2267 dealloc_VisBuf(d->visBuf);
2066 updateWindowTitle_DocumentWidget_(d); 2268 updateWindowTitle_DocumentWidget_(d);
@@ -2214,7 +2416,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2214 if (!isEmpty_Array(items)) { 2416 if (!isEmpty_Array(items)) {
2215 pushBack_Array(items, &(iMenuItem){ "---", 0, 0, 0 }); 2417 pushBack_Array(items, &(iMenuItem){ "---", 0, 0, 0 });
2216 } 2418 }
2217 pushBack_Array(items, &(iMenuItem){ "${dismiss}", 0, 0, "message.ok" }); 2419 pushBack_Array(items, &(iMenuItem){ "${close}", 0, 0, "message.ok" });
2218 iWidget *dlg = makeQuestion_Widget(uiHeading_ColorEscape "${heading.pageinfo}", 2420 iWidget *dlg = makeQuestion_Widget(uiHeading_ColorEscape "${heading.pageinfo}",
2219 cstr_String(msg), 2421 cstr_String(msg),
2220 data_Array(items), 2422 data_Array(items),
@@ -2334,6 +2536,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2334 if (category_GmStatusCode(status_GmRequest(d->request)) == categorySuccess_GmStatusCode) { 2536 if (category_GmStatusCode(status_GmRequest(d->request)) == categorySuccess_GmStatusCode) {
2335 init_Anim(&d->scrollY.pos, d->initNormScrollY * size_GmDocument(d->doc).y); /* TODO: unless user already scrolled! */ 2537 init_Anim(&d->scrollY.pos, d->initNormScrollY * size_GmDocument(d->doc).y); /* TODO: unless user already scrolled! */
2336 } 2538 }
2539 d->flags &= ~urlChanged_DocumentWidgetFlag;
2337 d->state = ready_RequestState; 2540 d->state = ready_RequestState;
2338 postProcessRequestContent_DocumentWidget_(d, iFalse); 2541 postProcessRequestContent_DocumentWidget_(d, iFalse);
2339 /* The response may be cached. */ 2542 /* The response may be cached. */
@@ -2409,7 +2612,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2409 const iMediaRequest *media = findMediaRequest_DocumentWidget_(d, linkId); 2612 const iMediaRequest *media = findMediaRequest_DocumentWidget_(d, linkId);
2410 if (media) { 2613 if (media) {
2411 saveToDownloads_(url_GmRequest(media->req), meta_GmRequest(media->req), 2614 saveToDownloads_(url_GmRequest(media->req), meta_GmRequest(media->req),
2412 body_GmRequest(media->req)); 2615 body_GmRequest(media->req), iTrue);
2413 } 2616 }
2414 } 2617 }
2415 else if (equal_Command(cmd, "document.save") && document_App() == d) { 2618 else if (equal_Command(cmd, "document.save") && document_App() == d) {
@@ -2418,7 +2621,13 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2418 "${dlg.save.incomplete}"); 2621 "${dlg.save.incomplete}");
2419 } 2622 }
2420 else if (!isEmpty_Block(&d->sourceContent)) { 2623 else if (!isEmpty_Block(&d->sourceContent)) {
2421 saveToDownloads_(d->mod.url, &d->sourceMime, &d->sourceContent); 2624 const iBool doOpen = argLabel_Command(cmd, "open");
2625 const iString *savePath = saveToDownloads_(d->mod.url, &d->sourceMime,
2626 &d->sourceContent, !doOpen);
2627 if (!isEmpty_String(savePath) && doOpen) {
2628 postCommandf_Root(
2629 w->root, "!open url:%s", cstrCollect_String(makeFileUrl_String(savePath)));
2630 }
2422 } 2631 }
2423 return iTrue; 2632 return iTrue;
2424 } 2633 }
@@ -2528,6 +2737,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2528 return iTrue; 2737 return iTrue;
2529 } 2738 }
2530 else if (equal_Command(cmd, "scroll.bottom") && document_App() == d) { 2739 else if (equal_Command(cmd, "scroll.bottom") && document_App() == d) {
2740 updateScrollMax_DocumentWidget_(d); /* scrollY.max might not be fully updated */
2531 init_Anim(&d->scrollY.pos, d->scrollY.max); 2741 init_Anim(&d->scrollY.pos, d->scrollY.max);
2532 invalidate_VisBuf(d->visBuf); 2742 invalidate_VisBuf(d->visBuf);
2533 clampScroll_DocumentWidget_(d); 2743 clampScroll_DocumentWidget_(d);
@@ -2835,7 +3045,7 @@ static size_t linkOrdinalFromKey_DocumentWidget_(const iDocumentWidget *d, int k
2835static iChar linkOrdinalChar_DocumentWidget_(const iDocumentWidget *d, size_t ord) { 3045static iChar linkOrdinalChar_DocumentWidget_(const iDocumentWidget *d, size_t ord) {
2836 if (d->ordinalMode == numbersAndAlphabet_DocumentLinkOrdinalMode) { 3046 if (d->ordinalMode == numbersAndAlphabet_DocumentLinkOrdinalMode) {
2837 if (ord < 9) { 3047 if (ord < 9) {
2838 return 0x278a + ord; 3048 return '1' + ord;
2839 } 3049 }
2840#if defined (iPlatformApple) 3050#if defined (iPlatformApple)
2841 if (ord < 9 + 22) { 3051 if (ord < 9 + 22) {
@@ -2844,17 +3054,17 @@ static iChar linkOrdinalChar_DocumentWidget_(const iDocumentWidget *d, size_t or
2844 if (key >= 'm') key++; 3054 if (key >= 'm') key++;
2845 if (key >= 'q') key++; 3055 if (key >= 'q') key++;
2846 if (key >= 'w') key++; 3056 if (key >= 'w') key++;
2847 return 0x24b6 + key - 'a'; 3057 return 'A' + key - 'a';
2848 } 3058 }
2849#else 3059#else
2850 if (ord < 9 + 26) { 3060 if (ord < 9 + 26) {
2851 return 0x24b6 + ord - 9; 3061 return 'A' + ord - 9;
2852 } 3062 }
2853#endif 3063#endif
2854 } 3064 }
2855 else { 3065 else {
2856 if (ord < iElemCount(homeRowKeys_)) { 3066 if (ord < iElemCount(homeRowKeys_)) {
2857 return 0x24b6 + homeRowKeys_[ord] - 'a'; 3067 return 'A' + homeRowKeys_[ord] - 'a';
2858 } 3068 }
2859 } 3069 }
2860 return 0; 3070 return 0;
@@ -3041,6 +3251,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3041 iArray items; 3251 iArray items;
3042 init_Array(&items, sizeof(iMenuItem)); 3252 init_Array(&items, sizeof(iMenuItem));
3043 if (d->contextLink) { 3253 if (d->contextLink) {
3254 /* Context menu for a link. */
3044 const iString *linkUrl = linkUrl_GmDocument(d->doc, d->contextLink->linkId); 3255 const iString *linkUrl = linkUrl_GmDocument(d->doc, d->contextLink->linkId);
3045// const int linkFlags = linkFlags_GmDocument(d->doc, d->contextLink->linkId); 3256// const int linkFlags = linkFlags_GmDocument(d->doc, d->contextLink->linkId);
3046 const iRangecc scheme = urlScheme_String(linkUrl); 3257 const iRangecc scheme = urlScheme_String(linkUrl);
@@ -3675,6 +3886,7 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
3675 /* Preformatted runs can be scrolled. */ 3886 /* Preformatted runs can be scrolled. */
3676 runOffset_DocumentWidget_(d->widget, run)); 3887 runOffset_DocumentWidget_(d->widget, run));
3677 const iRect visRect = { visPos, run->visBounds.size }; 3888 const iRect visRect = { visPos, run->visBounds.size };
3889#if 0
3678 if (run->flags & footer_GmRunFlag) { 3890 if (run->flags & footer_GmRunFlag) {
3679 iRect footerBack = 3891 iRect footerBack =
3680 (iRect){ visPos, init_I2(width_Rect(d->widgetBounds), run->visBounds.size.y) }; 3892 (iRect){ visPos, init_I2(width_Rect(d->widgetBounds), run->visBounds.size.y) };
@@ -3682,6 +3894,7 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
3682 fillRect_Paint(&d->paint, footerBack, tmBackground_ColorId); 3894 fillRect_Paint(&d->paint, footerBack, tmBackground_ColorId);
3683 return; 3895 return;
3684 } 3896 }
3897#endif
3685 /* Fill the background. */ { 3898 /* Fill the background. */ {
3686 if (run->linkId && linkFlags & isOpen_GmLinkFlag) { 3899 if (run->linkId && linkFlags & isOpen_GmLinkFlag) {
3687 /* Open links get a highlighted background. */ 3900 /* Open links get a highlighted background. */
@@ -3743,10 +3956,19 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
3743 const iChar ordChar = 3956 const iChar ordChar =
3744 linkOrdinalChar_DocumentWidget_(d->widget, ord - d->widget->ordinalBase); 3957 linkOrdinalChar_DocumentWidget_(d->widget, ord - d->widget->ordinalBase);
3745 if (ordChar) { 3958 if (ordChar) {
3746 drawString_Text(run->font, 3959 const char *circle = "\u25ef"; /* Large Circle */
3747 init_I2(d->viewPos.x - gap_UI / 3, visPos.y), 3960 iRect nbArea = { init_I2(d->viewPos.x - gap_UI / 3, visPos.y),
3961 init_I2(3.95f * gap_Text, 1.0f * lineHeight_Text(run->font)) };
3962 drawRange_Text(
3963 run->font, topLeft_Rect(nbArea), tmQuote_ColorId, range_CStr(circle));
3964 iRect circleArea = visualBounds_Text(run->font, range_CStr(circle));
3965 addv_I2(&circleArea.pos, topLeft_Rect(nbArea));
3966 drawCentered_Text(defaultContentSmall_FontId,
3967 circleArea,
3968 iTrue,
3748 tmQuote_ColorId, 3969 tmQuote_ColorId,
3749 collect_String(newUnicodeN_String(&ordChar, 1))); 3970 "%lc",
3971 (int) ordChar);
3750 goto runDrawn; 3972 goto runDrawn;
3751 } 3973 }
3752 } 3974 }
@@ -4424,9 +4646,16 @@ void deserializeState_DocumentWidget(iDocumentWidget *d, iStream *ins) {
4424 updateFromHistory_DocumentWidget_(d); 4646 updateFromHistory_DocumentWidget_(d);
4425} 4647}
4426 4648
4649static void setUrl_DocumentWidget_(iDocumentWidget *d, const iString *url) {
4650 if (!equal_String(d->mod.url, url)) {
4651 d->flags |= urlChanged_DocumentWidgetFlag;
4652 set_String(d->mod.url, url);
4653}
4654}
4655
4427void setUrlFromCache_DocumentWidget(iDocumentWidget *d, const iString *url, iBool isFromCache) { 4656void setUrlFromCache_DocumentWidget(iDocumentWidget *d, const iString *url, iBool isFromCache) {
4428 setLinkNumberMode_DocumentWidget_(d, iFalse); 4657 setLinkNumberMode_DocumentWidget_(d, iFalse);
4429 set_String(d->mod.url, urlFragmentStripped_String(url)); 4658 setUrl_DocumentWidget_(d, urlFragmentStripped_String(url));
4430 /* See if there a username in the URL. */ 4659 /* See if there a username in the URL. */
4431 parseUser_DocumentWidget_(d); 4660 parseUser_DocumentWidget_(d);
4432 if (!isFromCache || !updateFromHistory_DocumentWidget_(d)) { 4661 if (!isFromCache || !updateFromHistory_DocumentWidget_(d)) {
@@ -4434,6 +4663,20 @@ void setUrlFromCache_DocumentWidget(iDocumentWidget *d, const iString *url, iBoo
4434 } 4663 }
4435} 4664}
4436 4665
4666void setUrlAndSource_DocumentWidget(iDocumentWidget *d, const iString *url, const iString *mime,
4667 const iBlock *source) {
4668 setLinkNumberMode_DocumentWidget_(d, iFalse);
4669 setUrl_DocumentWidget_(d, url);
4670 parseUser_DocumentWidget_(d);
4671 iGmResponse *resp = new_GmResponse();
4672 resp->statusCode = success_GmStatusCode;
4673 initCurrent_Time(&resp->when);
4674 set_String(&resp->meta, mime);
4675 set_Block(&resp->body, source);
4676 updateFromCachedResponse_DocumentWidget_(d, 0, resp);
4677 delete_GmResponse(resp);
4678}
4679
4437iDocumentWidget *duplicate_DocumentWidget(const iDocumentWidget *orig) { 4680iDocumentWidget *duplicate_DocumentWidget(const iDocumentWidget *orig) {
4438 iDocumentWidget *d = new_DocumentWidget(); 4681 iDocumentWidget *d = new_DocumentWidget();
4439 delete_History(d->mod.history); 4682 delete_History(d->mod.history);