diff options
author | Jaakko Keränen <jaakko.keranen@iki.fi> | 2022-01-20 12:06:39 +0200 |
---|---|---|
committer | Jaakko Keränen <jaakko.keranen@iki.fi> | 2022-01-20 12:06:39 +0200 |
commit | 6da3bdb612e0ead07ff93f4f6dc5aef46c7b9c9e (patch) | |
tree | 4378984d744dc67b7453573524d5839b9ce7f9d5 /src | |
parent | d5169339b3454c80a6f2ed5f8cb937e5d5613fc0 (diff) | |
parent | 33816278c84fd7ac7e895f4111229c4ff4436b53 (diff) |
Merge branch 'dev' of skyjake.fi:gemini/lagrange into dev
Diffstat (limited to 'src')
77 files changed, 7543 insertions, 3550 deletions
@@ -55,6 +55,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
55 | #include <the_Foundation/path.h> | 55 | #include <the_Foundation/path.h> |
56 | #include <the_Foundation/process.h> | 56 | #include <the_Foundation/process.h> |
57 | #include <the_Foundation/sortedarray.h> | 57 | #include <the_Foundation/sortedarray.h> |
58 | #include <the_Foundation/stringset.h> | ||
58 | #include <the_Foundation/time.h> | 59 | #include <the_Foundation/time.h> |
59 | #include <the_Foundation/version.h> | 60 | #include <the_Foundation/version.h> |
60 | #include <SDL.h> | 61 | #include <SDL.h> |
@@ -71,6 +72,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
71 | #if defined (iPlatformAppleMobile) | 72 | #if defined (iPlatformAppleMobile) |
72 | # include "ios.h" | 73 | # include "ios.h" |
73 | #endif | 74 | #endif |
75 | #if defined (iPlatformAndroidMobile) | ||
76 | #include <SDL_log.h> | ||
77 | #endif | ||
74 | #if defined (iPlatformMsys) | 78 | #if defined (iPlatformMsys) |
75 | # include "win32.h" | 79 | # include "win32.h" |
76 | #endif | 80 | #endif |
@@ -108,6 +112,7 @@ static const char *defaultDataDir_App_ = "~/config/settings/lagrange"; | |||
108 | static const char *prefsFileName_App_ = "prefs.cfg"; | 112 | static const char *prefsFileName_App_ = "prefs.cfg"; |
109 | static const char *oldStateFileName_App_ = "state.binary"; | 113 | static const char *oldStateFileName_App_ = "state.binary"; |
110 | static const char *stateFileName_App_ = "state.lgr"; | 114 | static const char *stateFileName_App_ = "state.lgr"; |
115 | static const char *tempStateFileName_App_ = "state.lgr.tmp"; | ||
111 | static const char *defaultDownloadDir_App_ = "~/Downloads"; | 116 | static const char *defaultDownloadDir_App_ = "~/Downloads"; |
112 | 117 | ||
113 | static const int idleThreshold_App_ = 1000; /* ms */ | 118 | static const int idleThreshold_App_ = 1000; /* ms */ |
@@ -115,6 +120,7 @@ static const int idleThreshold_App_ = 1000; /* ms */ | |||
115 | struct Impl_App { | 120 | struct Impl_App { |
116 | iCommandLine args; | 121 | iCommandLine args; |
117 | iString * execPath; | 122 | iString * execPath; |
123 | iStringSet * tempFilesPendingDeletion; | ||
118 | iMimeHooks * mimehooks; | 124 | iMimeHooks * mimehooks; |
119 | iGmCerts * certs; | 125 | iGmCerts * certs; |
120 | iVisited * visited; | 126 | iVisited * visited; |
@@ -127,6 +133,7 @@ struct Impl_App { | |||
127 | iBool isRunning; | 133 | iBool isRunning; |
128 | iBool isRunningUnderWindowSystem; | 134 | iBool isRunningUnderWindowSystem; |
129 | iBool isDarkSystemTheme; | 135 | iBool isDarkSystemTheme; |
136 | iBool isSuspended; | ||
130 | #if defined (LAGRANGE_ENABLE_IDLE_SLEEP) | 137 | #if defined (LAGRANGE_ENABLE_IDLE_SLEEP) |
131 | iBool isIdling; | 138 | iBool isIdling; |
132 | uint32_t lastEventTime; | 139 | uint32_t lastEventTime; |
@@ -164,7 +171,11 @@ struct Impl_Ticker { | |||
164 | 171 | ||
165 | static int cmp_Ticker_(const void *a, const void *b) { | 172 | static int cmp_Ticker_(const void *a, const void *b) { |
166 | const iTicker *elems[2] = { a, b }; | 173 | const iTicker *elems[2] = { a, b }; |
167 | return iCmp(elems[0]->context, elems[1]->context); | 174 | const int cmp = iCmp(elems[0]->context, elems[1]->context); |
175 | if (cmp) { | ||
176 | return cmp; | ||
177 | } | ||
178 | return iCmp((void *) elems[0]->callback, (void *) elems[1]->callback); | ||
168 | } | 179 | } |
169 | 180 | ||
170 | /*----------------------------------------------------------------------------------------------*/ | 181 | /*----------------------------------------------------------------------------------------------*/ |
@@ -237,6 +248,13 @@ static iString *serializePrefs_App_(const iApp *d) { | |||
237 | appendFormat_String(str, "linewidth.set arg:%d\n", d->prefs.lineWidth); | 248 | appendFormat_String(str, "linewidth.set arg:%d\n", d->prefs.lineWidth); |
238 | appendFormat_String(str, "linespacing.set arg:%f\n", d->prefs.lineSpacing); | 249 | appendFormat_String(str, "linespacing.set arg:%f\n", d->prefs.lineSpacing); |
239 | appendFormat_String(str, "returnkey.set arg:%d\n", d->prefs.returnKey); | 250 | appendFormat_String(str, "returnkey.set arg:%d\n", d->prefs.returnKey); |
251 | for (size_t i = 0; i < iElemCount(d->prefs.navbarActions); i++) { | ||
252 | appendFormat_String(str, "navbar.action.set arg:%d button:%d\n", d->prefs.navbarActions[i], i); | ||
253 | } | ||
254 | #if defined (iPlatformMobile) | ||
255 | appendFormat_String(str, "toolbar.action.set arg:%d button:0\n", d->prefs.toolbarActions[0]); | ||
256 | appendFormat_String(str, "toolbar.action.set arg:%d button:1\n", d->prefs.toolbarActions[1]); | ||
257 | #endif | ||
240 | iConstForEach(StringSet, fp, d->prefs.disabledFontPacks) { | 258 | iConstForEach(StringSet, fp, d->prefs.disabledFontPacks) { |
241 | appendFormat_String(str, "fontpack.disable id:%s\n", cstr_String(fp.value)); | 259 | appendFormat_String(str, "fontpack.disable id:%s\n", cstr_String(fp.value)); |
242 | } | 260 | } |
@@ -263,6 +281,7 @@ static iString *serializePrefs_App_(const iApp *d) { | |||
263 | { "prefs.bookmarks.addbottom", &d->prefs.addBookmarksToBottom }, | 281 | { "prefs.bookmarks.addbottom", &d->prefs.addBookmarksToBottom }, |
264 | { "prefs.archive.openindex", &d->prefs.openArchiveIndexPages }, | 282 | { "prefs.archive.openindex", &d->prefs.openArchiveIndexPages }, |
265 | { "prefs.font.warnmissing", &d->prefs.warnAboutMissingGlyphs }, | 283 | { "prefs.font.warnmissing", &d->prefs.warnAboutMissingGlyphs }, |
284 | { "prefs.blink", &d->prefs.blinkingCursor }, | ||
266 | }; | 285 | }; |
267 | iForIndices(i, boolPrefs) { | 286 | iForIndices(i, boolPrefs) { |
268 | appendFormat_String(str, "%s.changed arg:%d\n", boolPrefs[i].id, *boolPrefs[i].value); | 287 | appendFormat_String(str, "%s.changed arg:%d\n", boolPrefs[i].id, *boolPrefs[i].value); |
@@ -313,6 +332,9 @@ static const char *dataDir_App_(void) { | |||
313 | } | 332 | } |
314 | 333 | ||
315 | static const char *downloadDir_App_(void) { | 334 | static const char *downloadDir_App_(void) { |
335 | #if defined (iPlatformAndroidMobile) | ||
336 | return concatPath_CStr(SDL_AndroidGetInternalStoragePath(), "Downloads"); | ||
337 | #endif | ||
316 | #if defined (iPlatformLinux) || defined (iPlatformOther) | 338 | #if defined (iPlatformLinux) || defined (iPlatformOther) |
317 | /* Parse user-dirs.dirs using the `xdg-user-dir` tool. */ | 339 | /* Parse user-dirs.dirs using the `xdg-user-dir` tool. */ |
318 | iProcess *proc = iClob(new_Process()); | 340 | iProcess *proc = iClob(new_Process()); |
@@ -363,7 +385,7 @@ static void loadPrefs_App_(iApp *d) { | |||
363 | setUiScale_Window(get_Window(), argf_Command(cmd)); | 385 | setUiScale_Window(get_Window(), argf_Command(cmd)); |
364 | } | 386 | } |
365 | else if (equal_Command(cmd, "uilang")) { | 387 | else if (equal_Command(cmd, "uilang")) { |
366 | const char *id = cstr_Rangecc(range_Command(cmd, "id")); | 388 | const char *id = cstr_Command(cmd, "id"); |
367 | setCStr_String(&d->prefs.strings[uiLanguage_PrefsString], id); | 389 | setCStr_String(&d->prefs.strings[uiLanguage_PrefsString], id); |
368 | setCurrent_Lang(id); | 390 | setCurrent_Lang(id); |
369 | } | 391 | } |
@@ -385,6 +407,12 @@ static void loadPrefs_App_(iApp *d) { | |||
385 | insert_StringSet(d->prefs.disabledFontPacks, | 407 | insert_StringSet(d->prefs.disabledFontPacks, |
386 | collect_String(suffix_Command(cmd, "id"))); | 408 | collect_String(suffix_Command(cmd, "id"))); |
387 | } | 409 | } |
410 | #if defined (iPlatformAndroidMobile) | ||
411 | else if (equal_Command(cmd, "returnkey.set")) { | ||
412 | /* Hardcoded to avoid accidental presses of the virtual Return key. */ | ||
413 | d->prefs.returnKey = default_ReturnKeyBehavior; | ||
414 | } | ||
415 | #endif | ||
388 | #if !defined (LAGRANGE_ENABLE_DOWNLOAD_EDIT) | 416 | #if !defined (LAGRANGE_ENABLE_DOWNLOAD_EDIT) |
389 | else if (equal_Command(cmd, "downloads")) { | 417 | else if (equal_Command(cmd, "downloads")) { |
390 | continue; /* can't change downloads directory */ | 418 | continue; /* can't change downloads directory */ |
@@ -410,11 +438,13 @@ static void loadPrefs_App_(iApp *d) { | |||
410 | iRelease(f); | 438 | iRelease(f); |
411 | /* Upgrade checks. */ | 439 | /* Upgrade checks. */ |
412 | if (cmp_Version(&upgradedFromAppVersion, &(iVersion){ 1, 8, 0 }) < 0) { | 440 | if (cmp_Version(&upgradedFromAppVersion, &(iVersion){ 1, 8, 0 }) < 0) { |
441 | #if !defined (iPlatformAppleMobile) && !defined (iPlatformAndroidMobile) | ||
413 | /* When upgrading to v1.8.0, the old hardcoded font library is gone and that means | 442 | /* When upgrading to v1.8.0, the old hardcoded font library is gone and that means |
414 | UI strings may not have the right fonts available for the UI to remain | 443 | UI strings may not have the right fonts available for the UI to remain |
415 | usable. */ | 444 | usable. */ |
416 | postCommandf_App("uilang id:en"); | 445 | postCommandf_App("uilang id:en"); |
417 | postCommand_App("~fontpack.suggest.classic"); | 446 | postCommand_App("~fontpack.suggest.classic"); |
447 | #endif | ||
418 | } | 448 | } |
419 | #if !defined (LAGRANGE_ENABLE_CUSTOM_FRAME) | 449 | #if !defined (LAGRANGE_ENABLE_CUSTOM_FRAME) |
420 | d->prefs.customFrame = iFalse; | 450 | d->prefs.customFrame = iFalse; |
@@ -505,7 +535,7 @@ static iBool loadState_App_(iApp *d) { | |||
505 | if (flags & 8) { | 535 | if (flags & 8) { |
506 | postCommand_Widget(sidebar2, "feeds.mode arg:%d", unread_FeedsMode); | 536 | postCommand_Widget(sidebar2, "feeds.mode arg:%d", unread_FeedsMode); |
507 | } | 537 | } |
508 | if (deviceType_App() != phone_AppDeviceType) { | 538 | if (deviceType_App() == desktop_AppDeviceType) { |
509 | setWidth_SidebarWidget(sidebar, widths[0]); | 539 | setWidth_SidebarWidget(sidebar, widths[0]); |
510 | setWidth_SidebarWidget(sidebar2, widths[1]); | 540 | setWidth_SidebarWidget(sidebar2, widths[1]); |
511 | if (flags & 1) postCommand_Root(root, "sidebar.toggle noanim:1"); | 541 | if (flags & 1) postCommand_Root(root, "sidebar.toggle noanim:1"); |
@@ -561,7 +591,7 @@ static void saveState_App_(const iApp *d) { | |||
561 | navigation history, cached content) and depends closely on the widget | 591 | navigation history, cached content) and depends closely on the widget |
562 | tree. The data is largely not reorderable and should not be modified | 592 | tree. The data is largely not reorderable and should not be modified |
563 | by the user manually. */ | 593 | by the user manually. */ |
564 | iFile *f = newCStr_File(concatPath_CStr(dataDir_App_(), stateFileName_App_)); | 594 | iFile *f = newCStr_File(concatPath_CStr(dataDir_App_(), tempStateFileName_App_)); |
565 | if (open_File(f, writeOnly_FileMode)) { | 595 | if (open_File(f, writeOnly_FileMode)) { |
566 | writeData_File(f, magicState_App_, 4); | 596 | writeData_File(f, magicState_App_, 4); |
567 | writeU32_File(f, latest_FileVersion); /* version */ | 597 | writeU32_File(f, latest_FileVersion); /* version */ |
@@ -603,11 +633,19 @@ static void saveState_App_(const iApp *d) { | |||
603 | write8_File(f, flags); | 633 | write8_File(f, flags); |
604 | serializeState_DocumentWidget(i.object, stream_File(f)); | 634 | serializeState_DocumentWidget(i.object, stream_File(f)); |
605 | } | 635 | } |
636 | iRelease(f); | ||
606 | } | 637 | } |
607 | else { | 638 | else { |
639 | iRelease(f); | ||
608 | fprintf(stderr, "[App] failed to save state: %s\n", strerror(errno)); | 640 | fprintf(stderr, "[App] failed to save state: %s\n", strerror(errno)); |
641 | return; | ||
609 | } | 642 | } |
610 | iRelease(f); | 643 | /* Copy it over to the real file. This avoids truncation if the app for any reason crashes |
644 | before the state file is fully written. */ | ||
645 | const char *tempName = concatPath_CStr(dataDir_App_(), tempStateFileName_App_); | ||
646 | const char *finalName = concatPath_CStr(dataDir_App_(), stateFileName_App_); | ||
647 | remove(finalName); | ||
648 | rename(tempName, finalName); | ||
611 | } | 649 | } |
612 | 650 | ||
613 | #if defined (LAGRANGE_ENABLE_IDLE_SLEEP) | 651 | #if defined (LAGRANGE_ENABLE_IDLE_SLEEP) |
@@ -678,18 +716,22 @@ static void communicateWithRunningInstance_App_(iApp *d, iProcessId instance, | |||
678 | appendCStr_String(cmds, "tabs.new\n"); | 716 | appendCStr_String(cmds, "tabs.new\n"); |
679 | requestRaise = iTrue; | 717 | requestRaise = iTrue; |
680 | } | 718 | } |
719 | iBool gotResult = iFalse; | ||
681 | if (!isEmpty_String(cmds)) { | 720 | if (!isEmpty_String(cmds)) { |
682 | iString *result = communicate_Ipc(cmds, requestRaise); | 721 | iString *result = communicate_Ipc(cmds, requestRaise); |
683 | if (result) { | 722 | if (result) { |
684 | fwrite(cstr_String(result), 1, size_String(result), stdout); | 723 | fwrite(cstr_String(result), 1, size_String(result), stdout); |
685 | fflush(stdout); | 724 | fflush(stdout); |
725 | if (!isEmpty_String(result)) { | ||
726 | gotResult = iTrue; | ||
727 | } | ||
686 | } | 728 | } |
687 | delete_String(result); | 729 | delete_String(result); |
688 | } | 730 | } |
689 | iUnused(instance); | 731 | iUnused(instance); |
690 | // else { | 732 | if (!gotResult) { |
691 | // printf("Lagrange already running (PID %d)\n", instance); | 733 | printf("Commands sent to Lagrange process %d\n", instance); |
692 | // } | 734 | } |
693 | terminate_App_(0); | 735 | terminate_App_(0); |
694 | } | 736 | } |
695 | #endif /* defined (LAGRANGE_ENABLE_IPC) */ | 737 | #endif /* defined (LAGRANGE_ENABLE_IPC) */ |
@@ -714,6 +756,8 @@ static void init_App_(iApp *d, int argc, char **argv) { | |||
714 | d->isRunningUnderWindowSystem = iTrue; | 756 | d->isRunningUnderWindowSystem = iTrue; |
715 | #endif | 757 | #endif |
716 | d->isDarkSystemTheme = iTrue; /* will be updated by system later on, if supported */ | 758 | d->isDarkSystemTheme = iTrue; /* will be updated by system later on, if supported */ |
759 | d->isSuspended = iFalse; | ||
760 | d->tempFilesPendingDeletion = new_StringSet(); | ||
717 | init_CommandLine(&d->args, argc, argv); | 761 | init_CommandLine(&d->args, argc, argv); |
718 | /* Where was the app started from? We ask SDL first because the command line alone | 762 | /* Where was the app started from? We ask SDL first because the command line alone |
719 | cannot be relied on (behavior differs depending on OS). */ { | 763 | cannot be relied on (behavior differs depending on OS). */ { |
@@ -920,6 +964,8 @@ static void init_App_(iApp *d, int argc, char **argv) { | |||
920 | if (!loadState_App_(d)) { | 964 | if (!loadState_App_(d)) { |
921 | postCommand_Root(NULL, "open url:about:help"); | 965 | postCommand_Root(NULL, "open url:about:help"); |
922 | } | 966 | } |
967 | postCommand_App("~navbar.actions.changed"); | ||
968 | postCommand_App("~toolbar.actions.changed"); | ||
923 | postCommand_Root(NULL, "~window.unfreeze"); | 969 | postCommand_Root(NULL, "~window.unfreeze"); |
924 | postCommand_Root(NULL, "font.reset"); | 970 | postCommand_Root(NULL, "font.reset"); |
925 | d->autoReloadTimer = SDL_AddTimer(60 * 1000, postAutoReloadCommand_App_, NULL); | 971 | d->autoReloadTimer = SDL_AddTimer(60 * 1000, postAutoReloadCommand_App_, NULL); |
@@ -986,6 +1032,11 @@ static void deinit_App(iApp *d) { | |||
986 | deinit_Periodic(&d->periodic); | 1032 | deinit_Periodic(&d->periodic); |
987 | deinit_Lang(); | 1033 | deinit_Lang(); |
988 | iRecycle(); | 1034 | iRecycle(); |
1035 | /* Delete all temporary files created while running. */ | ||
1036 | iConstForEach(StringSet, tmp, d->tempFilesPendingDeletion) { | ||
1037 | remove(cstr_String(tmp.value)); | ||
1038 | } | ||
1039 | iRelease(d->tempFilesPendingDeletion); | ||
989 | } | 1040 | } |
990 | 1041 | ||
991 | const iString *execPath_App(void) { | 1042 | const iString *execPath_App(void) { |
@@ -1000,7 +1051,7 @@ const iString *downloadDir_App(void) { | |||
1000 | return collect_String(cleaned_Path(&app_.prefs.strings[downloadDir_PrefsString])); | 1051 | return collect_String(cleaned_Path(&app_.prefs.strings[downloadDir_PrefsString])); |
1001 | } | 1052 | } |
1002 | 1053 | ||
1003 | const iString *downloadPathForUrl_App(const iString *url, const iString *mime) { | 1054 | const iString *fileNameForUrl_App(const iString *url, const iString *mime) { |
1004 | /* Figure out a file name from the URL. */ | 1055 | /* Figure out a file name from the URL. */ |
1005 | iUrl parts; | 1056 | iUrl parts; |
1006 | init_Url(&parts, url); | 1057 | init_Url(&parts, url); |
@@ -1026,22 +1077,27 @@ const iString *downloadPathForUrl_App(const iString *url, const iString *mime) { | |||
1026 | } | 1077 | } |
1027 | } | 1078 | } |
1028 | if (startsWith_String(name, "~")) { | 1079 | if (startsWith_String(name, "~")) { |
1029 | /* This would be interpreted as a reference to a home directory. */ | 1080 | /* This might be interpreted as a reference to a home directory. */ |
1030 | remove_Block(&name->chars, 0, 1); | 1081 | remove_Block(&name->chars, 0, 1); |
1031 | } | 1082 | } |
1032 | iString *savePath = concat_Path(downloadDir_App(), name); | 1083 | if (lastIndexOfCStr_String(name, ".") == iInvalidPos) { |
1033 | if (lastIndexOfCStr_String(savePath, ".") == iInvalidPos) { | 1084 | /* TODO: Needs the inverse of `mediaTypeFromFileExtension_String()`. */ |
1034 | /* No extension specified in URL. */ | 1085 | /* No extension specified in URL. */ |
1035 | if (startsWith_String(mime, "text/gemini")) { | 1086 | if (startsWith_String(mime, "text/gemini")) { |
1036 | appendCStr_String(savePath, ".gmi"); | 1087 | appendCStr_String(name, ".gmi"); |
1037 | } | 1088 | } |
1038 | else if (startsWith_String(mime, "text/")) { | 1089 | else if (startsWith_String(mime, "text/")) { |
1039 | appendCStr_String(savePath, ".txt"); | 1090 | appendCStr_String(name, ".txt"); |
1040 | } | 1091 | } |
1041 | else if (startsWith_String(mime, "image/")) { | 1092 | else if (startsWith_String(mime, "image/")) { |
1042 | appendCStr_String(savePath, cstr_String(mime) + 6); | 1093 | appendCStr_String(name, cstr_String(mime) + 6); |
1043 | } | 1094 | } |
1044 | } | 1095 | } |
1096 | return name; | ||
1097 | } | ||
1098 | |||
1099 | const iString *downloadPathForUrl_App(const iString *url, const iString *mime) { | ||
1100 | iString *savePath = concat_Path(downloadDir_App(), fileNameForUrl_App(url, mime)); | ||
1045 | if (fileExists_FileInfo(savePath)) { | 1101 | if (fileExists_FileInfo(savePath)) { |
1046 | /* Make it unique. */ | 1102 | /* Make it unique. */ |
1047 | iDate now; | 1103 | iDate now; |
@@ -1056,6 +1112,22 @@ const iString *downloadPathForUrl_App(const iString *url, const iString *mime) { | |||
1056 | return collect_String(savePath); | 1112 | return collect_String(savePath); |
1057 | } | 1113 | } |
1058 | 1114 | ||
1115 | const iString *temporaryPathForUrl_App(const iString *url, const iString *mime) { | ||
1116 | iApp *d = &app_; | ||
1117 | #if defined (P_tmpdir) | ||
1118 | iString * tmpPath = collectNew_String(); | ||
1119 | const iRangecc tmpDir = range_CStr(P_tmpdir); | ||
1120 | #else | ||
1121 | iString * tmpPath = collectNewCStr_String(tmpnam(NULL)); | ||
1122 | const iRangecc tmpDir = dirName_Path(tmpPath); | ||
1123 | #endif | ||
1124 | set_String( | ||
1125 | tmpPath, | ||
1126 | collect_String(concat_Path(collectNewRange_String(tmpDir), fileNameForUrl_App(url, mime)))); | ||
1127 | insert_StringSet(d->tempFilesPendingDeletion, tmpPath); /* deleted in `deinit_App` */ | ||
1128 | return tmpPath; | ||
1129 | } | ||
1130 | |||
1059 | const iString *debugInfo_App(void) { | 1131 | const iString *debugInfo_App(void) { |
1060 | extern char **environ; /* The environment variables. */ | 1132 | extern char **environ; /* The environment variables. */ |
1061 | iApp *d = &app_; | 1133 | iApp *d = &app_; |
@@ -1175,9 +1247,6 @@ iBool findCachedContent_App(const iString *url, iString *mime_out, iBlock *data_ | |||
1175 | #endif | 1247 | #endif |
1176 | 1248 | ||
1177 | iLocalDef iBool isWaitingAllowed_App_(iApp *d) { | 1249 | iLocalDef iBool isWaitingAllowed_App_(iApp *d) { |
1178 | if (!isEmpty_Periodic(&d->periodic)) { | ||
1179 | return iFalse; | ||
1180 | } | ||
1181 | if (d->warmupFrames > 0) { | 1250 | if (d->warmupFrames > 0) { |
1182 | return iFalse; | 1251 | return iFalse; |
1183 | } | 1252 | } |
@@ -1191,16 +1260,19 @@ iLocalDef iBool isWaitingAllowed_App_(iApp *d) { | |||
1191 | 1260 | ||
1192 | static iBool nextEvent_App_(iApp *d, enum iAppEventMode eventMode, SDL_Event *event) { | 1261 | static iBool nextEvent_App_(iApp *d, enum iAppEventMode eventMode, SDL_Event *event) { |
1193 | if (eventMode == waitForNewEvents_AppEventMode && isWaitingAllowed_App_(d)) { | 1262 | if (eventMode == waitForNewEvents_AppEventMode && isWaitingAllowed_App_(d)) { |
1194 | /* If there are periodic commands pending, wait only for a short while. */ | ||
1195 | if (!isEmpty_Periodic(&d->periodic)) { | ||
1196 | return SDL_WaitEventTimeout(event, 500); | ||
1197 | } | ||
1198 | /* We may be allowed to block here until an event comes in. */ | 1263 | /* We may be allowed to block here until an event comes in. */ |
1199 | if (isWaitingAllowed_App_(d)) { | 1264 | if (isWaitingAllowed_App_(d)) { |
1200 | return SDL_WaitEvent(event); | 1265 | return SDL_WaitEvent(event); |
1201 | } | 1266 | } |
1202 | } | 1267 | } |
1268 | /* SDL regression circa 2.0.18? SDL_PollEvent() doesn't always return | ||
1269 | events posted immediately beforehand. Waiting with a very short timeout | ||
1270 | seems to work better. */ | ||
1271 | #if defined (iPlatformLinux) && SDL_VERSION_ATLEAST(2, 0, 18) | ||
1272 | return SDL_WaitEventTimeout(event, 1); | ||
1273 | #else | ||
1203 | return SDL_PollEvent(event); | 1274 | return SDL_PollEvent(event); |
1275 | #endif | ||
1204 | } | 1276 | } |
1205 | 1277 | ||
1206 | static iPtrArray *listWindows_App_(const iApp *d, iPtrArray *windows) { | 1278 | static iPtrArray *listWindows_App_(const iApp *d, iPtrArray *windows) { |
@@ -1239,6 +1311,7 @@ void processEvents_App(enum iAppEventMode eventMode) { | |||
1239 | break; | 1311 | break; |
1240 | case SDL_APP_WILLENTERFOREGROUND: | 1312 | case SDL_APP_WILLENTERFOREGROUND: |
1241 | invalidate_Window(as_Window(d->window)); | 1313 | invalidate_Window(as_Window(d->window)); |
1314 | d->isSuspended = iFalse; | ||
1242 | break; | 1315 | break; |
1243 | case SDL_APP_DIDENTERFOREGROUND: | 1316 | case SDL_APP_DIDENTERFOREGROUND: |
1244 | gotEvents = iTrue; | 1317 | gotEvents = iTrue; |
@@ -1256,6 +1329,7 @@ void processEvents_App(enum iAppEventMode eventMode) { | |||
1256 | setFreezeDraw_MainWindow(d->window, iTrue); | 1329 | setFreezeDraw_MainWindow(d->window, iTrue); |
1257 | savePrefs_App_(d); | 1330 | savePrefs_App_(d); |
1258 | saveState_App_(d); | 1331 | saveState_App_(d); |
1332 | d->isSuspended = iTrue; | ||
1259 | break; | 1333 | break; |
1260 | case SDL_APP_TERMINATING: | 1334 | case SDL_APP_TERMINATING: |
1261 | setFreezeDraw_MainWindow(d->window, iTrue); | 1335 | setFreezeDraw_MainWindow(d->window, iTrue); |
@@ -1284,6 +1358,10 @@ void processEvents_App(enum iAppEventMode eventMode) { | |||
1284 | break; | 1358 | break; |
1285 | } | 1359 | } |
1286 | default: { | 1360 | default: { |
1361 | if (ev.type == SDL_USEREVENT && ev.user.code == periodic_UserEventCode) { | ||
1362 | dispatchCommands_Periodic(&d->periodic); | ||
1363 | continue; | ||
1364 | } | ||
1287 | #if defined (LAGRANGE_ENABLE_IDLE_SLEEP) | 1365 | #if defined (LAGRANGE_ENABLE_IDLE_SLEEP) |
1288 | if (ev.type == SDL_USEREVENT && ev.user.code == asleep_UserEventCode) { | 1366 | if (ev.type == SDL_USEREVENT && ev.user.code == asleep_UserEventCode) { |
1289 | if (SDL_GetTicks() - d->lastEventTime > idleThreshold_App_ && | 1367 | if (SDL_GetTicks() - d->lastEventTime > idleThreshold_App_ && |
@@ -1323,28 +1401,6 @@ void processEvents_App(enum iAppEventMode eventMode) { | |||
1323 | #endif | 1401 | #endif |
1324 | /* Scroll events may be per-pixel or mouse wheel steps. */ | 1402 | /* Scroll events may be per-pixel or mouse wheel steps. */ |
1325 | if (ev.type == SDL_MOUSEWHEEL) { | 1403 | if (ev.type == SDL_MOUSEWHEEL) { |
1326 | #if defined (iPlatformAppleDesktop) | ||
1327 | /* On macOS, we handle both trackpad and mouse events. We expect SDL to identify | ||
1328 | which device is sending the event. */ | ||
1329 | if (ev.wheel.which == 0) { | ||
1330 | /* Trackpad with precise scrolling w/inertia (points). */ | ||
1331 | setPerPixel_MouseWheelEvent(&ev.wheel, iTrue); | ||
1332 | ev.wheel.x *= -d->window->base.pixelRatio; | ||
1333 | ev.wheel.y *= d->window->base.pixelRatio; | ||
1334 | /* Only scroll on one axis at a time. */ | ||
1335 | if (iAbs(ev.wheel.x) > iAbs(ev.wheel.y)) { | ||
1336 | ev.wheel.y = 0; | ||
1337 | } | ||
1338 | else { | ||
1339 | ev.wheel.x = 0; | ||
1340 | } | ||
1341 | } | ||
1342 | else { | ||
1343 | /* Disregard wheel acceleration applied by the OS. */ | ||
1344 | ev.wheel.x = -ev.wheel.x; | ||
1345 | ev.wheel.y = iSign(ev.wheel.y); | ||
1346 | } | ||
1347 | #endif | ||
1348 | #if defined (iPlatformMsys) | 1404 | #if defined (iPlatformMsys) |
1349 | ev.wheel.x = -ev.wheel.x; | 1405 | ev.wheel.x = -ev.wheel.x; |
1350 | #endif | 1406 | #endif |
@@ -1460,10 +1516,10 @@ void processEvents_App(enum iAppEventMode eventMode) { | |||
1460 | deinit_PtrArray(&windows); | 1516 | deinit_PtrArray(&windows); |
1461 | #if defined (LAGRANGE_ENABLE_IDLE_SLEEP) | 1517 | #if defined (LAGRANGE_ENABLE_IDLE_SLEEP) |
1462 | if (d->isIdling && !gotEvents) { | 1518 | if (d->isIdling && !gotEvents) { |
1463 | /* This is where we spend most of our time when idle. 60 Hz still quite a lot but we | 1519 | /* This is where we spend most of our time when idle. 30 Hz still quite a lot but we |
1464 | can't wait too long after the user tries to interact again with the app. In any | 1520 | can't wait too long after the user tries to interact again with the app. In any |
1465 | case, on macOS SDL_WaitEvent() seems to use 10x more CPU time than sleeping. */ | 1521 | case, on iOS SDL_WaitEvent() seems to use 10x more CPU time than sleeping (2.0.18). */ |
1466 | SDL_Delay(1000 / 60); | 1522 | SDL_Delay(1000 / 30); |
1467 | } | 1523 | } |
1468 | #endif | 1524 | #endif |
1469 | backToMainLoop:; | 1525 | backToMainLoop:; |
@@ -1478,6 +1534,12 @@ static void runTickers_App_(iApp *d) { | |||
1478 | d->lastTickerTime = 0; | 1534 | d->lastTickerTime = 0; |
1479 | return; | 1535 | return; |
1480 | } | 1536 | } |
1537 | iForIndices(i, d->window->base.roots) { | ||
1538 | iRoot *root = d->window->base.roots[i]; | ||
1539 | if (root) { | ||
1540 | root->didAnimateVisualOffsets = iFalse; | ||
1541 | } | ||
1542 | } | ||
1481 | /* Tickers may add themselves again, so we'll run off a copy. */ | 1543 | /* Tickers may add themselves again, so we'll run off a copy. */ |
1482 | iSortedArray *pending = copy_SortedArray(&d->tickers); | 1544 | iSortedArray *pending = copy_SortedArray(&d->tickers); |
1483 | clear_SortedArray(&d->tickers); | 1545 | clear_SortedArray(&d->tickers); |
@@ -1494,6 +1556,12 @@ static void runTickers_App_(iApp *d) { | |||
1494 | if (isEmpty_SortedArray(&d->tickers)) { | 1556 | if (isEmpty_SortedArray(&d->tickers)) { |
1495 | d->lastTickerTime = 0; | 1557 | d->lastTickerTime = 0; |
1496 | } | 1558 | } |
1559 | // iForIndices(i, d->window->base.roots) { | ||
1560 | // iRoot *root = d->window->base.roots[i]; | ||
1561 | // if (root) { | ||
1562 | // notifyVisualOffsetChange_Root(root); | ||
1563 | // } | ||
1564 | // } | ||
1497 | } | 1565 | } |
1498 | 1566 | ||
1499 | static int resizeWatcher_(void *user, SDL_Event *event) { | 1567 | static int resizeWatcher_(void *user, SDL_Event *event) { |
@@ -1526,7 +1594,6 @@ static int run_App_(iApp *d) { | |||
1526 | SDL_AddEventWatch(resizeWatcher_, d); /* redraw window during resizing */ | 1594 | SDL_AddEventWatch(resizeWatcher_, d); /* redraw window during resizing */ |
1527 | #endif | 1595 | #endif |
1528 | while (d->isRunning) { | 1596 | while (d->isRunning) { |
1529 | dispatchCommands_Periodic(&d->periodic); | ||
1530 | processEvents_App(waitForNewEvents_AppEventMode); | 1597 | processEvents_App(waitForNewEvents_AppEventMode); |
1531 | runTickers_App_(d); | 1598 | runTickers_App_(d); |
1532 | refresh_App(); | 1599 | refresh_App(); |
@@ -1680,13 +1747,20 @@ void postCommand_Root(iRoot *d, const char *command) { | |||
1680 | ev.user.data1 = strdup(command); | 1747 | ev.user.data1 = strdup(command); |
1681 | ev.user.data2 = d; /* all events are root-specific */ | 1748 | ev.user.data2 = d; /* all events are root-specific */ |
1682 | SDL_PushEvent(&ev); | 1749 | SDL_PushEvent(&ev); |
1750 | iWindow *win = get_Window(); | ||
1751 | #if defined (iPlatformAndroid) | ||
1752 | SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "%s[command] {%d} %s", | ||
1753 | app_.isLoadingPrefs ? "[Prefs] " : "", | ||
1754 | (d == NULL || win == NULL ? 0 : d == win->roots[0] ? 1 : 2), | ||
1755 | command); | ||
1756 | #else | ||
1683 | if (app_.commandEcho) { | 1757 | if (app_.commandEcho) { |
1684 | iWindow *win = get_Window(); | ||
1685 | printf("%s[command] {%d} %s\n", | 1758 | printf("%s[command] {%d} %s\n", |
1686 | app_.isLoadingPrefs ? "[Prefs] " : "", | 1759 | app_.isLoadingPrefs ? "[Prefs] " : "", |
1687 | (d == NULL || win == NULL ? 0 : d == win->roots[0] ? 1 : 2), | 1760 | (d == NULL || win == NULL ? 0 : d == win->roots[0] ? 1 : 2), |
1688 | command); fflush(stdout); | 1761 | command); fflush(stdout); |
1689 | } | 1762 | } |
1763 | #endif | ||
1690 | } | 1764 | } |
1691 | 1765 | ||
1692 | void postCommandf_Root(iRoot *d, const char *command, ...) { | 1766 | void postCommandf_Root(iRoot *d, const char *command, ...) { |
@@ -1823,6 +1897,12 @@ static void updatePrefsPinSplitButtons_(iWidget *d, int value) { | |||
1823 | } | 1897 | } |
1824 | } | 1898 | } |
1825 | 1899 | ||
1900 | static void updatePrefsToolBarActionButton_(iWidget *prefs, int buttonIndex, int action) { | ||
1901 | updateDropdownSelection_LabelWidget( | ||
1902 | findChild_Widget(prefs, format_CStr("prefs.toolbaraction%d", buttonIndex + 1)), | ||
1903 | format_CStr(" arg:%d button:%d", action, buttonIndex)); | ||
1904 | } | ||
1905 | |||
1826 | static void updateScrollSpeedButtons_(iWidget *d, enum iScrollType type, const int value) { | 1906 | static void updateScrollSpeedButtons_(iWidget *d, enum iScrollType type, const int value) { |
1827 | const char *typeStr = (type == mouse_ScrollType ? "mouse" : "keyboard"); | 1907 | const char *typeStr = (type == mouse_ScrollType ? "mouse" : "keyboard"); |
1828 | for (int i = 0; i <= 40; i++) { | 1908 | for (int i = 0; i <= 40; i++) { |
@@ -1913,6 +1993,10 @@ static iBool handlePrefsCommands_(iWidget *d, const char *cmd) { | |||
1913 | format_CStr("returnkey.set arg:%d", arg_Command(cmd))); | 1993 | format_CStr("returnkey.set arg:%d", arg_Command(cmd))); |
1914 | return iFalse; | 1994 | return iFalse; |
1915 | } | 1995 | } |
1996 | else if (equal_Command(cmd, "toolbar.action.set")) { | ||
1997 | updatePrefsToolBarActionButton_(d, argLabel_Command(cmd, "button"), arg_Command(cmd)); | ||
1998 | return iFalse; | ||
1999 | } | ||
1916 | else if (equal_Command(cmd, "pinsplit.set")) { | 2000 | else if (equal_Command(cmd, "pinsplit.set")) { |
1917 | updatePrefsPinSplitButtons_(d, arg_Command(cmd)); | 2001 | updatePrefsPinSplitButtons_(d, arg_Command(cmd)); |
1918 | return iFalse; | 2002 | return iFalse; |
@@ -1957,8 +2041,11 @@ static iBool handlePrefsCommands_(iWidget *d, const char *cmd) { | |||
1957 | } | 2041 | } |
1958 | } | 2042 | } |
1959 | else if (equalWidget_Command(cmd, d, "input.resized")) { | 2043 | else if (equalWidget_Command(cmd, d, "input.resized")) { |
1960 | updatePreferencesLayout_Widget(d); | 2044 | if (!d->root->pendingArrange) { |
1961 | return iFalse; | 2045 | d->root->pendingArrange = iTrue; |
2046 | postCommand_Root(d->root, "root.arrange"); | ||
2047 | } | ||
2048 | return iTrue; | ||
1962 | } | 2049 | } |
1963 | return iFalse; | 2050 | return iFalse; |
1964 | } | 2051 | } |
@@ -2176,6 +2263,7 @@ iBool handleCommand_App(const char *cmd) { | |||
2176 | return iTrue; | 2263 | return iTrue; |
2177 | } | 2264 | } |
2178 | else if (equal_Command(cmd, "fontpack.suggest.classic")) { | 2265 | else if (equal_Command(cmd, "fontpack.suggest.classic")) { |
2266 | /* TODO: Don't use this when system fonts are accessible. */ | ||
2179 | if (!isInstalled_Fonts("classic-set") && !isInstalled_Fonts("cjk")) { | 2267 | if (!isInstalled_Fonts("classic-set") && !isInstalled_Fonts("cjk")) { |
2180 | makeQuestion_Widget( | 2268 | makeQuestion_Widget( |
2181 | uiHeading_ColorEscape "${heading.fontpack.classic}", | 2269 | uiHeading_ColorEscape "${heading.fontpack.classic}", |
@@ -2208,6 +2296,22 @@ iBool handleCommand_App(const char *cmd) { | |||
2208 | } | 2296 | } |
2209 | return iTrue; | 2297 | return iTrue; |
2210 | } | 2298 | } |
2299 | else if (equal_Command(cmd, "navbar.action.set")) { | ||
2300 | d->prefs.navbarActions[iClamp(argLabel_Command(cmd, "button"), 0, maxNavbarActions_Prefs - 1)] = | ||
2301 | iClamp(arg_Command(cmd), 0, max_ToolbarAction - 1); | ||
2302 | if (!isFrozen) { | ||
2303 | postCommand_App("~navbar.actions.changed"); | ||
2304 | } | ||
2305 | return iTrue; | ||
2306 | } | ||
2307 | else if (equal_Command(cmd, "toolbar.action.set")) { | ||
2308 | d->prefs.toolbarActions[iClamp(argLabel_Command(cmd, "button"), 0, 1)] = | ||
2309 | iClamp(arg_Command(cmd), 0, max_ToolbarAction - 1); | ||
2310 | if (!isFrozen) { | ||
2311 | postCommand_App("~toolbar.actions.changed"); | ||
2312 | } | ||
2313 | return iTrue; | ||
2314 | } | ||
2211 | else if (equal_Command(cmd, "translation.languages")) { | 2315 | else if (equal_Command(cmd, "translation.languages")) { |
2212 | d->prefs.langFrom = argLabel_Command(cmd, "from"); | 2316 | d->prefs.langFrom = argLabel_Command(cmd, "from"); |
2213 | d->prefs.langTo = argLabel_Command(cmd, "to"); | 2317 | d->prefs.langTo = argLabel_Command(cmd, "to"); |
@@ -2229,6 +2333,9 @@ iBool handleCommand_App(const char *cmd) { | |||
2229 | (argLabel_Command(cmd, "axis") ? vertical_WindowSplit : 0) | (arg_Command(cmd) << 1); | 2333 | (argLabel_Command(cmd, "axis") ? vertical_WindowSplit : 0) | (arg_Command(cmd) << 1); |
2230 | const char *url = suffixPtr_Command(cmd, "url"); | 2334 | const char *url = suffixPtr_Command(cmd, "url"); |
2231 | setCStr_String(d->window->pendingSplitUrl, url ? url : ""); | 2335 | setCStr_String(d->window->pendingSplitUrl, url ? url : ""); |
2336 | if (hasLabel_Command(cmd, "origin")) { | ||
2337 | set_String(d->window->pendingSplitOrigin, string_Command(cmd, "origin")); | ||
2338 | } | ||
2232 | postRefresh_App(); | 2339 | postRefresh_App(); |
2233 | return iTrue; | 2340 | return iTrue; |
2234 | } | 2341 | } |
@@ -2587,6 +2694,10 @@ iBool handleCommand_App(const char *cmd) { | |||
2587 | d->prefs.uiAnimations = arg_Command(cmd) != 0; | 2694 | d->prefs.uiAnimations = arg_Command(cmd) != 0; |
2588 | return iTrue; | 2695 | return iTrue; |
2589 | } | 2696 | } |
2697 | else if (equal_Command(cmd, "prefs.blink.changed")) { | ||
2698 | d->prefs.blinkingCursor = arg_Command(cmd) != 0; | ||
2699 | return iTrue; | ||
2700 | } | ||
2590 | else if (equal_Command(cmd, "prefs.time.24h.changed")) { | 2701 | else if (equal_Command(cmd, "prefs.time.24h.changed")) { |
2591 | d->prefs.time24h = arg_Command(cmd) != 0; | 2702 | d->prefs.time24h = arg_Command(cmd) != 0; |
2592 | return iTrue; | 2703 | return iTrue; |
@@ -2642,7 +2753,9 @@ iBool handleCommand_App(const char *cmd) { | |||
2642 | } | 2753 | } |
2643 | #endif | 2754 | #endif |
2644 | else if (equal_Command(cmd, "downloads.open")) { | 2755 | else if (equal_Command(cmd, "downloads.open")) { |
2645 | postCommandf_App("open url:%s", cstrCollect_String(makeFileUrl_String(downloadDir_App()))); | 2756 | postCommandf_App("open newtab:%d url:%s", |
2757 | argLabel_Command(cmd, "newtab"), | ||
2758 | cstrCollect_String(makeFileUrl_String(downloadDir_App()))); | ||
2646 | return iTrue; | 2759 | return iTrue; |
2647 | } | 2760 | } |
2648 | else if (equal_Command(cmd, "ca.file")) { | 2761 | else if (equal_Command(cmd, "ca.file")) { |
@@ -2673,14 +2786,29 @@ iBool handleCommand_App(const char *cmd) { | |||
2673 | } | 2786 | } |
2674 | return iTrue; | 2787 | return iTrue; |
2675 | } | 2788 | } |
2789 | else if (equal_Command(cmd, "reveal")) { | ||
2790 | const iString *path = NULL; | ||
2791 | if (hasLabel_Command(cmd, "path")) { | ||
2792 | path = suffix_Command(cmd, "path"); | ||
2793 | } | ||
2794 | else if (hasLabel_Command(cmd, "url")) { | ||
2795 | path = collect_String(localFilePathFromUrl_String(suffix_Command(cmd, "url"))); | ||
2796 | } | ||
2797 | if (path) { | ||
2798 | revealPath_App(path); | ||
2799 | } | ||
2800 | return iTrue; | ||
2801 | } | ||
2676 | else if (equal_Command(cmd, "open")) { | 2802 | else if (equal_Command(cmd, "open")) { |
2677 | const char *urlArg = suffixPtr_Command(cmd, "url"); | 2803 | const char *urlArg = suffixPtr_Command(cmd, "url"); |
2678 | if (!urlArg) { | 2804 | if (!urlArg) { |
2679 | return iTrue; /* invalid command */ | 2805 | return iTrue; /* invalid command */ |
2680 | } | 2806 | } |
2681 | iString *url = collectNewCStr_String(urlArg); | 2807 | if (findWidget_App("prefs")) { |
2682 | const iBool noProxy = argLabel_Command(cmd, "noproxy") != 0; | 2808 | postCommand_App("prefs.dismiss"); |
2683 | const iBool fromSidebar = argLabel_Command(cmd, "fromsidebar") != 0; | 2809 | } |
2810 | iString *url = collectNewCStr_String(urlArg); | ||
2811 | const iBool noProxy = argLabel_Command(cmd, "noproxy") != 0; | ||
2684 | iUrl parts; | 2812 | iUrl parts; |
2685 | init_Url(&parts, url); | 2813 | init_Url(&parts, url); |
2686 | if (equal_Rangecc(parts.scheme, "about") && equal_Rangecc(parts.path, "command") && | 2814 | if (equal_Rangecc(parts.scheme, "about") && equal_Rangecc(parts.path, "command") && |
@@ -2711,12 +2839,23 @@ iBool handleCommand_App(const char *cmd) { | |||
2711 | openInDefaultBrowser_App(url); | 2839 | openInDefaultBrowser_App(url); |
2712 | return iTrue; | 2840 | return iTrue; |
2713 | } | 2841 | } |
2842 | iDocumentWidget *doc = document_Command(cmd); | ||
2843 | iDocumentWidget *origin = doc; | ||
2844 | if (hasLabel_Command(cmd, "origin")) { | ||
2845 | iDocumentWidget *cmdOrig = findWidget_App(cstr_Command(cmd, "origin")); | ||
2846 | if (cmdOrig) { | ||
2847 | origin = cmdOrig; | ||
2848 | } | ||
2849 | } | ||
2714 | const int newTab = argLabel_Command(cmd, "newtab"); | 2850 | const int newTab = argLabel_Command(cmd, "newtab"); |
2715 | if (newTab & otherRoot_OpenTabFlag && numRoots_Window(get_Window()) == 1) { | 2851 | if (newTab & otherRoot_OpenTabFlag && numRoots_Window(get_Window()) == 1) { |
2716 | /* Need to split first. */ | 2852 | /* Need to split first. */ |
2717 | const iInt2 winSize = get_Window()->size; | 2853 | const iInt2 winSize = get_Window()->size; |
2718 | postCommandf_App("ui.split arg:3 axis:%d newtab:%d url:%s", | 2854 | const int splitMode = argLabel_Command(cmd, "splitmode"); |
2855 | postCommandf_App("ui.split arg:%d axis:%d origin:%s newtab:%d url:%s", | ||
2856 | splitMode ? splitMode : 3, | ||
2719 | (float) winSize.x / (float) winSize.y < 0.7f ? 1 : 0, | 2857 | (float) winSize.x / (float) winSize.y < 0.7f ? 1 : 0, |
2858 | cstr_String(id_Widget(as_Widget(origin))), | ||
2720 | newTab & ~otherRoot_OpenTabFlag, | 2859 | newTab & ~otherRoot_OpenTabFlag, |
2721 | cstr_String(url)); | 2860 | cstr_String(url)); |
2722 | return iTrue; | 2861 | return iTrue; |
@@ -2727,15 +2866,16 @@ iBool handleCommand_App(const char *cmd) { | |||
2727 | root = otherRoot_Window(as_Window(d->window), root); | 2866 | root = otherRoot_Window(as_Window(d->window), root); |
2728 | setKeyRoot_Window(as_Window(d->window), root); | 2867 | setKeyRoot_Window(as_Window(d->window), root); |
2729 | setCurrent_Root(root); /* need to change for widget creation */ | 2868 | setCurrent_Root(root); /* need to change for widget creation */ |
2869 | doc = document_Command(cmd); /* may be different */ | ||
2730 | } | 2870 | } |
2731 | iDocumentWidget *doc = document_Command(cmd); | ||
2732 | if (newTab & (new_OpenTabFlag | newBackground_OpenTabFlag)) { | 2871 | if (newTab & (new_OpenTabFlag | newBackground_OpenTabFlag)) { |
2733 | doc = newTab_App(NULL, (newTab & new_OpenTabFlag) != 0); /* `newtab:2` to open in background */ | 2872 | doc = newTab_App(NULL, (newTab & new_OpenTabFlag) != 0); /* `newtab:2` to open in background */ |
2734 | } | 2873 | } |
2735 | iHistory *history = history_DocumentWidget(doc); | 2874 | iHistory *history = history_DocumentWidget(doc); |
2736 | const iBool isHistory = argLabel_Command(cmd, "history") != 0; | 2875 | const iBool isHistory = argLabel_Command(cmd, "history") != 0; |
2737 | int redirectCount = argLabel_Command(cmd, "redirect"); | 2876 | int redirectCount = argLabel_Command(cmd, "redirect"); |
2738 | if (!isHistory) { | 2877 | if (!isHistory) { |
2878 | /* TODO: Shouldn't DocumentWidget manage history on its own? */ | ||
2739 | if (redirectCount) { | 2879 | if (redirectCount) { |
2740 | replace_History(history, url); | 2880 | replace_History(history, url); |
2741 | } | 2881 | } |
@@ -2745,16 +2885,10 @@ iBool handleCommand_App(const char *cmd) { | |||
2745 | } | 2885 | } |
2746 | setInitialScroll_DocumentWidget(doc, argfLabel_Command(cmd, "scroll")); | 2886 | setInitialScroll_DocumentWidget(doc, argfLabel_Command(cmd, "scroll")); |
2747 | setRedirectCount_DocumentWidget(doc, redirectCount); | 2887 | setRedirectCount_DocumentWidget(doc, redirectCount); |
2888 | setOrigin_DocumentWidget(doc, origin); | ||
2748 | showCollapsed_Widget(findWidget_App("document.progress"), iFalse); | 2889 | showCollapsed_Widget(findWidget_App("document.progress"), iFalse); |
2749 | if (prefs_App()->decodeUserVisibleURLs) { | ||
2750 | urlDecodePath_String(url); | ||
2751 | } | ||
2752 | else { | ||
2753 | urlEncodePath_String(url); | ||
2754 | } | ||
2755 | setUrlFlags_DocumentWidget(doc, url, | 2890 | setUrlFlags_DocumentWidget(doc, url, |
2756 | (isHistory ? useCachedContentIfAvailable_DocumentWidgetSetUrlFlag : 0) | | 2891 | isHistory ? useCachedContentIfAvailable_DocumentWidgetSetUrlFlag : 0); |
2757 | (fromSidebar ? openedFromSidebar_DocumentWidgetSetUrlFlag : 0)); | ||
2758 | /* Optionally, jump to a text in the document. This will only work if the document | 2892 | /* Optionally, jump to a text in the document. This will only work if the document |
2759 | is already available, e.g., it's from "about:" or restored from cache. */ | 2893 | is already available, e.g., it's from "about:" or restored from cache. */ |
2760 | const iRangecc gotoHeading = range_Command(cmd, "gotoheading"); | 2894 | const iRangecc gotoHeading = range_Command(cmd, "gotoheading"); |
@@ -2895,6 +3029,8 @@ iBool handleCommand_App(const char *cmd) { | |||
2895 | iWidget *dlg = makePreferences_Widget(); | 3029 | iWidget *dlg = makePreferences_Widget(); |
2896 | updatePrefsThemeButtons_(dlg); | 3030 | updatePrefsThemeButtons_(dlg); |
2897 | setText_InputWidget(findChild_Widget(dlg, "prefs.downloads"), &d->prefs.strings[downloadDir_PrefsString]); | 3031 | setText_InputWidget(findChild_Widget(dlg, "prefs.downloads"), &d->prefs.strings[downloadDir_PrefsString]); |
3032 | /* TODO: Use a common table in Prefs to do this more conviently. | ||
3033 | Also see `serializePrefs_App_()`. */ | ||
2898 | setToggle_Widget(findChild_Widget(dlg, "prefs.hoverlink"), d->prefs.hoverLink); | 3034 | setToggle_Widget(findChild_Widget(dlg, "prefs.hoverlink"), d->prefs.hoverLink); |
2899 | setToggle_Widget(findChild_Widget(dlg, "prefs.smoothscroll"), d->prefs.smoothScrolling); | 3035 | setToggle_Widget(findChild_Widget(dlg, "prefs.smoothscroll"), d->prefs.smoothScrolling); |
2900 | setToggle_Widget(findChild_Widget(dlg, "prefs.imageloadscroll"), d->prefs.loadImageInsteadOfScrolling); | 3036 | setToggle_Widget(findChild_Widget(dlg, "prefs.imageloadscroll"), d->prefs.loadImageInsteadOfScrolling); |
@@ -2905,7 +3041,7 @@ iBool handleCommand_App(const char *cmd) { | |||
2905 | setToggle_Widget(findChild_Widget(dlg, "prefs.ostheme"), d->prefs.useSystemTheme); | 3041 | setToggle_Widget(findChild_Widget(dlg, "prefs.ostheme"), d->prefs.useSystemTheme); |
2906 | setToggle_Widget(findChild_Widget(dlg, "prefs.customframe"), d->prefs.customFrame); | 3042 | setToggle_Widget(findChild_Widget(dlg, "prefs.customframe"), d->prefs.customFrame); |
2907 | setToggle_Widget(findChild_Widget(dlg, "prefs.animate"), d->prefs.uiAnimations); | 3043 | setToggle_Widget(findChild_Widget(dlg, "prefs.animate"), d->prefs.uiAnimations); |
2908 | // setText_InputWidget(findChild_Widget(dlg, "prefs.userfont"), &d->prefs.symbolFontPath); | 3044 | setToggle_Widget(findChild_Widget(dlg, "prefs.blink"), d->prefs.blinkingCursor); |
2909 | updatePrefsPinSplitButtons_(dlg, d->prefs.pinSplit); | 3045 | updatePrefsPinSplitButtons_(dlg, d->prefs.pinSplit); |
2910 | updateScrollSpeedButtons_(dlg, mouse_ScrollType, d->prefs.smoothScrollSpeed[mouse_ScrollType]); | 3046 | updateScrollSpeedButtons_(dlg, mouse_ScrollType, d->prefs.smoothScrollSpeed[mouse_ScrollType]); |
2911 | updateScrollSpeedButtons_(dlg, keyboard_ScrollType, d->prefs.smoothScrollSpeed[keyboard_ScrollType]); | 3047 | updateScrollSpeedButtons_(dlg, keyboard_ScrollType, d->prefs.smoothScrollSpeed[keyboard_ScrollType]); |
@@ -2914,16 +3050,11 @@ iBool handleCommand_App(const char *cmd) { | |||
2914 | updateDropdownSelection_LabelWidget( | 3050 | updateDropdownSelection_LabelWidget( |
2915 | findChild_Widget(dlg, "prefs.returnkey"), | 3051 | findChild_Widget(dlg, "prefs.returnkey"), |
2916 | format_CStr("returnkey.set arg:%d", d->prefs.returnKey)); | 3052 | format_CStr("returnkey.set arg:%d", d->prefs.returnKey)); |
3053 | updatePrefsToolBarActionButton_(dlg, 0, d->prefs.toolbarActions[0]); | ||
3054 | updatePrefsToolBarActionButton_(dlg, 1, d->prefs.toolbarActions[1]); | ||
2917 | setToggle_Widget(findChild_Widget(dlg, "prefs.retainwindow"), d->prefs.retainWindowSize); | 3055 | setToggle_Widget(findChild_Widget(dlg, "prefs.retainwindow"), d->prefs.retainWindowSize); |
2918 | setText_InputWidget(findChild_Widget(dlg, "prefs.uiscale"), | 3056 | setText_InputWidget(findChild_Widget(dlg, "prefs.uiscale"), |
2919 | collectNewFormat_String("%g", uiScale_Window(as_Window(d->window)))); | 3057 | collectNewFormat_String("%g", uiScale_Window(as_Window(d->window)))); |
2920 | // setFlags_Widget(findChild_Widget(dlg, format_CStr("prefs.font.%d", d->prefs.font)), | ||
2921 | // selected_WidgetFlag, | ||
2922 | // iTrue); | ||
2923 | // setFlags_Widget( | ||
2924 | // findChild_Widget(dlg, format_CStr("prefs.headingfont.%d", d->prefs.headingFont)), | ||
2925 | // selected_WidgetFlag, | ||
2926 | // iTrue); | ||
2927 | setFlags_Widget(findChild_Widget(dlg, "prefs.mono.gemini"), | 3058 | setFlags_Widget(findChild_Widget(dlg, "prefs.mono.gemini"), |
2928 | selected_WidgetFlag, | 3059 | selected_WidgetFlag, |
2929 | d->prefs.monospaceGemini); | 3060 | d->prefs.monospaceGemini); |
@@ -2990,13 +3121,16 @@ iBool handleCommand_App(const char *cmd) { | |||
2990 | showTabPage_Widget(tabs, tabPage_Widget(tabs, d->prefs.dialogTab)); | 3121 | showTabPage_Widget(tabs, tabPage_Widget(tabs, d->prefs.dialogTab)); |
2991 | } | 3122 | } |
2992 | setCommandHandler_Widget(dlg, handlePrefsCommands_); | 3123 | setCommandHandler_Widget(dlg, handlePrefsCommands_); |
3124 | if (argLabel_Command(cmd, "idents") && deviceType_App() != desktop_AppDeviceType) { | ||
3125 | iWidget *idPanel = panel_Mobile(dlg, 2); | ||
3126 | iWidget *button = findUserData_Widget(findChild_Widget(dlg, "panel.top"), idPanel); | ||
3127 | postCommand_Widget(button, "panel.open"); | ||
3128 | } | ||
2993 | } | 3129 | } |
2994 | else if (equal_Command(cmd, "navigate.home")) { | 3130 | else if (equal_Command(cmd, "navigate.home")) { |
2995 | /* Look for bookmarks tagged "homepage". */ | 3131 | /* Look for bookmarks tagged "homepage". */ |
2996 | iRegExp *pattern = iClob(new_RegExp("\\b" homepage_BookmarkTag "\\b", | ||
2997 | caseInsensitive_RegExpOption)); | ||
2998 | const iPtrArray *homepages = | 3132 | const iPtrArray *homepages = |
2999 | list_Bookmarks(d->bookmarks, NULL, filterTagsRegExp_Bookmarks, pattern); | 3133 | list_Bookmarks(d->bookmarks, NULL, filterHomepage_Bookmark, NULL); |
3000 | if (isEmpty_PtrArray(homepages)) { | 3134 | if (isEmpty_PtrArray(homepages)) { |
3001 | postCommand_Root(get_Root(), "open url:about:lagrange"); | 3135 | postCommand_Root(get_Root(), "open url:about:lagrange"); |
3002 | } | 3136 | } |
@@ -3147,6 +3281,7 @@ iBool handleCommand_App(const char *cmd) { | |||
3147 | d->certs, | 3281 | d->certs, |
3148 | findIdentity_GmCerts(d->certs, collect_Block(hexDecode_Rangecc(range_Command(cmd, "ident")))), | 3282 | findIdentity_GmCerts(d->certs, collect_Block(hexDecode_Rangecc(range_Command(cmd, "ident")))), |
3149 | url); | 3283 | url); |
3284 | postCommand_App("navigate.reload"); | ||
3150 | postCommand_App("idents.changed"); | 3285 | postCommand_App("idents.changed"); |
3151 | return iTrue; | 3286 | return iTrue; |
3152 | } | 3287 | } |
@@ -3159,9 +3294,29 @@ iBool handleCommand_App(const char *cmd) { | |||
3159 | else { | 3294 | else { |
3160 | setUse_GmIdentity(ident, collect_String(suffix_Command(cmd, "url")), iFalse); | 3295 | setUse_GmIdentity(ident, collect_String(suffix_Command(cmd, "url")), iFalse); |
3161 | } | 3296 | } |
3297 | postCommand_App("navigate.reload"); | ||
3162 | postCommand_App("idents.changed"); | 3298 | postCommand_App("idents.changed"); |
3163 | return iTrue; | 3299 | return iTrue; |
3164 | } | 3300 | } |
3301 | else if (equal_Command(cmd, "ident.switch")) { | ||
3302 | /* This is different than "ident.signin" in that the currently used identity's activation | ||
3303 | URL is used instead of the current one. */ | ||
3304 | const iString *docUrl = url_DocumentWidget(document_App()); | ||
3305 | const iGmIdentity *cur = identityForUrl_GmCerts(d->certs, docUrl); | ||
3306 | iGmIdentity *dst = findIdentity_GmCerts( | ||
3307 | d->certs, collect_Block(hexDecode_Rangecc(range_Command(cmd, "fp")))); | ||
3308 | if (dst && cur != dst) { | ||
3309 | iString *useUrl = copy_String(findUse_GmIdentity(cur, docUrl)); | ||
3310 | if (isEmpty_String(useUrl)) { | ||
3311 | useUrl = copy_String(docUrl); | ||
3312 | } | ||
3313 | signIn_GmCerts(d->certs, dst, useUrl); | ||
3314 | postCommand_App("idents.changed"); | ||
3315 | postCommand_App("navigate.reload"); | ||
3316 | delete_String(useUrl); | ||
3317 | } | ||
3318 | return iTrue; | ||
3319 | } | ||
3165 | else if (equal_Command(cmd, "idents.changed")) { | 3320 | else if (equal_Command(cmd, "idents.changed")) { |
3166 | saveIdentities_GmCerts(d->certs); | 3321 | saveIdentities_GmCerts(d->certs); |
3167 | return iFalse; | 3322 | return iFalse; |
@@ -3259,50 +3414,69 @@ void openInDefaultBrowser_App(const iString *url) { | |||
3259 | return; | 3414 | return; |
3260 | } | 3415 | } |
3261 | #endif | 3416 | #endif |
3262 | #if !defined (iPlatformAppleMobile) | 3417 | #if defined (iPlatformAppleMobile) |
3418 | if (equalCase_Rangecc(urlScheme_String(url), "file")) { | ||
3419 | revealPath_App(collect_String(localFilePathFromUrl_String(url))); | ||
3420 | } | ||
3421 | return; | ||
3422 | #endif | ||
3263 | iProcess *proc = new_Process(); | 3423 | iProcess *proc = new_Process(); |
3264 | setArguments_Process(proc, | 3424 | setArguments_Process(proc, iClob(newStringsCStr_StringList( |
3265 | #if defined (iPlatformAppleDesktop) | 3425 | #if defined (iPlatformAppleDesktop) |
3266 | iClob(newStringsCStr_StringList("/usr/bin/env", "open", cstr_String(url), NULL)) | 3426 | "/usr/bin/env", |
3427 | "open", | ||
3428 | cstr_String(url), | ||
3267 | #elif defined (iPlatformLinux) || defined (iPlatformOther) || defined (iPlatformHaiku) | 3429 | #elif defined (iPlatformLinux) || defined (iPlatformOther) || defined (iPlatformHaiku) |
3268 | iClob(newStringsCStr_StringList("/usr/bin/env", "xdg-open", cstr_String(url), NULL)) | 3430 | "/usr/bin/env", |
3431 | "xdg-open", | ||
3432 | cstr_String(url), | ||
3269 | #elif defined (iPlatformMsys) | 3433 | #elif defined (iPlatformMsys) |
3270 | iClob(newStringsCStr_StringList( | 3434 | concatPath_CStr(cstr_String(execPath_App()), "../urlopen.bat"), |
3271 | concatPath_CStr(cstr_String(execPath_App()), "../urlopen.bat"), | 3435 | cstr_String(url), |
3272 | cstr_String(url), | ||
3273 | NULL)) | ||
3274 | /* TODO: The prompt window is shown momentarily... */ | 3436 | /* TODO: The prompt window is shown momentarily... */ |
3275 | #endif | 3437 | #endif |
3438 | NULL)) | ||
3276 | ); | 3439 | ); |
3277 | start_Process(proc); | 3440 | start_Process(proc); |
3278 | waitForFinished_Process(proc); /* TODO: test on Windows */ | 3441 | waitForFinished_Process(proc); |
3279 | iRelease(proc); | 3442 | iRelease(proc); |
3280 | #endif | ||
3281 | } | 3443 | } |
3282 | 3444 | ||
3445 | #include <the_Foundation/thread.h> | ||
3446 | |||
3283 | void revealPath_App(const iString *path) { | 3447 | void revealPath_App(const iString *path) { |
3284 | #if defined (iPlatformAppleDesktop) | 3448 | #if defined (iPlatformAppleDesktop) |
3285 | const char *scriptPath = concatPath_CStr(dataDir_App_(), "revealfile.scpt"); | 3449 | iProcess *proc = new_Process(); |
3286 | iFile *f = newCStr_File(scriptPath); | 3450 | setArguments_Process( |
3287 | if (open_File(f, writeOnly_FileMode | text_FileMode)) { | 3451 | proc, iClob(newStringsCStr_StringList("/usr/bin/open", "-R", cstr_String(path), NULL))); |
3288 | /* AppleScript to select a specific file. */ | 3452 | start_Process(proc); |
3289 | write_File(f, collect_Block(newCStr_Block("on run argv\n" | 3453 | iRelease(proc); |
3290 | " tell application \"Finder\"\n" | 3454 | #elif defined (iPlatformAppleMobile) |
3291 | " activate\n" | 3455 | /* Use a share sheet. */ |
3292 | " reveal POSIX file (item 1 of argv) as text\n" | 3456 | openFileActivityView_iOS(path); |
3293 | " end tell\n" | 3457 | #elif defined (iPlatformLinux) || defined (iPlatformHaiku) |
3294 | "end run\n"))); | 3458 | iProcess *proc = NULL; |
3295 | close_File(f); | 3459 | /* Try with `dbus-send` first. */ { |
3296 | iProcess *proc = new_Process(); | 3460 | proc = new_Process(); |
3297 | setArguments_Process( | 3461 | setArguments_Process( |
3298 | proc, | 3462 | proc, |
3299 | iClob(newStringsCStr_StringList( | 3463 | iClob(newStringsCStr_StringList( |
3300 | "/usr/bin/osascript", scriptPath, cstr_String(path), NULL))); | 3464 | "/usr/bin/dbus-send", |
3465 | "--print-reply", | ||
3466 | "--dest=org.freedesktop.FileManager1", | ||
3467 | "/org/freedesktop/FileManager1", | ||
3468 | "org.freedesktop.FileManager1.ShowItems", | ||
3469 | format_CStr("array:string:%s", makeFileUrl_CStr(cstr_String(path))), | ||
3470 | "string:", | ||
3471 | NULL))); | ||
3301 | start_Process(proc); | 3472 | start_Process(proc); |
3473 | waitForFinished_Process(proc); | ||
3474 | const iBool dbusDidSucceed = (exitStatus_Process(proc) == 0); | ||
3302 | iRelease(proc); | 3475 | iRelease(proc); |
3476 | if (dbusDidSucceed) { | ||
3477 | return; | ||
3478 | } | ||
3303 | } | 3479 | } |
3304 | iRelease(f); | ||
3305 | #elif defined (iPlatformLinux) || defined (iPlatformHaiku) | ||
3306 | iFileInfo *inf = iClob(new_FileInfo(path)); | 3480 | iFileInfo *inf = iClob(new_FileInfo(path)); |
3307 | iRangecc target; | 3481 | iRangecc target; |
3308 | if (isDirectory_FileInfo(inf)) { | 3482 | if (isDirectory_FileInfo(inf)) { |
@@ -3311,7 +3485,7 @@ void revealPath_App(const iString *path) { | |||
3311 | else { | 3485 | else { |
3312 | target = dirName_Path(path); | 3486 | target = dirName_Path(path); |
3313 | } | 3487 | } |
3314 | iProcess *proc = new_Process(); | 3488 | proc = new_Process(); |
3315 | setArguments_Process( | 3489 | setArguments_Process( |
3316 | proc, iClob(newStringsCStr_StringList("/usr/bin/env", "xdg-open", cstr_Rangecc(target), NULL))); | 3490 | proc, iClob(newStringsCStr_StringList("/usr/bin/env", "xdg-open", cstr_Rangecc(target), NULL))); |
3317 | start_Process(proc); | 3491 | start_Process(proc); |
@@ -3365,8 +3539,21 @@ void closePopups_App(void) { | |||
3365 | } | 3539 | } |
3366 | 3540 | ||
3367 | #if defined (iPlatformAndroidMobile) | 3541 | #if defined (iPlatformAndroidMobile) |
3542 | |||
3368 | float displayDensity_Android(void) { | 3543 | float displayDensity_Android(void) { |
3369 | iApp *d = &app_; | 3544 | iApp *d = &app_; |
3370 | return toFloat_String(at_CommandLine(&d->args, 1)); | 3545 | return toFloat_String(at_CommandLine(&d->args, 1)); |
3371 | } | 3546 | } |
3547 | |||
3548 | #include <jni.h> | ||
3549 | |||
3550 | JNIEXPORT void JNICALL Java_fi_skyjake_lagrange_LagrangeActivity_postAppCommand( | ||
3551 | JNIEnv* env, jclass jcls, | ||
3552 | jstring command) | ||
3553 | { | ||
3554 | const char *cmd = (*env)->GetStringUTFChars(env, command, NULL); | ||
3555 | postCommand_Root(NULL, cmd); | ||
3556 | (*env)->ReleaseStringUTFChars(env, command, cmd); | ||
3557 | } | ||
3558 | |||
3372 | #endif | 3559 | #endif |
@@ -61,6 +61,7 @@ enum iUserEventCode { | |||
61 | command_UserEventCode = 1, | 61 | command_UserEventCode = 1, |
62 | refresh_UserEventCode, | 62 | refresh_UserEventCode, |
63 | asleep_UserEventCode, | 63 | asleep_UserEventCode, |
64 | periodic_UserEventCode, | ||
64 | /* The start of a potential touch tap event is notified via a custom event because | 65 | /* The start of a potential touch tap event is notified via a custom event because |
65 | sending SDL_MOUSEBUTTONDOWN would be premature: we don't know how long the tap will | 66 | sending SDL_MOUSEBUTTONDOWN would be premature: we don't know how long the tap will |
66 | take, it could turn into a tap-and-hold for example. */ | 67 | take, it could turn into a tap-and-hold for example. */ |
@@ -110,6 +111,8 @@ enum iColorTheme colorTheme_App (void); | |||
110 | const iString * schemeProxy_App (iRangecc scheme); | 111 | const iString * schemeProxy_App (iRangecc scheme); |
111 | iBool willUseProxy_App (const iRangecc scheme); | 112 | iBool willUseProxy_App (const iRangecc scheme); |
112 | const iString * searchQueryUrl_App (const iString *queryStringUnescaped); | 113 | const iString * searchQueryUrl_App (const iString *queryStringUnescaped); |
114 | const iString * fileNameForUrl_App (const iString *url, const iString *mime); | ||
115 | const iString * temporaryPathForUrl_App(const iString *url, const iString *mime); /* deleted before quitting */ | ||
113 | const iString * downloadPathForUrl_App(const iString *url, const iString *mime); | 116 | const iString * downloadPathForUrl_App(const iString *url, const iString *mime); |
114 | 117 | ||
115 | typedef void (*iTickerFunc)(iAny *); | 118 | typedef void (*iTickerFunc)(iAny *); |
diff --git a/src/audio/player.c b/src/audio/player.c index 94bcd065..de430b17 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. */ | |||
31 | #include <the_Foundation/thread.h> | 31 | #include <the_Foundation/thread.h> |
32 | #include <SDL_audio.h> | 32 | #include <SDL_audio.h> |
33 | #include <SDL_timer.h> | 33 | #include <SDL_timer.h> |
34 | #include <SDL.h> | ||
34 | 35 | ||
35 | #if defined (LAGRANGE_ENABLE_MPG123) | 36 | #if defined (LAGRANGE_ENABLE_MPG123) |
36 | # include <mpg123.h> | 37 | # include <mpg123.h> |
@@ -191,7 +192,7 @@ static enum iDecoderStatus decodeVorbis_Decoder_(iDecoder *d) { | |||
191 | int error; | 192 | int error; |
192 | int consumed; | 193 | int consumed; |
193 | d->vorbis = stb_vorbis_open_pushdata( | 194 | d->vorbis = stb_vorbis_open_pushdata( |
194 | constData_Block(input), size_Block(input), &consumed, &error, NULL); | 195 | constData_Block(input), (int) size_Block(input), &consumed, &error, NULL); |
195 | if (!d->vorbis) { | 196 | if (!d->vorbis) { |
196 | return needMoreInput_DecoderStatus; | 197 | return needMoreInput_DecoderStatus; |
197 | } | 198 | } |
@@ -224,7 +225,7 @@ static enum iDecoderStatus decodeVorbis_Decoder_(iDecoder *d) { | |||
224 | lock_Mutex(&d->input->mtx); | 225 | lock_Mutex(&d->input->mtx); |
225 | d->totalInputSize = size_Block(input); | 226 | d->totalInputSize = size_Block(input); |
226 | int error = 0; | 227 | int error = 0; |
227 | stb_vorbis *vrb = stb_vorbis_open_memory(constData_Block(input), size_Block(input), | 228 | stb_vorbis *vrb = stb_vorbis_open_memory(constData_Block(input), (int) size_Block(input), |
228 | &error, NULL); | 229 | &error, NULL); |
229 | if (vrb) { | 230 | if (vrb) { |
230 | d->totalSamples = stb_vorbis_stream_length_in_samples(vrb); | 231 | d->totalSamples = stb_vorbis_stream_length_in_samples(vrb); |
@@ -739,6 +740,22 @@ size_t sourceDataSize_Player(const iPlayer *d) { | |||
739 | return size; | 740 | return size; |
740 | } | 741 | } |
741 | 742 | ||
743 | static iBool setupSDLAudio_(iBool init) { | ||
744 | static iBool isAudioInited_ = iFalse; | ||
745 | if (init) { | ||
746 | if (SDL_InitSubSystem(SDL_INIT_AUDIO)) { | ||
747 | fprintf(stderr, "[SDL] audio init failed: %s\n", SDL_GetError()); | ||
748 | return iFalse; | ||
749 | } | ||
750 | isAudioInited_ = iTrue; | ||
751 | } | ||
752 | else if (isAudioInited_) { | ||
753 | SDL_QuitSubSystem(SDL_INIT_AUDIO); | ||
754 | isAudioInited_ = iFalse; | ||
755 | } | ||
756 | return isAudioInited_; | ||
757 | } | ||
758 | |||
742 | iBool start_Player(iPlayer *d) { | 759 | iBool start_Player(iPlayer *d) { |
743 | if (isStarted_Player(d)) { | 760 | if (isStarted_Player(d)) { |
744 | return iFalse; | 761 | return iFalse; |
@@ -757,6 +774,9 @@ iBool start_Player(iPlayer *d) { | |||
757 | } | 774 | } |
758 | content.output.callback = writeOutputSamples_Player_; | 775 | content.output.callback = writeOutputSamples_Player_; |
759 | content.output.userdata = d; | 776 | content.output.userdata = d; |
777 | if (!setupSDLAudio_(iTrue)) { | ||
778 | return iFalse; | ||
779 | } | ||
760 | d->device = SDL_OpenAudioDevice(NULL, SDL_FALSE /* playback */, &content.output, &d->spec, 0); | 780 | d->device = SDL_OpenAudioDevice(NULL, SDL_FALSE /* playback */, &content.output, &d->spec, 0); |
761 | if (!d->device) { | 781 | if (!d->device) { |
762 | return iFalse; | 782 | return iFalse; |
@@ -796,6 +816,7 @@ void stop_Player(iPlayer *d) { | |||
796 | d->device = 0; | 816 | d->device = 0; |
797 | delete_Decoder(d->decoder); | 817 | delete_Decoder(d->decoder); |
798 | d->decoder = NULL; | 818 | d->decoder = NULL; |
819 | setupSDLAudio_(iFalse); | ||
799 | } | 820 | } |
800 | } | 821 | } |
801 | 822 | ||
diff --git a/src/bookmarks.c b/src/bookmarks.c index 5e943387..500caa38 100644 --- a/src/bookmarks.c +++ b/src/bookmarks.c | |||
@@ -37,6 +37,7 @@ void init_Bookmark(iBookmark *d) { | |||
37 | init_String(&d->url); | 37 | init_String(&d->url); |
38 | init_String(&d->title); | 38 | init_String(&d->title); |
39 | init_String(&d->tags); | 39 | init_String(&d->tags); |
40 | iZap(d->flags); | ||
40 | iZap(d->when); | 41 | iZap(d->when); |
41 | d->parentId = 0; | 42 | d->parentId = 0; |
42 | d->order = 0; | 43 | d->order = 0; |
@@ -48,6 +49,7 @@ void deinit_Bookmark(iBookmark *d) { | |||
48 | deinit_String(&d->url); | 49 | deinit_String(&d->url); |
49 | } | 50 | } |
50 | 51 | ||
52 | #if 0 | ||
51 | iBool hasTag_Bookmark(const iBookmark *d, const char *tag) { | 53 | iBool hasTag_Bookmark(const iBookmark *d, const char *tag) { |
52 | if (!d) return iFalse; | 54 | if (!d) return iFalse; |
53 | iRegExp *pattern = new_RegExp(format_CStr("\\b%s\\b", tag), caseSensitive_RegExpOption); | 55 | 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) { | |||
60 | 62 | ||
61 | void addTag_Bookmark(iBookmark *d, const char *tag) { | 63 | void addTag_Bookmark(iBookmark *d, const char *tag) { |
62 | if (!isEmpty_String(&d->tags)) { | 64 | if (!isEmpty_String(&d->tags)) { |
63 | appendChar_String(&d->tags, ' '); | 65 | appendCStr_String(&d->tags, " "); |
64 | } | 66 | } |
65 | appendCStr_String(&d->tags, tag); | 67 | appendCStr_String(&d->tags, tag); |
66 | } | 68 | } |
@@ -72,6 +74,93 @@ void removeTag_Bookmark(iBookmark *d, const char *tag) { | |||
72 | trim_String(&d->tags); | 74 | trim_String(&d->tags); |
73 | } | 75 | } |
74 | } | 76 | } |
77 | #endif | ||
78 | |||
79 | static struct { | ||
80 | uint32_t bit; | ||
81 | const char *tag; | ||
82 | iRegExp * pattern; | ||
83 | iRegExp * oldPattern; | ||
84 | } | ||
85 | specialTags_[] = { | ||
86 | { homepage_BookmarkFlag, ".homepage" }, | ||
87 | { remoteSource_BookmarkFlag, ".remotesource" }, | ||
88 | { linkSplit_BookmarkFlag, ".linksplit" }, | ||
89 | { userIcon_BookmarkFlag, ".usericon" }, | ||
90 | { subscribed_BookmarkFlag, ".subscribed" }, | ||
91 | { headings_BookmarkFlag, ".headings" }, | ||
92 | { ignoreWeb_BookmarkFlag, ".ignoreweb" }, | ||
93 | /* `remote_BookmarkFlag` not included because it's runtime only */ | ||
94 | }; | ||
95 | |||
96 | static void updatePatterns_(size_t index) { | ||
97 | if (!specialTags_[index].pattern) { | ||
98 | specialTags_[index].pattern = new_RegExp(format_CStr("(?<!\\w)\\%s\\b(?!\\w)", | ||
99 | specialTags_[index].tag), | ||
100 | caseSensitive_RegExpOption); /* never released */ | ||
101 | } | ||
102 | if (!specialTags_[index].oldPattern) { | ||
103 | /* TODO: Get rid of these when compatibility with v1.9 or older is not important. */ | ||
104 | specialTags_[index].oldPattern = | ||
105 | new_RegExp(format_CStr("\\b%s\\b", specialTags_[index].tag + 1), /* dotless */ | ||
106 | caseSensitive_RegExpOption); /* never released */ | ||
107 | } | ||
108 | } | ||
109 | |||
110 | static void normalizeSpacesInTags_(iString *tags) { | ||
111 | iBool wasSpace = iFalse; | ||
112 | iString out; | ||
113 | init_String(&out); | ||
114 | for (const char *ch = constBegin_String(tags); ch != constEnd_String(tags); ch++) { | ||
115 | if (*ch == ' ') { | ||
116 | if (!wasSpace) { | ||
117 | wasSpace = iTrue; | ||
118 | } | ||
119 | else { | ||
120 | continue; | ||
121 | } | ||
122 | } | ||
123 | else { | ||
124 | wasSpace = iFalse; | ||
125 | } | ||
126 | appendData_Block(&out.chars, ch, 1); | ||
127 | } | ||
128 | trim_String(&out); | ||
129 | set_String(tags, &out); | ||
130 | deinit_String(&out); | ||
131 | } | ||
132 | |||
133 | static void unpackDotTags_Bookmark_(iBookmark *d) { | ||
134 | iZap(d->flags); | ||
135 | iForIndices(i, specialTags_) { | ||
136 | updatePatterns_(i); | ||
137 | iRegExpMatch m; | ||
138 | init_RegExpMatch(&m); | ||
139 | iBool isSet = matchString_RegExp(specialTags_[i].pattern, &d->tags, &m); | ||
140 | if (!isSet) { | ||
141 | init_RegExpMatch(&m); | ||
142 | isSet = matchString_RegExp(specialTags_[i].oldPattern, &d->tags, &m); | ||
143 | } | ||
144 | iChangeFlags(d->flags, specialTags_[i].bit, isSet); | ||
145 | if (isSet) { | ||
146 | remove_Block(&d->tags.chars, m.range.start, size_Range(&m.range)); | ||
147 | } | ||
148 | } | ||
149 | normalizeSpacesInTags_(&d->tags); | ||
150 | } | ||
151 | |||
152 | static iString *packedDotTags_Bookmark_(const iBookmark *d) { | ||
153 | iString *withDot = copy_String(&d->tags); | ||
154 | iForIndices(i, specialTags_) { | ||
155 | if (d->flags & specialTags_[i].bit) { | ||
156 | if (!isEmpty_String(withDot)) { | ||
157 | appendCStr_String(withDot, " "); | ||
158 | } | ||
159 | appendCStr_String(withDot, specialTags_[i].tag); | ||
160 | } | ||
161 | } | ||
162 | return withDot; | ||
163 | } | ||
75 | 164 | ||
76 | iDefineTypeConstruction(Bookmark) | 165 | iDefineTypeConstruction(Bookmark) |
77 | 166 | ||
@@ -176,6 +265,7 @@ static void loadOldFormat_Bookmarks(iBookmarks *d, const char *dirPath) { | |||
176 | setRange_String(&bm->title, line); | 265 | setRange_String(&bm->title, line); |
177 | nextSplit_Rangecc(src, "\n", &line); | 266 | nextSplit_Rangecc(src, "\n", &line); |
178 | setRange_String(&bm->tags, line); | 267 | setRange_String(&bm->tags, line); |
268 | unpackDotTags_Bookmark_(bm); | ||
179 | insert_Bookmarks_(d, bm); | 269 | insert_Bookmarks_(d, bm); |
180 | } | 270 | } |
181 | } | 271 | } |
@@ -220,6 +310,7 @@ static void handleKeyValue_BookmarkLoader_(void *context, const iString *table, | |||
220 | } | 310 | } |
221 | else if (!cmp_String(key, "tags") && tv->type == string_TomlType) { | 311 | else if (!cmp_String(key, "tags") && tv->type == string_TomlType) { |
222 | set_String(&bm->tags, tv->value.string); | 312 | set_String(&bm->tags, tv->value.string); |
313 | unpackDotTags_Bookmark_(bm); | ||
223 | } | 314 | } |
224 | else if (!cmp_String(key, "icon") && tv->type == int64_TomlType) { | 315 | else if (!cmp_String(key, "icon") && tv->type == int64_TomlType) { |
225 | bm->icon = (iChar) tv->value.int64; | 316 | bm->icon = (iChar) tv->value.int64; |
@@ -292,7 +383,6 @@ void load_Bookmarks(iBookmarks *d, const char *dirPath) { | |||
292 | 383 | ||
293 | void save_Bookmarks(const iBookmarks *d, const char *dirPath) { | 384 | void save_Bookmarks(const iBookmarks *d, const char *dirPath) { |
294 | lock_Mutex(d->mtx); | 385 | lock_Mutex(d->mtx); |
295 | iRegExp *remotePattern = iClob(new_RegExp("\\bremote\\b", caseSensitive_RegExpOption)); | ||
296 | iFile *f = newCStr_File(concatPath_CStr(dirPath, fileName_Bookmarks_)); | 386 | iFile *f = newCStr_File(concatPath_CStr(dirPath, fileName_Bookmarks_)); |
297 | if (open_File(f, writeOnly_FileMode | text_FileMode)) { | 387 | if (open_File(f, writeOnly_FileMode | text_FileMode)) { |
298 | iString *str = collectNew_String(); | 388 | iString *str = collectNew_String(); |
@@ -300,13 +390,12 @@ void save_Bookmarks(const iBookmarks *d, const char *dirPath) { | |||
300 | writeData_File(f, cstr_String(str), size_String(str)); | 390 | writeData_File(f, cstr_String(str), size_String(str)); |
301 | iConstForEach(Hash, i, &d->bookmarks) { | 391 | iConstForEach(Hash, i, &d->bookmarks) { |
302 | const iBookmark *bm = (const iBookmark *) i.value; | 392 | const iBookmark *bm = (const iBookmark *) i.value; |
303 | iRegExpMatch m; | 393 | if (bm->flags & remote_BookmarkFlag) { |
304 | init_RegExpMatch(&m); | ||
305 | if (matchString_RegExp(remotePattern, &bm->tags, &m)) { | ||
306 | /* Remote bookmarks are not saved. */ | 394 | /* Remote bookmarks are not saved. */ |
307 | continue; | 395 | continue; |
308 | } | 396 | } |
309 | iBeginCollect(); | 397 | iBeginCollect(); |
398 | const iString *packedTags = collect_String(packedDotTags_Bookmark_(bm)); | ||
310 | format_String(str, | 399 | format_String(str, |
311 | "[%d]\n" | 400 | "[%d]\n" |
312 | "url = \"%s\"\n" | 401 | "url = \"%s\"\n" |
@@ -317,7 +406,7 @@ void save_Bookmarks(const iBookmarks *d, const char *dirPath) { | |||
317 | id_Bookmark(bm), | 406 | id_Bookmark(bm), |
318 | cstrCollect_String(quote_String(&bm->url, iFalse)), | 407 | cstrCollect_String(quote_String(&bm->url, iFalse)), |
319 | cstrCollect_String(quote_String(&bm->title, iFalse)), | 408 | cstrCollect_String(quote_String(&bm->title, iFalse)), |
320 | cstrCollect_String(quote_String(&bm->tags, iFalse)), | 409 | cstrCollect_String(quote_String(packedTags, iFalse)), |
321 | bm->icon, | 410 | bm->icon, |
322 | seconds_Time(&bm->when), | 411 | seconds_Time(&bm->when), |
323 | cstrCollect_String(format_Time(&bm->when, "%Y-%m-%d"))); | 412 | cstrCollect_String(format_Time(&bm->when, "%Y-%m-%d"))); |
@@ -397,7 +486,7 @@ iBool updateBookmarkIcon_Bookmarks(iBookmarks *d, const iString *url, iChar icon | |||
397 | const uint32_t id = findUrl_Bookmarks(d, url); | 486 | const uint32_t id = findUrl_Bookmarks(d, url); |
398 | if (id) { | 487 | if (id) { |
399 | iBookmark *bm = get_Bookmarks(d, id); | 488 | iBookmark *bm = get_Bookmarks(d, id); |
400 | if (!hasTag_Bookmark(bm, remote_BookmarkTag) && !hasTag_Bookmark(bm, userIcon_BookmarkTag)) { | 489 | if (~bm->flags & remote_BookmarkFlag && ~bm->flags & userIcon_BookmarkFlag) { |
401 | if (icon != bm->icon) { | 490 | if (icon != bm->icon) { |
402 | bm->icon = icon; | 491 | bm->icon = icon; |
403 | changed = iTrue; | 492 | changed = iTrue; |
@@ -422,19 +511,13 @@ iChar siteIcon_Bookmarks(const iBookmarks *d, const iString *url) { | |||
422 | if (isEmpty_String(url)) { | 511 | if (isEmpty_String(url)) { |
423 | return 0; | 512 | return 0; |
424 | } | 513 | } |
425 | static iRegExp *tagPattern_; | ||
426 | if (!tagPattern_) { | ||
427 | tagPattern_ = new_RegExp("\\b" userIcon_BookmarkTag "\\b", caseSensitive_RegExpOption); | ||
428 | } | ||
429 | const iRangecc urlRoot = urlRoot_String(url); | 514 | const iRangecc urlRoot = urlRoot_String(url); |
430 | size_t matchingSize = iInvalidSize; /* we'll pick the shortest matching */ | 515 | size_t matchingSize = iInvalidSize; /* we'll pick the shortest matching */ |
431 | iChar icon = 0; | 516 | iChar icon = 0; |
432 | lock_Mutex(d->mtx); | 517 | lock_Mutex(d->mtx); |
433 | iConstForEach(Hash, i, &d->bookmarks) { | 518 | iConstForEach(Hash, i, &d->bookmarks) { |
434 | const iBookmark *bm = (const iBookmark *) i.value; | 519 | const iBookmark *bm = (const iBookmark *) i.value; |
435 | iRegExpMatch m; | 520 | if (bm->icon && bm->flags & userIcon_BookmarkFlag) { |
436 | init_RegExpMatch(&m); | ||
437 | if (bm->icon && matchString_RegExp(tagPattern_, &bm->tags, &m)) { | ||
438 | const iRangecc bmRoot = urlRoot_String(&bm->url); | 521 | const iRangecc bmRoot = urlRoot_String(&bm->url); |
439 | if (equalRangeCase_Rangecc(urlRoot, bmRoot)) { | 522 | if (equalRangeCase_Rangecc(urlRoot, bmRoot)) { |
440 | const size_t n = size_String(&bm->url); | 523 | const size_t n = size_String(&bm->url); |
@@ -467,10 +550,15 @@ void reorder_Bookmarks(iBookmarks *d, uint32_t id, int newOrder) { | |||
467 | unlock_Mutex(d->mtx); | 550 | unlock_Mutex(d->mtx); |
468 | } | 551 | } |
469 | 552 | ||
470 | iBool filterTagsRegExp_Bookmarks(void *regExp, const iBookmark *bm) { | 553 | //iBool filterTagsRegExp_Bookmarks(void *regExp, const iBookmark *bm) { |
471 | iRegExpMatch m; | 554 | // iRegExpMatch m; |
472 | init_RegExpMatch(&m); | 555 | // init_RegExpMatch(&m); |
473 | return matchString_RegExp(regExp, &bm->tags, &m); | 556 | // return matchString_RegExp(regExp, &bm->tags, &m); |
557 | //} | ||
558 | |||
559 | iBool filterHomepage_Bookmark(void *d, const iBookmark *bm) { | ||
560 | iUnused(d); | ||
561 | return (bm->flags & homepage_BookmarkFlag) != 0; | ||
474 | } | 562 | } |
475 | 563 | ||
476 | static iBool matchUrl_(void *url, const iBookmark *bm) { | 564 | static iBool matchUrl_(void *url, const iBookmark *bm) { |
@@ -618,7 +706,7 @@ const iString *bookmarkListPage_Bookmarks(const iBookmarks *d, enum iBookmarkLis | |||
618 | 706 | ||
619 | static iBool isRemoteSource_Bookmark_(void *context, const iBookmark *d) { | 707 | static iBool isRemoteSource_Bookmark_(void *context, const iBookmark *d) { |
620 | iUnused(context); | 708 | iUnused(context); |
621 | return hasTag_Bookmark(d, remoteSource_BookmarkTag); | 709 | return (d->flags & remoteSource_BookmarkFlag) != 0; |
622 | } | 710 | } |
623 | 711 | ||
624 | void remoteRequestFinished_Bookmarks_(iBookmarks *d, iGmRequest *req) { | 712 | void remoteRequestFinished_Bookmarks_(iBookmarks *d, iGmRequest *req) { |
@@ -642,7 +730,6 @@ void requestFinished_Bookmarks(iBookmarks *d, iGmRequest *req) { | |||
642 | initCurrent_Time(&now); | 730 | initCurrent_Time(&now); |
643 | iRegExp *linkPattern = new_RegExp("^=>\\s*([^\\s]+)(\\s+(.*))?", 0); | 731 | iRegExp *linkPattern = new_RegExp("^=>\\s*([^\\s]+)(\\s+(.*))?", 0); |
644 | iString src; | 732 | iString src; |
645 | const iString *remoteTag = collectNewCStr_String("remote"); | ||
646 | initBlock_String(&src, body_GmRequest(req)); | 733 | initBlock_String(&src, body_GmRequest(req)); |
647 | iRangecc srcLine = iNullRange; | 734 | iRangecc srcLine = iNullRange; |
648 | while (nextSplit_Rangecc(range_String(&src), "\n", &srcLine)) { | 735 | while (nextSplit_Rangecc(range_String(&src), "\n", &srcLine)) { |
@@ -660,8 +747,9 @@ void requestFinished_Bookmarks(iBookmarks *d, iGmRequest *req) { | |||
660 | if (isEmpty_String(titleStr)) { | 747 | if (isEmpty_String(titleStr)) { |
661 | setRange_String(titleStr, urlHost_String(urlStr)); | 748 | setRange_String(titleStr, urlHost_String(urlStr)); |
662 | } | 749 | } |
663 | const uint32_t bmId = add_Bookmarks(d, absUrl, titleStr, remoteTag, 0x2913); | 750 | const uint32_t bmId = add_Bookmarks(d, absUrl, titleStr, NULL, 0x2913); |
664 | iBookmark *bm = get_Bookmarks(d, bmId); | 751 | iBookmark *bm = get_Bookmarks(d, bmId); |
752 | bm->flags |= remote_BookmarkFlag; | ||
665 | bm->parentId = *(uint32_t *) userData_Object(req); | 753 | bm->parentId = *(uint32_t *) userData_Object(req); |
666 | delete_String(titleStr); | 754 | delete_String(titleStr); |
667 | } | 755 | } |
@@ -690,7 +778,7 @@ void fetchRemote_Bookmarks(iBookmarks *d) { | |||
690 | size_t numRemoved = 0; | 778 | size_t numRemoved = 0; |
691 | iForEach(Hash, i, &d->bookmarks) { | 779 | iForEach(Hash, i, &d->bookmarks) { |
692 | iBookmark *bm = (iBookmark *) i.value; | 780 | iBookmark *bm = (iBookmark *) i.value; |
693 | if (hasTag_Bookmark(bm, remote_BookmarkTag)) { | 781 | if (bm->flags & remote_BookmarkFlag) { |
694 | remove_HashIterator(&i); | 782 | remove_HashIterator(&i); |
695 | delete_Bookmark(bm); | 783 | delete_Bookmark(bm); |
696 | numRemoved++; | 784 | 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) | |||
31 | 31 | ||
32 | iDeclareType(Bookmark) | 32 | iDeclareType(Bookmark) |
33 | iDeclareTypeConstruction(Bookmark) | 33 | iDeclareTypeConstruction(Bookmark) |
34 | 34 | ||
35 | /* TODO: Make the special internal tags a bitfield, separate from user's tags. */ | 35 | /* These values are not serialized as-is in bookmarks.ini. Instead, they are included in `tags` |
36 | 36 | with a dot prefix. This helps retain backwards and forwards compatibility. */ | |
37 | #define headings_BookmarkTag "headings" | 37 | enum iBookmarkFlags { |
38 | #define ignoreWeb_BookmarkTag "ignoreweb" | 38 | homepage_BookmarkFlag = iBit(1), |
39 | #define homepage_BookmarkTag "homepage" | 39 | remoteSource_BookmarkFlag = iBit(2), |
40 | #define linkSplit_BookmarkTag "linksplit" | 40 | linkSplit_BookmarkFlag = iBit(3), |
41 | #define remote_BookmarkTag "remote" | 41 | userIcon_BookmarkFlag = iBit(4), |
42 | #define remoteSource_BookmarkTag "remotesource" | 42 | subscribed_BookmarkFlag = iBit(17), |
43 | #define subscribed_BookmarkTag "subscribed" | 43 | headings_BookmarkFlag = iBit(18), |
44 | #define userIcon_BookmarkTag "usericon" | 44 | ignoreWeb_BookmarkFlag = iBit(19), |
45 | remote_BookmarkFlag = iBit(31), | ||
46 | }; | ||
45 | 47 | ||
46 | struct Impl_Bookmark { | 48 | struct Impl_Bookmark { |
47 | iHashNode node; | 49 | iHashNode node; |
48 | iString url; | 50 | iString url; |
49 | iString title; | 51 | iString title; |
50 | iString tags; | 52 | iString tags; |
51 | iChar icon; | 53 | uint32_t flags; |
52 | iTime when; | 54 | iChar icon; |
53 | uint32_t parentId; /* remote source or folder */ | 55 | iTime when; |
54 | int order; /* sort order */ | 56 | uint32_t parentId; /* remote source or folder */ |
57 | int order; /* sort order */ | ||
55 | }; | 58 | }; |
56 | 59 | ||
57 | iLocalDef uint32_t id_Bookmark (const iBookmark *d) { return d->node.key; } | 60 | 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 | |||
59 | 62 | ||
60 | iBool hasParent_Bookmark (const iBookmark *, uint32_t parentId); | 63 | iBool hasParent_Bookmark (const iBookmark *, uint32_t parentId); |
61 | int depth_Bookmark (const iBookmark *); | 64 | int depth_Bookmark (const iBookmark *); |
62 | iBool hasTag_Bookmark (const iBookmark *, const char *tag); | 65 | |
63 | void addTag_Bookmark (iBookmark *, const char *tag); | 66 | //iBool hasTag_Bookmark (const iBookmark *, const char *tag); |
64 | void removeTag_Bookmark (iBookmark *, const char *tag); | 67 | //void addTag_Bookmark (iBookmark *, const char *tag); |
65 | 68 | //void removeTag_Bookmark (iBookmark *, const char *tag); | |
66 | iLocalDef void addTagIfMissing_Bookmark(iBookmark *d, const char *tag) { | 69 | |
67 | if (!hasTag_Bookmark(d, tag)) { | 70 | //iLocalDef void addTagIfMissing_Bookmark(iBookmark *d, const char *tag) { |
68 | addTag_Bookmark(d, tag); | 71 | // if (!hasTag_Bookmark(d, tag)) { |
69 | } | 72 | // addTag_Bookmark(d, tag); |
70 | } | 73 | // } |
71 | iLocalDef void addOrRemoveTag_Bookmark(iBookmark *d, const char *tag, iBool add) { | 74 | //} |
72 | if (add) { | 75 | //iLocalDef void addOrRemoveTag_Bookmark(iBookmark *d, const char *tag, iBool add) { |
73 | addTagIfMissing_Bookmark(d, tag); | 76 | // if (add) { |
74 | } | 77 | // addTagIfMissing_Bookmark(d, tag); |
75 | else { | 78 | // } |
76 | removeTag_Bookmark(d, tag); | 79 | // else { |
77 | } | 80 | // removeTag_Bookmark(d, tag); |
78 | } | 81 | // } |
82 | //} | ||
79 | 83 | ||
80 | int cmpTitleAscending_Bookmark (const iBookmark **, const iBookmark **); | 84 | int cmpTitleAscending_Bookmark (const iBookmark **, const iBookmark **); |
81 | int cmpTree_Bookmark (const iBookmark **, const iBookmark **); | 85 | int cmpTree_Bookmark (const iBookmark **, const iBookmark **); |
@@ -109,7 +113,8 @@ iChar siteIcon_Bookmarks (const iBookmarks *, const iString *url) | |||
109 | uint32_t findUrl_Bookmarks (const iBookmarks *, const iString *url); /* O(n) */ | 113 | uint32_t findUrl_Bookmarks (const iBookmarks *, const iString *url); /* O(n) */ |
110 | uint32_t recentFolder_Bookmarks (const iBookmarks *); | 114 | uint32_t recentFolder_Bookmarks (const iBookmarks *); |
111 | 115 | ||
112 | iBool filterTagsRegExp_Bookmarks (void *regExp, const iBookmark *); | 116 | //iBool filterTagsRegExp_Bookmarks (void *regExp, const iBookmark *); |
117 | iBool filterHomepage_Bookmark (void *, const iBookmark *); | ||
113 | 118 | ||
114 | /** | 119 | /** |
115 | * Lists all or a subset of the bookmarks in a sorted array of Bookmark pointers. | 120 | * Lists all or a subset of the bookmarks in a sorted array of Bookmark pointers. |
@@ -66,10 +66,28 @@ enum iReturnKeyFlag { | |||
66 | accept_ReturnKeyFlag = 4, /* shift */ | 66 | accept_ReturnKeyFlag = 4, /* shift */ |
67 | }; | 67 | }; |
68 | 68 | ||
69 | enum iToolbarAction { | ||
70 | back_ToolbarAction = 0, | ||
71 | forward_ToolbarAction = 1, | ||
72 | home_ToolbarAction = 2, | ||
73 | parent_ToolbarAction = 3, | ||
74 | reload_ToolbarAction = 4, | ||
75 | newTab_ToolbarAction = 5, | ||
76 | closeTab_ToolbarAction = 6, | ||
77 | addBookmark_ToolbarAction = 7, | ||
78 | translate_ToolbarAction = 8, | ||
79 | upload_ToolbarAction = 9, | ||
80 | editPage_ToolbarAction = 10, | ||
81 | findText_ToolbarAction = 11, | ||
82 | settings_ToolbarAction = 12, | ||
83 | sidebar_ToolbarAction = 13, /* desktop only */ | ||
84 | max_ToolbarAction | ||
85 | }; | ||
86 | |||
69 | /* Return key behavior is not handled via normal bindings because only certain combinations | 87 | /* Return key behavior is not handled via normal bindings because only certain combinations |
70 | are valid. */ | 88 | are valid. */ |
71 | enum iReturnKeyBehavior { | 89 | enum iReturnKeyBehavior { |
72 | default_ReturnKeyBehavior = | 90 | acceptWithoutMod_ReturnKeyBehavior = |
73 | shiftReturn_ReturnKeyFlag | (return_ReturnKeyFlag << accept_ReturnKeyFlag), | 91 | shiftReturn_ReturnKeyFlag | (return_ReturnKeyFlag << accept_ReturnKeyFlag), |
74 | acceptWithShift_ReturnKeyBehavior = | 92 | acceptWithShift_ReturnKeyBehavior = |
75 | return_ReturnKeyFlag | (shiftReturn_ReturnKeyFlag << accept_ReturnKeyFlag), | 93 | return_ReturnKeyFlag | (shiftReturn_ReturnKeyFlag << accept_ReturnKeyFlag), |
@@ -79,6 +97,11 @@ enum iReturnKeyBehavior { | |||
79 | #else | 97 | #else |
80 | return_ReturnKeyFlag | (controlReturn_ReturnKeyFlag << accept_ReturnKeyFlag), | 98 | return_ReturnKeyFlag | (controlReturn_ReturnKeyFlag << accept_ReturnKeyFlag), |
81 | #endif | 99 | #endif |
100 | #if defined (iPlatformAndroidMobile) | ||
101 | default_ReturnKeyBehavior = acceptWithShift_ReturnKeyBehavior, | ||
102 | #else | ||
103 | default_ReturnKeyBehavior = acceptWithoutMod_ReturnKeyBehavior, | ||
104 | #endif | ||
82 | }; | 105 | }; |
83 | 106 | ||
84 | int keyMod_ReturnKeyFlag (int flag); | 107 | int keyMod_ReturnKeyFlag (int flag); |
@@ -94,7 +117,7 @@ iLocalDef int acceptKeyMod_ReturnKeyBehavior(int behavior) { | |||
94 | 117 | ||
95 | #define menu_Icon "\U0001d362" | 118 | #define menu_Icon "\U0001d362" |
96 | #define rightArrowhead_Icon "\u27a4" | 119 | #define rightArrowhead_Icon "\u27a4" |
97 | #define leftArrowhead_Icon "\u27a4" | 120 | #define leftArrowhead_Icon "\u2b9c" |
98 | #define warning_Icon "\u26a0" | 121 | #define warning_Icon "\u26a0" |
99 | #define openLock_Icon "\U0001f513" | 122 | #define openLock_Icon "\U0001f513" |
100 | #define closedLock_Icon "\U0001f512" | 123 | #define closedLock_Icon "\U0001f512" |
@@ -102,7 +125,7 @@ iLocalDef int acceptKeyMod_ReturnKeyBehavior(int behavior) { | |||
102 | #define reload_Icon "\U0001f503" | 125 | #define reload_Icon "\U0001f503" |
103 | #define backArrow_Icon "\U0001f870" | 126 | #define backArrow_Icon "\U0001f870" |
104 | #define forwardArrow_Icon "\U0001f872" | 127 | #define forwardArrow_Icon "\U0001f872" |
105 | #define upArrow_Icon "\u2191" | 128 | #define upArrow_Icon "\U0001f871" |
106 | #define upArrowBar_Icon "\u2912" | 129 | #define upArrowBar_Icon "\u2912" |
107 | #define downArrowBar_Icon "\u2913" | 130 | #define downArrowBar_Icon "\u2913" |
108 | #define rightArrowWhite_Icon "\u21e8" | 131 | #define rightArrowWhite_Icon "\u21e8" |
diff --git a/src/feeds.c b/src/feeds.c index 26b3d6db..a8cbf47a 100644 --- a/src/feeds.c +++ b/src/feeds.c | |||
@@ -65,7 +65,9 @@ const iString *url_FeedEntry(const iFeedEntry *d) { | |||
65 | iBool isUnread_FeedEntry(const iFeedEntry *d) { | 65 | iBool isUnread_FeedEntry(const iFeedEntry *d) { |
66 | const size_t fragPos = indexOf_String(&d->url, '#'); | 66 | const size_t fragPos = indexOf_String(&d->url, '#'); |
67 | if (fragPos != iInvalidPos) { | 67 | if (fragPos != iInvalidPos) { |
68 | /* Check if the entry is newer than the latest visit. */ | 68 | /* Check if the entry is newer than the latest visit. If the URL has not been visited, |
69 | `urlVisitTime_Visited` returns a zero timestamp that is always earlier than | ||
70 | `posted`. */ | ||
69 | const iTime visTime = urlVisitTime_Visited(visited_App(), url_FeedEntry(d)); | 71 | const iTime visTime = urlVisitTime_Visited(visited_App(), url_FeedEntry(d)); |
70 | return cmp_Time(&visTime, &d->posted) < 0; | 72 | return cmp_Time(&visTime, &d->posted) < 0; |
71 | } | 73 | } |
@@ -97,8 +99,8 @@ static void init_FeedJob(iFeedJob *d, const iBookmark *bookmark) { | |||
97 | init_PtrArray(&d->results); | 99 | init_PtrArray(&d->results); |
98 | iZap(d->startTime); | 100 | iZap(d->startTime); |
99 | d->isFirstUpdate = iFalse; | 101 | d->isFirstUpdate = iFalse; |
100 | d->checkHeadings = hasTag_Bookmark(bookmark, headings_BookmarkTag); | 102 | d->checkHeadings = (bookmark->flags & headings_BookmarkFlag) != 0; |
101 | d->ignoreWeb = hasTag_Bookmark(bookmark, ignoreWeb_BookmarkTag); | 103 | d->ignoreWeb = (bookmark->flags & ignoreWeb_BookmarkFlag) != 0; |
102 | } | 104 | } |
103 | 105 | ||
104 | static void deinit_FeedJob(iFeedJob *d) { | 106 | static void deinit_FeedJob(iFeedJob *d) { |
@@ -146,13 +148,7 @@ static void submit_FeedJob_(iFeedJob *d) { | |||
146 | 148 | ||
147 | static iBool isSubscribed_(void *context, const iBookmark *bm) { | 149 | static iBool isSubscribed_(void *context, const iBookmark *bm) { |
148 | iUnused(context); | 150 | iUnused(context); |
149 | static iRegExp *pattern_ = NULL; | 151 | return (bm->flags & subscribed_BookmarkFlag) != 0; |
150 | if (!pattern_) { | ||
151 | pattern_ = new_RegExp("\\bsubscribed\\b", caseSensitive_RegExpOption); | ||
152 | } | ||
153 | iRegExpMatch m; | ||
154 | init_RegExpMatch(&m); | ||
155 | return matchString_RegExp(pattern_, &bm->tags, &m); | ||
156 | } | 152 | } |
157 | 153 | ||
158 | static const iPtrArray *listSubscriptions_(void) { | 154 | static const iPtrArray *listSubscriptions_(void) { |
@@ -443,7 +439,7 @@ static iThreadResult fetch_Feeds_(iThread *thread) { | |||
443 | iZap(work); | 439 | iZap(work); |
444 | iBool gotNew = iFalse; | 440 | iBool gotNew = iFalse; |
445 | postCommand_App("feeds.update.started"); | 441 | postCommand_App("feeds.update.started"); |
446 | const int totalJobs = size_PtrArray(&d->jobs); | 442 | const size_t totalJobs = size_PtrArray(&d->jobs); |
447 | int numFinishedJobs = 0; | 443 | int numFinishedJobs = 0; |
448 | while (!d->stopWorker) { | 444 | while (!d->stopWorker) { |
449 | /* Start new jobs. */ | 445 | /* Start new jobs. */ |
@@ -482,7 +478,7 @@ static iThreadResult fetch_Feeds_(iThread *thread) { | |||
482 | } | 478 | } |
483 | } | 479 | } |
484 | if (doNotify) { | 480 | if (doNotify) { |
485 | postCommandf_App("feeds.update.progress arg:%d total:%d", numFinishedJobs, totalJobs); | 481 | postCommandf_App("feeds.update.progress arg:%d total:%zu", numFinishedJobs, totalJobs); |
486 | } | 482 | } |
487 | /* Stop if everything has finished. */ | 483 | /* Stop if everything has finished. */ |
488 | if (ongoing == 0 && isEmpty_PtrArray(&d->jobs)) { | 484 | if (ongoing == 0 && isEmpty_PtrArray(&d->jobs)) { |
@@ -620,7 +616,7 @@ static void load_Feeds_(iFeeds *d) { | |||
620 | /* TODO: Cleanup needed... | 616 | /* TODO: Cleanup needed... |
621 | All right, this could maybe use a bit more robust, structured format. | 617 | All right, this could maybe use a bit more robust, structured format. |
622 | The code below is messy. */ | 618 | The code below is messy. */ |
623 | const uint32_t feedId = strtoul(line.start, NULL, 16); | 619 | const uint32_t feedId = (uint32_t) strtoul(line.start, NULL, 16); |
624 | if (!nextSplit_Rangecc(range_Block(src), "\n", &line)) { | 620 | if (!nextSplit_Rangecc(range_Block(src), "\n", &line)) { |
625 | goto aborted; | 621 | goto aborted; |
626 | } | 622 | } |
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) { | |||
818 | pushBack_Array( | 818 | pushBack_Array( |
819 | items, | 819 | items, |
820 | &(iMenuItem){ format_Lang(isEnabled ? close_Icon " ${fontpack.disable}" | 820 | &(iMenuItem){ format_Lang(isEnabled ? close_Icon " ${fontpack.disable}" |
821 | : leftArrowhead_Icon " ${fontpack.enable}", | 821 | : "${fontpack.enable}", |
822 | fpId), | 822 | fpId), |
823 | 0, | 823 | 0, |
824 | 0, | 824 | 0, |
diff --git a/src/gmcerts.c b/src/gmcerts.c index f95fea7d..7b05103b 100644 --- a/src/gmcerts.c +++ b/src/gmcerts.c | |||
@@ -90,7 +90,7 @@ void serialize_GmIdentity(const iGmIdentity *d, iStream *outs) { | |||
90 | writeU32_Stream(outs, d->icon); | 90 | writeU32_Stream(outs, d->icon); |
91 | serialize_String(&d->notes, outs); | 91 | serialize_String(&d->notes, outs); |
92 | write32_Stream(outs, d->flags); | 92 | write32_Stream(outs, d->flags); |
93 | writeU32_Stream(outs, size_StringSet(d->useUrls)); | 93 | writeU32_Stream(outs, (uint32_t) size_StringSet(d->useUrls)); |
94 | iConstForEach(StringSet, i, d->useUrls) { | 94 | iConstForEach(StringSet, i, d->useUrls) { |
95 | serialize_String(i.value, outs); | 95 | serialize_String(i.value, outs); |
96 | } | 96 | } |
@@ -146,6 +146,7 @@ iBool isUsed_GmIdentity(const iGmIdentity *d) { | |||
146 | } | 146 | } |
147 | 147 | ||
148 | iBool isUsedOn_GmIdentity(const iGmIdentity *d, const iString *url) { | 148 | iBool isUsedOn_GmIdentity(const iGmIdentity *d, const iString *url) { |
149 | #if 0 | ||
149 | size_t pos = iInvalidPos; | 150 | size_t pos = iInvalidPos; |
150 | locate_StringSet(d->useUrls, url, &pos); | 151 | locate_StringSet(d->useUrls, url, &pos); |
151 | if (pos < size_StringSet(d->useUrls)) { | 152 | if (pos < size_StringSet(d->useUrls)) { |
@@ -159,6 +160,12 @@ iBool isUsedOn_GmIdentity(const iGmIdentity *d, const iString *url) { | |||
159 | return iTrue; | 160 | return iTrue; |
160 | } | 161 | } |
161 | } | 162 | } |
163 | #endif | ||
164 | iConstForEach(StringSet, i, d->useUrls) { | ||
165 | if (startsWithCase_String(url, cstr_String(i.value))) { | ||
166 | return iTrue; | ||
167 | } | ||
168 | } | ||
162 | return iFalse; | 169 | return iFalse; |
163 | } | 170 | } |
164 | 171 | ||
@@ -193,7 +200,13 @@ void setUse_GmIdentity(iGmIdentity *d, const iString *url, iBool use) { | |||
193 | iAssert(wasInserted); | 200 | iAssert(wasInserted); |
194 | } | 201 | } |
195 | else { | 202 | else { |
196 | remove_StringSet(d->useUrls, url); | 203 | iForEach(Array, i, &d->useUrls->strings.values) { |
204 | iString *used = i.value; | ||
205 | if (startsWithCase_String(url, cstr_String(used))) { | ||
206 | deinit_String(used); | ||
207 | remove_ArrayIterator(&i); | ||
208 | } | ||
209 | } | ||
197 | } | 210 | } |
198 | } | 211 | } |
199 | 212 | ||
@@ -201,6 +214,16 @@ void clearUse_GmIdentity(iGmIdentity *d) { | |||
201 | clear_StringSet(d->useUrls); | 214 | clear_StringSet(d->useUrls); |
202 | } | 215 | } |
203 | 216 | ||
217 | const iString *findUse_GmIdentity(const iGmIdentity *d, const iString *url) { | ||
218 | if (!d) return NULL; | ||
219 | iConstForEach(StringSet, using, d->useUrls) { | ||
220 | if (startsWith_String(url, cstr_String(using.value))) { | ||
221 | return using.value; | ||
222 | } | ||
223 | } | ||
224 | return NULL; | ||
225 | } | ||
226 | |||
204 | const iString *name_GmIdentity(const iGmIdentity *d) { | 227 | const iString *name_GmIdentity(const iGmIdentity *d) { |
205 | iString *name = collect_String(subject_TlsCertificate(d->cert)); | 228 | iString *name = collect_String(subject_TlsCertificate(d->cert)); |
206 | if (startsWith_String(name, "CN = ")) { | 229 | if (startsWith_String(name, "CN = ")) { |
@@ -631,7 +654,7 @@ void importIdentity_GmCerts(iGmCerts *d, iTlsCertificate *cert, const iString *n | |||
631 | } | 654 | } |
632 | 655 | ||
633 | static const char *certPath_GmCerts_(const iGmCerts *d, const iGmIdentity *identity) { | 656 | static const char *certPath_GmCerts_(const iGmCerts *d, const iGmIdentity *identity) { |
634 | if (!(identity->flags & (temporary_GmIdentityFlag | imported_GmIdentityFlag))) { | 657 | if (!(identity->flags & temporary_GmIdentityFlag)) { |
635 | const char *finger = cstrCollect_String(hexEncode_Block(&identity->fingerprint)); | 658 | const char *finger = cstrCollect_String(hexEncode_Block(&identity->fingerprint)); |
636 | return concatPath_CStr(cstr_String(&d->saveDir), format_CStr("idents/%s", finger)); | 659 | return concatPath_CStr(cstr_String(&d->saveDir), format_CStr("idents/%s", finger)); |
637 | } | 660 | } |
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 *); | |||
48 | iBool isUsedOn_GmIdentity (const iGmIdentity *, const iString *url); | 48 | iBool isUsedOn_GmIdentity (const iGmIdentity *, const iString *url); |
49 | iBool isUsedOnDomain_GmIdentity (const iGmIdentity *, const iRangecc domain); | 49 | iBool isUsedOnDomain_GmIdentity (const iGmIdentity *, const iRangecc domain); |
50 | 50 | ||
51 | void setUse_GmIdentity (iGmIdentity *, const iString *url, iBool use); | 51 | void setUse_GmIdentity (iGmIdentity *, const iString *url, iBool use); |
52 | void clearUse_GmIdentity (iGmIdentity *); | 52 | void clearUse_GmIdentity (iGmIdentity *); |
53 | const iString *findUse_GmIdentity (const iGmIdentity *, const iString *url); | ||
53 | 54 | ||
54 | const iString *name_GmIdentity(const iGmIdentity *); | 55 | const iString *name_GmIdentity(const iGmIdentity *); |
55 | 56 | ||
diff --git a/src/gmdocument.c b/src/gmdocument.c index 9d79830b..19230392 100644 --- a/src/gmdocument.c +++ b/src/gmdocument.c | |||
@@ -37,6 +37,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
37 | #include <the_Foundation/intset.h> | 37 | #include <the_Foundation/intset.h> |
38 | #include <the_Foundation/ptrarray.h> | 38 | #include <the_Foundation/ptrarray.h> |
39 | #include <the_Foundation/regexp.h> | 39 | #include <the_Foundation/regexp.h> |
40 | #include <the_Foundation/stringarray.h> | ||
40 | #include <the_Foundation/stringset.h> | 41 | #include <the_Foundation/stringset.h> |
41 | 42 | ||
42 | #include <ctype.h> | 43 | #include <ctype.h> |
@@ -163,6 +164,7 @@ struct Impl_GmDocument { | |||
163 | iBool enableCommandLinks; /* `about:command?` only allowed on selected pages */ | 164 | iBool enableCommandLinks; /* `about:command?` only allowed on selected pages */ |
164 | iBool isLayoutInvalidated; | 165 | iBool isLayoutInvalidated; |
165 | iArray layout; /* contents of source, laid out in document space */ | 166 | iArray layout; /* contents of source, laid out in document space */ |
167 | iStringArray auxText; /* generated text that appears on the page but is not part of the source */ | ||
166 | iPtrArray links; | 168 | iPtrArray links; |
167 | iString title; /* the first top-level title */ | 169 | iString title; /* the first top-level title */ |
168 | iArray headings; | 170 | iArray headings; |
@@ -554,16 +556,22 @@ static const int maxLedeLines_ = 10; | |||
554 | 556 | ||
555 | static void applyAttributes_RunTypesetter_(iRunTypesetter *d, iTextAttrib attrib) { | 557 | static void applyAttributes_RunTypesetter_(iRunTypesetter *d, iTextAttrib attrib) { |
556 | /* WARNING: This is duplicated in run_Font_(). Make sure they behave identically. */ | 558 | /* WARNING: This is duplicated in run_Font_(). Make sure they behave identically. */ |
557 | if (attrib.bold) { | 559 | if (attrib.monospace) { |
558 | d->run.font = fontWithStyle_Text(d->baseFont, bold_FontStyle); | 560 | d->run.font = fontWithFamily_Text(d->baseFont, monospace_FontId); |
559 | d->run.color = tmFirstParagraph_ColorId; | 561 | d->run.color = tmPreformatted_ColorId; |
560 | } | 562 | } |
561 | else if (attrib.italic) { | 563 | else if (attrib.italic) { |
562 | d->run.font = fontWithStyle_Text(d->baseFont, italic_FontStyle); | 564 | d->run.font = fontWithStyle_Text(d->baseFont, italic_FontStyle); |
563 | } | 565 | } |
564 | else if (attrib.monospace) { | 566 | else if (attrib.regular) { |
565 | d->run.font = fontWithFamily_Text(d->baseFont, monospace_FontId); | 567 | d->run.font = fontWithStyle_Text(d->baseFont, regular_FontStyle); |
566 | d->run.color = tmPreformatted_ColorId; | 568 | } |
569 | else if (attrib.bold) { | ||
570 | d->run.font = fontWithStyle_Text(d->baseFont, bold_FontStyle); | ||
571 | d->run.color = tmFirstParagraph_ColorId; | ||
572 | } | ||
573 | else if (attrib.light) { | ||
574 | d->run.font = fontWithStyle_Text(d->baseFont, light_FontStyle); | ||
567 | } | 575 | } |
568 | else { | 576 | else { |
569 | d->run.font = d->baseFont; | 577 | d->run.font = d->baseFont; |
@@ -643,6 +651,7 @@ static void doLayout_GmDocument_(iGmDocument *d) { | |||
643 | static const char *uploadArrow = upload_Icon; | 651 | static const char *uploadArrow = upload_Icon; |
644 | static const char *image = photo_Icon; | 652 | static const char *image = photo_Icon; |
645 | clear_Array(&d->layout); | 653 | clear_Array(&d->layout); |
654 | clear_StringArray(&d->auxText); | ||
646 | clearLinks_GmDocument_(d); | 655 | clearLinks_GmDocument_(d); |
647 | clear_Array(&d->headings); | 656 | clear_Array(&d->headings); |
648 | const iArray *oldPreMeta = collect_Array(copy_Array(&d->preMeta)); /* remember fold states */ | 657 | const iArray *oldPreMeta = collect_Array(copy_Array(&d->preMeta)); /* remember fold states */ |
@@ -927,7 +936,7 @@ static void doLayout_GmDocument_(iGmDocument *d) { | |||
927 | : paragraph_FontId; | 936 | : paragraph_FontId; |
928 | alignDecoration_GmRun_(&icon, iFalse); | 937 | alignDecoration_GmRun_(&icon, iFalse); |
929 | icon.color = linkColor_GmDocument(d, run.linkId, icon_GmLinkPart); | 938 | icon.color = linkColor_GmDocument(d, run.linkId, icon_GmLinkPart); |
930 | icon.flags |= decoration_GmRunFlag; | 939 | icon.flags |= decoration_GmRunFlag | startOfLine_GmRunFlag; |
931 | pushBack_Array(&d->layout, &icon); | 940 | pushBack_Array(&d->layout, &icon); |
932 | } | 941 | } |
933 | run.lineType = type; | 942 | run.lineType = type; |
@@ -1041,7 +1050,12 @@ static void doLayout_GmDocument_(iGmDocument *d) { | |||
1041 | deinit_RunTypesetter_(&rts); | 1050 | deinit_RunTypesetter_(&rts); |
1042 | } | 1051 | } |
1043 | /* Flag the end of line, too. */ | 1052 | /* Flag the end of line, too. */ |
1044 | ((iGmRun *) back_Array(&d->layout))->flags |= endOfLine_GmRunFlag; | 1053 | iGmRun *lastRun = back_Array(&d->layout); |
1054 | lastRun->flags |= endOfLine_GmRunFlag; | ||
1055 | if (lastRun->linkId && lastRun->flags & startOfLine_GmRunFlag) { | ||
1056 | /* Single-run link: the icon should also be marked endOfLine. */ | ||
1057 | lastRun[-1].flags |= endOfLine_GmRunFlag; | ||
1058 | } | ||
1045 | /* Image or audio content. */ | 1059 | /* Image or audio content. */ |
1046 | if (type == link_GmLineType) { | 1060 | if (type == link_GmLineType) { |
1047 | /* TODO: Cleanup here? Move to a function of its own. */ | 1061 | /* TODO: Cleanup here? Move to a function of its own. */ |
@@ -1075,7 +1089,9 @@ static void doLayout_GmDocument_(iGmDocument *d) { | |||
1075 | run.bounds.pos.x -= d->outsideMargin; | 1089 | run.bounds.pos.x -= d->outsideMargin; |
1076 | } | 1090 | } |
1077 | run.visBounds = run.bounds; | 1091 | run.visBounds = run.bounds; |
1078 | const iInt2 maxSize = mulf_I2(imgSize, get_Window()->pixelRatio); | 1092 | const iInt2 maxSize = mulf_I2( |
1093 | imgSize, | ||
1094 | get_Window()->pixelRatio * iMax(1.0f, (prefs_App()->zoomPercent / 100.0f))); | ||
1079 | if (width_Rect(run.visBounds) > maxSize.x) { | 1095 | if (width_Rect(run.visBounds) > maxSize.x) { |
1080 | /* Don't scale the image up. */ | 1096 | /* Don't scale the image up. */ |
1081 | run.visBounds.size.y = | 1097 | run.visBounds.size.y = |
@@ -1085,6 +1101,34 @@ static void doLayout_GmDocument_(iGmDocument *d) { | |||
1085 | run.bounds.size.y = run.visBounds.size.y; | 1101 | run.bounds.size.y = run.visBounds.size.y; |
1086 | } | 1102 | } |
1087 | pushBack_Array(&d->layout, &run); | 1103 | pushBack_Array(&d->layout, &run); |
1104 | pos.y += run.bounds.size.y + margin / 2; | ||
1105 | /* Image metadata caption. */ { | ||
1106 | run.font = FONT_ID(documentBody_FontId, semiBold_FontStyle, contentSmall_FontSize); | ||
1107 | run.color = tmQuoteIcon_ColorId; | ||
1108 | run.flags = decoration_GmRunFlag; | ||
1109 | run.mediaId = 0; | ||
1110 | run.mediaType = 0; | ||
1111 | run.visBounds.pos.y = pos.y; | ||
1112 | run.visBounds.size.y = lineHeight_Text(run.font); | ||
1113 | run.bounds = zero_Rect(); | ||
1114 | iString caption; | ||
1115 | init_String(&caption); | ||
1116 | format_String(&caption, | ||
1117 | "%s \u2014 %d x %d \u2014 %.1f%s", | ||
1118 | info.type, | ||
1119 | imgSize.x, | ||
1120 | imgSize.y, | ||
1121 | info.numBytes / 1.0e6f, | ||
1122 | cstr_Lang("mb")); | ||
1123 | pushBack_StringArray(&d->auxText, &caption); | ||
1124 | run.text = range_String(&caption); | ||
1125 | /* Center it. */ | ||
1126 | run.visBounds.size.x = measureRange_Text(run.font, range_String(&caption)).bounds.size.x; | ||
1127 | run.visBounds.pos.x = d->size.x / 2 - run.visBounds.size.x / 2; | ||
1128 | deinit_String(&caption); | ||
1129 | pushBack_Array(&d->layout, &run); | ||
1130 | pos.y += run.visBounds.size.y + margin; | ||
1131 | } | ||
1088 | break; | 1132 | break; |
1089 | } | 1133 | } |
1090 | case audio_MediaType: { | 1134 | case audio_MediaType: { |
@@ -1157,6 +1201,7 @@ void init_GmDocument(iGmDocument *d) { | |||
1157 | d->enableCommandLinks = iFalse; | 1201 | d->enableCommandLinks = iFalse; |
1158 | d->isLayoutInvalidated = iFalse; | 1202 | d->isLayoutInvalidated = iFalse; |
1159 | init_Array(&d->layout, sizeof(iGmRun)); | 1203 | init_Array(&d->layout, sizeof(iGmRun)); |
1204 | init_StringArray(&d->auxText); | ||
1160 | init_PtrArray(&d->links); | 1205 | init_PtrArray(&d->links); |
1161 | init_String(&d->title); | 1206 | init_String(&d->title); |
1162 | init_Array(&d->headings, sizeof(iGmHeading)); | 1207 | init_Array(&d->headings, sizeof(iGmHeading)); |
@@ -1178,6 +1223,7 @@ void deinit_GmDocument(iGmDocument *d) { | |||
1178 | deinit_PtrArray(&d->links); | 1223 | deinit_PtrArray(&d->links); |
1179 | deinit_Array(&d->preMeta); | 1224 | deinit_Array(&d->preMeta); |
1180 | deinit_Array(&d->headings); | 1225 | deinit_Array(&d->headings); |
1226 | deinit_StringArray(&d->auxText); | ||
1181 | deinit_Array(&d->layout); | 1227 | deinit_Array(&d->layout); |
1182 | deinit_String(&d->localHost); | 1228 | deinit_String(&d->localHost); |
1183 | deinit_String(&d->url); | 1229 | deinit_String(&d->url); |
@@ -1228,8 +1274,8 @@ static void setDerivedThemeColors_(enum iGmDocumentTheme theme) { | |||
1228 | mix_Color(get_Color(tmQuoteIcon_ColorId), get_Color(tmBackground_ColorId), 0.4f)); | 1274 | mix_Color(get_Color(tmQuoteIcon_ColorId), get_Color(tmBackground_ColorId), 0.4f)); |
1229 | set_Color(tmBackgroundOpenLink_ColorId, | 1275 | set_Color(tmBackgroundOpenLink_ColorId, |
1230 | mix_Color(get_Color(tmLinkText_ColorId), get_Color(tmBackground_ColorId), 0.90f)); | 1276 | mix_Color(get_Color(tmLinkText_ColorId), get_Color(tmBackground_ColorId), 0.90f)); |
1231 | set_Color(tmFrameOpenLink_ColorId, | 1277 | set_Color(tmLinkFeedEntryDate_ColorId, |
1232 | mix_Color(get_Color(tmLinkText_ColorId), get_Color(tmBackground_ColorId), 0.75f)); | 1278 | mix_Color(get_Color(tmLinkText_ColorId), get_Color(tmBackground_ColorId), 0.25f)); |
1233 | if (theme == colorfulDark_GmDocumentTheme) { | 1279 | if (theme == colorfulDark_GmDocumentTheme) { |
1234 | /* Ensure paragraph text and link text aren't too similarly colored. */ | 1280 | /* Ensure paragraph text and link text aren't too similarly colored. */ |
1235 | if (delta_Color(get_Color(tmLinkText_ColorId), get_Color(tmParagraph_ColorId)) < 100) { | 1281 | if (delta_Color(get_Color(tmLinkText_ColorId), get_Color(tmParagraph_ColorId)) < 100) { |
@@ -1715,9 +1761,12 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) { | |||
1715 | } | 1761 | } |
1716 | 1762 | ||
1717 | void makePaletteGlobal_GmDocument(const iGmDocument *d) { | 1763 | void makePaletteGlobal_GmDocument(const iGmDocument *d) { |
1718 | if (d->isPaletteValid) { | 1764 | if (!d->isPaletteValid) { |
1719 | memcpy(get_Root()->tmPalette, d->palette, sizeof(d->palette)); | 1765 | /* Recompute the palette since it's needed now. */ |
1766 | setThemeSeed_GmDocument((iGmDocument *) d, urlThemeSeed_String(&d->url)); | ||
1720 | } | 1767 | } |
1768 | iAssert(d->isPaletteValid); | ||
1769 | memcpy(get_Root()->tmPalette, d->palette, sizeof(d->palette)); | ||
1721 | } | 1770 | } |
1722 | 1771 | ||
1723 | void invalidatePalette_GmDocument(iGmDocument *d) { | 1772 | void invalidatePalette_GmDocument(iGmDocument *d) { |
@@ -1754,6 +1803,7 @@ static void markLinkRunsVisited_GmDocument_(iGmDocument *d, const iIntSet *linkI | |||
1754 | iForEach(Array, r, &d->layout) { | 1803 | iForEach(Array, r, &d->layout) { |
1755 | iGmRun *run = r.value; | 1804 | iGmRun *run = r.value; |
1756 | if (run->linkId && !run->mediaId && contains_IntSet(linkIds, run->linkId)) { | 1805 | if (run->linkId && !run->mediaId && contains_IntSet(linkIds, run->linkId)) { |
1806 | /* TODO: Does this even work? The font IDs may be different. */ | ||
1757 | if (run->font == bold_FontId) { | 1807 | if (run->font == bold_FontId) { |
1758 | run->font = paragraph_FontId; | 1808 | run->font = paragraph_FontId; |
1759 | } | 1809 | } |
@@ -1890,6 +1940,7 @@ static void normalize_GmDocument(iGmDocument *d) { | |||
1890 | void setUrl_GmDocument(iGmDocument *d, const iString *url) { | 1940 | void setUrl_GmDocument(iGmDocument *d, const iString *url) { |
1891 | url = canonicalUrl_String(url); | 1941 | url = canonicalUrl_String(url); |
1892 | set_String(&d->url, url); | 1942 | set_String(&d->url, url); |
1943 | setThemeSeed_GmDocument(d, urlThemeSeed_String(url)); | ||
1893 | iUrl parts; | 1944 | iUrl parts; |
1894 | init_Url(&parts, url); | 1945 | init_Url(&parts, url); |
1895 | setRange_String(&d->localHost, parts.host); | 1946 | setRange_String(&d->localHost, parts.host); |
@@ -1900,9 +1951,9 @@ void setUrl_GmDocument(iGmDocument *d, const iString *url) { | |||
1900 | } | 1951 | } |
1901 | } | 1952 | } |
1902 | 1953 | ||
1903 | static int replaceRegExp_String(iString *d, const iRegExp *regexp, const char *replacement, | 1954 | int replaceRegExp_String(iString *d, const iRegExp *regexp, const char *replacement, |
1904 | void (*matchHandler)(void *, const iRegExpMatch *), | 1955 | void (*matchHandler)(void *, const iRegExpMatch *), |
1905 | void *context) { | 1956 | void *context) { |
1906 | iRegExpMatch m; | 1957 | iRegExpMatch m; |
1907 | iString result; | 1958 | iString result; |
1908 | int numMatches = 0; | 1959 | int numMatches = 0; |
@@ -2103,6 +2154,7 @@ void setSource_GmDocument(iGmDocument *d, const iString *source, int width, int | |||
2103 | if (size_String(source) == size_String(&d->unormSource)) { | 2154 | if (size_String(source) == size_String(&d->unormSource)) { |
2104 | iAssert(equal_String(source, &d->unormSource)); | 2155 | iAssert(equal_String(source, &d->unormSource)); |
2105 | // printf("[GmDocument] source is unchanged!\n"); | 2156 | // printf("[GmDocument] source is unchanged!\n"); |
2157 | updateWidth_GmDocument(d, width, canvasWidth); | ||
2106 | return; /* Nothing to do. */ | 2158 | return; /* Nothing to do. */ |
2107 | } | 2159 | } |
2108 | /* Normalize and convert to Gemtext if needed. */ | 2160 | /* Normalize and convert to Gemtext if needed. */ |
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 { | |||
139 | 139 | ||
140 | uint32_t font : 14; | 140 | uint32_t font : 14; |
141 | uint32_t mediaType : 3; /* note: max_MediaType means preformatted block */ | 141 | uint32_t mediaType : 3; /* note: max_MediaType means preformatted block */ |
142 | uint32_t mediaId : 11; /* zero if not an image */ | 142 | uint32_t mediaId : 11; |
143 | uint32_t lineType : 3; | 143 | uint32_t lineType : 3; |
144 | uint32_t isLede : 1; | 144 | uint32_t isLede : 1; |
145 | }; | 145 | }; |
diff --git a/src/gmrequest.c b/src/gmrequest.c index 23845475..3d5a4aef 100644 --- a/src/gmrequest.c +++ b/src/gmrequest.c | |||
@@ -585,8 +585,10 @@ void setUrl_GmRequest(iGmRequest *d, const iString *url) { | |||
585 | /* TODO: Gemini spec allows UTF-8 encoded URLs, but still need to percent-encode non-ASCII | 585 | /* TODO: Gemini spec allows UTF-8 encoded URLs, but still need to percent-encode non-ASCII |
586 | characters? Could be a server-side issue, e.g., if they're using a URL parser meant for | 586 | characters? Could be a server-side issue, e.g., if they're using a URL parser meant for |
587 | the web. */ | 587 | the web. */ |
588 | urlEncodePath_String(&d->url); | 588 | /* Encode everything except already-percent encoded characters. */ |
589 | urlEncodeSpaces_String(&d->url); | 589 | iString *enc = urlEncodeExclude_String(&d->url, "%" URL_RESERVED_CHARS); |
590 | set_String(&d->url, enc); | ||
591 | delete_String(enc); | ||
590 | d->identity = identityForUrl_GmCerts(d->certs, &d->url); | 592 | d->identity = identityForUrl_GmCerts(d->certs, &d->url); |
591 | } | 593 | } |
592 | 594 | ||
@@ -692,9 +694,11 @@ void submit_GmRequest(iGmRequest *d) { | |||
692 | setCStr_String(&resp->meta, "text/gemini"); | 694 | setCStr_String(&resp->meta, "text/gemini"); |
693 | iString *page = collectNew_String(); | 695 | iString *page = collectNew_String(); |
694 | iString *parentDir = collectNewRange_String(dirName_Path(path)); | 696 | iString *parentDir = collectNewRange_String(dirName_Path(path)); |
697 | #if !defined (iPlatformMobile) | ||
695 | appendFormat_String(page, "=> %s " upArrow_Icon " %s" iPathSeparator "\n\n", | 698 | appendFormat_String(page, "=> %s " upArrow_Icon " %s" iPathSeparator "\n\n", |
696 | cstrCollect_String(makeFileUrl_String(parentDir)), | 699 | cstrCollect_String(makeFileUrl_String(parentDir)), |
697 | cstr_String(parentDir)); | 700 | cstr_String(parentDir)); |
701 | #endif | ||
698 | appendFormat_String(page, "# %s\n", cstr_Rangecc(baseName_Path(path))); | 702 | appendFormat_String(page, "# %s\n", cstr_Rangecc(baseName_Path(path))); |
699 | /* Make a directory index page. */ | 703 | /* Make a directory index page. */ |
700 | iPtrArray *sortedInfo = collectNew_PtrArray(); | 704 | iPtrArray *sortedInfo = collectNew_PtrArray(); |
@@ -790,7 +794,8 @@ void submit_GmRequest(iGmRequest *d) { | |||
790 | cstr_String(containerUrl)); | 794 | cstr_String(containerUrl)); |
791 | appendFormat_String(page, "# %s\n\n", cstr_Rangecc(containerName)); | 795 | appendFormat_String(page, "# %s\n\n", cstr_Rangecc(containerName)); |
792 | appendFormat_String(page, | 796 | appendFormat_String(page, |
793 | cstrCount_Lang("archive.summary.n", numEntries_Archive(arch)), | 797 | cstrCount_Lang("archive.summary.n", |
798 | (int) numEntries_Archive(arch)), | ||
794 | numEntries_Archive(arch), | 799 | numEntries_Archive(arch), |
795 | (double) sourceSize_Archive(arch) / 1.0e6); | 800 | (double) sourceSize_Archive(arch) / 1.0e6); |
796 | appendCStr_String(page, "\n\n"); | 801 | appendCStr_String(page, "\n\n"); |
@@ -802,7 +807,7 @@ void submit_GmRequest(iGmRequest *d) { | |||
802 | } | 807 | } |
803 | else if (size_StringSet(contents) > 1) { | 808 | else if (size_StringSet(contents) > 1) { |
804 | appendFormat_String(page, cstrCount_Lang("dir.summary.n", | 809 | appendFormat_String(page, cstrCount_Lang("dir.summary.n", |
805 | size_StringSet(contents)), | 810 | (int) size_StringSet(contents)), |
806 | size_StringSet(contents)); | 811 | size_StringSet(contents)); |
807 | appendCStr_String(page, "\n\n"); | 812 | appendCStr_String(page, "\n\n"); |
808 | } | 813 | } |
diff --git a/src/gmutil.c b/src/gmutil.c index 70a3608e..98e4d4d6 100644 --- a/src/gmutil.c +++ b/src/gmutil.c | |||
@@ -253,6 +253,17 @@ iRangecc urlRoot_String(const iString *d) { | |||
253 | return (iRangecc){ constBegin_String(d), rootEnd }; | 253 | return (iRangecc){ constBegin_String(d), rootEnd }; |
254 | } | 254 | } |
255 | 255 | ||
256 | const iBlock *urlThemeSeed_String(const iString *url) { | ||
257 | if (equalCase_Rangecc(urlScheme_String(url), "file")) { | ||
258 | return collect_Block(new_Block(0)); | ||
259 | } | ||
260 | const iRangecc user = urlUser_String(url); | ||
261 | if (isEmpty_Range(&user)) { | ||
262 | return collect_Block(newRange_Block(urlHost_String(url))); | ||
263 | } | ||
264 | return collect_Block(newRange_Block(user)); | ||
265 | } | ||
266 | |||
256 | static iBool isAbsolutePath_(iRangecc path) { | 267 | static iBool isAbsolutePath_(iRangecc path) { |
257 | return isAbsolute_Path(collect_String(urlDecode_String(collect_String(newRange_String(path))))); | 268 | return isAbsolute_Path(collect_String(urlDecode_String(collect_String(newRange_String(path))))); |
258 | } | 269 | } |
@@ -319,6 +330,28 @@ void urlEncodePath_String(iString *d) { | |||
319 | delete_String(encoded); | 330 | delete_String(encoded); |
320 | } | 331 | } |
321 | 332 | ||
333 | void urlEncodeQuery_String(iString *d) { | ||
334 | iUrl url; | ||
335 | init_Url(&url, d); | ||
336 | if (isEmpty_Range(&url.query)) { | ||
337 | return; | ||
338 | } | ||
339 | iString encoded; | ||
340 | init_String(&encoded); | ||
341 | appendRange_String(&encoded, (iRangecc){ constBegin_String(d), url.query.start }); | ||
342 | iString query; | ||
343 | url.query.start++; /* omit the question mark */ | ||
344 | initRange_String(&query, url.query); | ||
345 | iString *encQuery = urlEncode_String(&query); /* fully encoded */ | ||
346 | appendCStr_String(&encoded, "?"); | ||
347 | append_String(&encoded, encQuery); | ||
348 | delete_String(encQuery); | ||
349 | deinit_String(&query); | ||
350 | appendRange_String(&encoded, (iRangecc){ url.query.end, constEnd_String(d) }); | ||
351 | set_String(d, &encoded); | ||
352 | deinit_String(&encoded); | ||
353 | } | ||
354 | |||
322 | iBool isKnownScheme_Rangecc(iRangecc scheme) { | 355 | iBool isKnownScheme_Rangecc(iRangecc scheme) { |
323 | if (isKnownUrlScheme_Rangecc(scheme)) { | 356 | if (isKnownUrlScheme_Rangecc(scheme)) { |
324 | return iTrue; | 357 | return iTrue; |
@@ -651,25 +684,25 @@ const iString *withSpacesEncoded_String(const iString *d) { | |||
651 | const iString *canonicalUrl_String(const iString *d) { | 684 | const iString *canonicalUrl_String(const iString *d) { |
652 | /* The "canonical" form, used for internal storage and comparisons, is: | 685 | /* The "canonical" form, used for internal storage and comparisons, is: |
653 | - all non-reserved characters decoded (i.e., it's an IRI) | 686 | - all non-reserved characters decoded (i.e., it's an IRI) |
654 | - expect for spaces, which are always `%20` | 687 | - except spaces, which are always `%20` |
655 | This means a canonical URL can be used on a gemtext link line without modifications. */ | 688 | This means a canonical URL can be used on a gemtext link line without modifications. */ |
656 | iString *canon = NULL; | 689 | iString *canon = NULL; |
657 | iUrl parts; | 690 | iUrl parts; |
658 | init_Url(&parts, d); | 691 | init_Url(&parts, d); |
659 | /* Colons are in decoded form in the URL path. */ | 692 | /* Colons (0x3a) are in decoded form in the URL path. */ |
660 | if (iStrStrN(parts.path.start, "%3A", size_Range(&parts.path)) || | 693 | if (iStrStrN(parts.path.start, "%3A", size_Range(&parts.path)) || |
661 | iStrStrN(parts.path.start, "%3a", size_Range(&parts.path))) { | 694 | iStrStrN(parts.path.start, "%3a", size_Range(&parts.path))) { |
662 | /* This is done separately to avoid the copy if %3A is not present; it's rare. */ | 695 | /* This is done separately to avoid the copy if %3A is not present; it's rare. */ |
663 | canon = copy_String(d); | 696 | canon = copy_String(d); |
664 | urlDecodePath_String(canon); | 697 | urlDecodePath_String(canon); |
665 | iString *dec = maybeUrlDecodeExclude_String(canon, "%/?:;#&+= "); /* decode everything else in all parts */ | 698 | iString *dec = maybeUrlDecodeExclude_String(canon, "% " URL_RESERVED_CHARS); /* decode everything else in all parts */ |
666 | if (dec) { | 699 | if (dec) { |
667 | set_String(canon, dec); | 700 | set_String(canon, dec); |
668 | delete_String(dec); | 701 | delete_String(dec); |
669 | } | 702 | } |
670 | } | 703 | } |
671 | else { | 704 | else { |
672 | canon = maybeUrlDecodeExclude_String(d, "%/?:;#&+= "); | 705 | canon = maybeUrlDecodeExclude_String(d, "% " URL_RESERVED_CHARS); |
673 | } | 706 | } |
674 | /* `canon` may now be NULL if nothing was decoded. */ | 707 | /* `canon` may now be NULL if nothing was decoded. */ |
675 | if (indexOfCStr_String(canon ? canon : d, " ") != iInvalidPos || | 708 | if (indexOfCStr_String(canon ? canon : d, " ") != iInvalidPos || |
@@ -678,7 +711,7 @@ const iString *canonicalUrl_String(const iString *d) { | |||
678 | canon = copy_String(d); | 711 | canon = copy_String(d); |
679 | } | 712 | } |
680 | urlEncodeSpaces_String(canon); | 713 | urlEncodeSpaces_String(canon); |
681 | } | 714 | } |
682 | return canon ? collect_String(canon) : d; | 715 | return canon ? collect_String(canon) : d; |
683 | } | 716 | } |
684 | 717 | ||
diff --git a/src/gmutil.h b/src/gmutil.h index 6b10b444..15bb7b2e 100644 --- a/src/gmutil.h +++ b/src/gmutil.h | |||
@@ -27,6 +27,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
27 | 27 | ||
28 | iDeclareType(GmError) | 28 | iDeclareType(GmError) |
29 | iDeclareType(RegExp) | 29 | iDeclareType(RegExp) |
30 | iDeclareType(RegExpMatch) | ||
30 | iDeclareType(Url) | 31 | iDeclareType(Url) |
31 | 32 | ||
32 | /* Response status codes. */ | 33 | /* Response status codes. */ |
@@ -99,6 +100,7 @@ iRegExp * newGemtextLink_RegExp (void); | |||
99 | 100 | ||
100 | #define GEMINI_DEFAULT_PORT ((uint16_t) 1965) | 101 | #define GEMINI_DEFAULT_PORT ((uint16_t) 1965) |
101 | #define GEMINI_DEFAULT_PORT_CSTR "1965" | 102 | #define GEMINI_DEFAULT_PORT_CSTR "1965" |
103 | #define URL_RESERVED_CHARS ":/?#[]@!$&'()*+,;=" /* RFC 3986 */ | ||
102 | 104 | ||
103 | struct Impl_Url { | 105 | struct Impl_Url { |
104 | iRangecc scheme; | 106 | iRangecc scheme; |
@@ -117,6 +119,8 @@ iRangecc urlHost_String (const iString *); | |||
117 | uint16_t urlPort_String (const iString *); | 119 | uint16_t urlPort_String (const iString *); |
118 | iRangecc urlUser_String (const iString *); | 120 | iRangecc urlUser_String (const iString *); |
119 | iRangecc urlRoot_String (const iString *); | 121 | iRangecc urlRoot_String (const iString *); |
122 | const iBlock * urlThemeSeed_String (const iString *); | ||
123 | |||
120 | const iString * absoluteUrl_String (const iString *, const iString *urlMaybeRelative); | 124 | const iString * absoluteUrl_String (const iString *, const iString *urlMaybeRelative); |
121 | iBool isLikelyUrl_String (const iString *); | 125 | iBool isLikelyUrl_String (const iString *); |
122 | iBool isKnownScheme_Rangecc (iRangecc scheme); /* any URI scheme */ | 126 | iBool isKnownScheme_Rangecc (iRangecc scheme); /* any URI scheme */ |
@@ -128,6 +132,7 @@ const iString * urlFragmentStripped_String(const iString *); | |||
128 | const iString * urlQueryStripped_String (const iString *); | 132 | const iString * urlQueryStripped_String (const iString *); |
129 | void urlDecodePath_String (iString *); | 133 | void urlDecodePath_String (iString *); |
130 | void urlEncodePath_String (iString *); | 134 | void urlEncodePath_String (iString *); |
135 | void urlEncodeQuery_String (iString *); | ||
131 | iString * makeFileUrl_String (const iString *localFilePath); | 136 | iString * makeFileUrl_String (const iString *localFilePath); |
132 | const char * makeFileUrl_CStr (const char *localFilePath); | 137 | const char * makeFileUrl_CStr (const char *localFilePath); |
133 | iString * localFilePathFromUrl_String(const iString *); | 138 | iString * localFilePathFromUrl_String(const iString *); |
@@ -143,3 +148,8 @@ const iString * findContainerArchive_Path (const iString *path); | |||
143 | 148 | ||
144 | 149 | ||
145 | const iString * feedEntryOpenCommand_String (const iString *url, int newTab); /* checks fragment */ | 150 | const iString * feedEntryOpenCommand_String (const iString *url, int newTab); /* checks fragment */ |
151 | |||
152 | /* TODO: Consider adding this to the_Foundation. */ | ||
153 | int replaceRegExp_String (iString *, const iRegExp *regexp, const char *replacement, | ||
154 | void (*matchHandler)(void *, const iRegExpMatch *), | ||
155 | void *context); | ||
diff --git a/src/history.c b/src/history.c index 7185912f..56454009 100644 --- a/src/history.c +++ b/src/history.c | |||
@@ -37,7 +37,7 @@ void init_RecentUrl(iRecentUrl *d) { | |||
37 | d->normScrollY = 0; | 37 | d->normScrollY = 0; |
38 | d->cachedResponse = NULL; | 38 | d->cachedResponse = NULL; |
39 | d->cachedDoc = NULL; | 39 | d->cachedDoc = NULL; |
40 | d->flags.openedFromSidebar = iFalse; | 40 | d->flags = 0; |
41 | } | 41 | } |
42 | 42 | ||
43 | void deinit_RecentUrl(iRecentUrl *d) { | 43 | void deinit_RecentUrl(iRecentUrl *d) { |
@@ -110,6 +110,14 @@ iHistory *copy_History(const iHistory *d) { | |||
110 | return copy; | 110 | return copy; |
111 | } | 111 | } |
112 | 112 | ||
113 | void lock_History(iHistory *d) { | ||
114 | lock_Mutex(d->mtx); | ||
115 | } | ||
116 | |||
117 | void unlock_History(iHistory *d) { | ||
118 | unlock_Mutex(d->mtx); | ||
119 | } | ||
120 | |||
113 | iMemInfo memoryUsage_History(const iHistory *d) { | 121 | iMemInfo memoryUsage_History(const iHistory *d) { |
114 | iMemInfo mem = { 0, 0 }; | 122 | iMemInfo mem = { 0, 0 }; |
115 | iConstForEach(Array, i, &d->recent) { | 123 | iConstForEach(Array, i, &d->recent) { |
@@ -173,7 +181,7 @@ void serialize_History(const iHistory *d, iStream *outs) { | |||
173 | const iRecentUrl *item = i.value; | 181 | const iRecentUrl *item = i.value; |
174 | serialize_String(&item->url, outs); | 182 | serialize_String(&item->url, outs); |
175 | write32_Stream(outs, item->normScrollY * 1.0e6f); | 183 | write32_Stream(outs, item->normScrollY * 1.0e6f); |
176 | writeU16_Stream(outs, item->flags.openedFromSidebar ? iBit(1) : 0); | 184 | writeU16_Stream(outs, item->flags); |
177 | if (item->cachedResponse) { | 185 | if (item->cachedResponse) { |
178 | write8_Stream(outs, 1); | 186 | write8_Stream(outs, 1); |
179 | serialize_GmResponse(item->cachedResponse, outs); | 187 | serialize_GmResponse(item->cachedResponse, outs); |
@@ -197,10 +205,7 @@ void deserialize_History(iHistory *d, iStream *ins) { | |||
197 | set_String(&item.url, canonicalUrl_String(&item.url)); | 205 | set_String(&item.url, canonicalUrl_String(&item.url)); |
198 | item.normScrollY = (float) read32_Stream(ins) / 1.0e6f; | 206 | item.normScrollY = (float) read32_Stream(ins) / 1.0e6f; |
199 | if (version_Stream(ins) >= addedRecentUrlFlags_FileVersion) { | 207 | if (version_Stream(ins) >= addedRecentUrlFlags_FileVersion) { |
200 | uint16_t flags = readU16_Stream(ins); | 208 | item.flags = readU16_Stream(ins); |
201 | if (flags & iBit(1)) { | ||
202 | item.flags.openedFromSidebar = iTrue; | ||
203 | } | ||
204 | } | 209 | } |
205 | if (read8_Stream(ins)) { | 210 | if (read8_Stream(ins)) { |
206 | item.cachedResponse = new_GmResponse(); | 211 | item.cachedResponse = new_GmResponse(); |
@@ -246,18 +251,26 @@ const iString *url_History(const iHistory *d, size_t pos) { | |||
246 | return collectNew_String(); | 251 | return collectNew_String(); |
247 | } | 252 | } |
248 | 253 | ||
249 | iRecentUrl *findUrl_History(iHistory *d, const iString *url) { | 254 | #if 0 |
255 | iRecentUrl *findUrl_History(iHistory *d, const iString *url, int timeDir) { | ||
250 | url = canonicalUrl_String(url); | 256 | url = canonicalUrl_String(url); |
257 | // if (!timeDir) { | ||
258 | // timeDir = -1; | ||
259 | // } | ||
251 | lock_Mutex(d->mtx); | 260 | lock_Mutex(d->mtx); |
252 | iReverseForEach(Array, i, &d->recent) { | 261 | for (size_t i = size_Array(&d->recent) - 1 - d->recentPos; i < size_Array(&d->recent); |
253 | if (cmpStringCase_String(url, &((iRecentUrl *) i.value)->url) == 0) { | 262 | i += timeDir) { |
263 | iRecentUrl *item = at_Array(&d->recent, i); | ||
264 | if (cmpStringCase_String(url, &item->url) == 0) { | ||
254 | unlock_Mutex(d->mtx); | 265 | unlock_Mutex(d->mtx); |
255 | return i.value; | 266 | return item; /* FIXME: Returning an internal pointer; should remain locked. */ |
256 | } | 267 | } |
268 | if (!timeDir) break; | ||
257 | } | 269 | } |
258 | unlock_Mutex(d->mtx); | 270 | unlock_Mutex(d->mtx); |
259 | return NULL; | 271 | return NULL; |
260 | } | 272 | } |
273 | #endif | ||
261 | 274 | ||
262 | void replace_History(iHistory *d, const iString *url) { | 275 | void replace_History(iHistory *d, const iString *url) { |
263 | url = canonicalUrl_String(url); | 276 | url = canonicalUrl_String(url); |
@@ -297,20 +310,31 @@ void add_History(iHistory *d, const iString *url) { | |||
297 | unlock_Mutex(d->mtx); | 310 | unlock_Mutex(d->mtx); |
298 | } | 311 | } |
299 | 312 | ||
300 | iBool preceding_History(iHistory *d, iRecentUrl *recent_out) { | 313 | void undo_History(iHistory *d) { |
301 | iBool ok = iFalse; | ||
302 | lock_Mutex(d->mtx); | 314 | lock_Mutex(d->mtx); |
303 | if (!isEmpty_Array(&d->recent) && d->recentPos < size_Array(&d->recent) - 1) { | 315 | if (!isEmpty_Array(&d->recent) || d->recentPos != 0) { |
304 | const iRecentUrl *recent = constAt_Array(&d->recent, size_Array(&d->recent) - 1 - | 316 | deinit_RecentUrl(back_Array(&d->recent)); |
305 | (d->recentPos + 1)); | 317 | popBack_Array(&d->recent); |
306 | set_String(&recent_out->url, &recent->url); | 318 | } |
307 | recent_out->normScrollY = recent->normScrollY; | 319 | unlock_Mutex(d->mtx); |
308 | iChangeRef(recent_out->cachedDoc, recent->cachedDoc); | 320 | } |
321 | |||
322 | iRecentUrl *precedingLocked_History(iHistory *d) { | ||
323 | /* NOTE: Manual lock and unlock are required when using this; returning an internal pointer. */ | ||
324 | iBool ok = iFalse; | ||
325 | //lock_Mutex(d->mtx); | ||
326 | const size_t lastIndex = size_Array(&d->recent) - 1; | ||
327 | if (!isEmpty_Array(&d->recent) && d->recentPos < lastIndex) { | ||
328 | return at_Array(&d->recent, lastIndex - (d->recentPos + 1)); | ||
329 | // set_String(&recent_out->url, &recent->url); | ||
330 | // recent_out->normScrollY = recent->normScrollY; | ||
331 | // iChangeRef(recent_out->cachedDoc, recent->cachedDoc); | ||
309 | /* Cached response is not returned, would involve a deep copy. */ | 332 | /* Cached response is not returned, would involve a deep copy. */ |
310 | ok = iTrue; | 333 | // ok = iTrue; |
311 | } | 334 | } |
312 | unlock_Mutex(d->mtx); | 335 | //unlock_Mutex(d->mtx); |
313 | return ok; | 336 | // return ok; |
337 | return NULL; | ||
314 | } | 338 | } |
315 | 339 | ||
316 | #if 0 | 340 | #if 0 |
@@ -360,7 +384,7 @@ iBool goForward_History(iHistory *d) { | |||
360 | return iFalse; | 384 | return iFalse; |
361 | } | 385 | } |
362 | 386 | ||
363 | iBool atLatest_History(const iHistory *d) { | 387 | iBool atNewest_History(const iHistory *d) { |
364 | iBool isLatest; | 388 | iBool isLatest; |
365 | iGuardMutex(d->mtx, isLatest = (d->recentPos == 0)); | 389 | iGuardMutex(d->mtx, isLatest = (d->recentPos == 0)); |
366 | return isLatest; | 390 | return isLatest; |
@@ -391,12 +415,18 @@ void setCachedResponse_History(iHistory *d, const iGmResponse *response) { | |||
391 | unlock_Mutex(d->mtx); | 415 | unlock_Mutex(d->mtx); |
392 | } | 416 | } |
393 | 417 | ||
394 | void setCachedDocument_History(iHistory *d, iGmDocument *doc, iBool openedFromSidebar) { | 418 | void setCachedDocument_History(iHistory *d, iGmDocument *doc) { |
395 | lock_Mutex(d->mtx); | 419 | lock_Mutex(d->mtx); |
396 | iRecentUrl *item = mostRecentUrl_History(d); | 420 | iRecentUrl *item = mostRecentUrl_History(d); |
421 | iAssert(size_GmDocument(doc).x > 0); | ||
397 | if (item) { | 422 | if (item) { |
398 | iAssert(equal_String(url_GmDocument(doc), &item->url)); | 423 | #if !defined (NDEBUG) |
399 | item->flags.openedFromSidebar = openedFromSidebar; | 424 | if (!equal_String(url_GmDocument(doc), &item->url)) { |
425 | printf("[History] Cache mismatch! Expecting data for item {%s} but document URL is {%s}\n", | ||
426 | cstr_String(&item->url), | ||
427 | cstr_String(url_GmDocument(doc))); | ||
428 | } | ||
429 | #endif | ||
400 | if (item->cachedDoc != doc) { | 430 | if (item->cachedDoc != doc) { |
401 | iRelease(item->cachedDoc); | 431 | iRelease(item->cachedDoc); |
402 | item->cachedDoc = ref_Object(doc); | 432 | item->cachedDoc = ref_Object(doc); |
diff --git a/src/history.h b/src/history.h index d3daae80..7959187d 100644 --- a/src/history.h +++ b/src/history.h | |||
@@ -39,9 +39,7 @@ struct Impl_RecentUrl { | |||
39 | float normScrollY; /* normalized to document height */ | 39 | float normScrollY; /* normalized to document height */ |
40 | iGmResponse *cachedResponse; /* kept in memory for quicker back navigation */ | 40 | iGmResponse *cachedResponse; /* kept in memory for quicker back navigation */ |
41 | iGmDocument *cachedDoc; /* cached copy of the presentation: layout and media (not serialized) */ | 41 | iGmDocument *cachedDoc; /* cached copy of the presentation: layout and media (not serialized) */ |
42 | struct { | 42 | uint16_t flags; |
43 | uint8_t openedFromSidebar : 1; | ||
44 | } flags; | ||
45 | }; | 43 | }; |
46 | 44 | ||
47 | iDeclareType(MemInfo) | 45 | iDeclareType(MemInfo) |
@@ -58,19 +56,21 @@ iDeclareTypeConstruction(History) | |||
58 | iDeclareTypeSerialization(History) | 56 | iDeclareTypeSerialization(History) |
59 | 57 | ||
60 | iHistory * copy_History (const iHistory *); | 58 | iHistory * copy_History (const iHistory *); |
59 | void lock_History (iHistory *); | ||
60 | void unlock_History (iHistory *); | ||
61 | 61 | ||
62 | void clear_History (iHistory *); | 62 | void clear_History (iHistory *); |
63 | void add_History (iHistory *, const iString *url); | 63 | void add_History (iHistory *, const iString *url); |
64 | void undo_History (iHistory *); /* removes the most recent URL */ | ||
64 | void replace_History (iHistory *, const iString *url); | 65 | void replace_History (iHistory *, const iString *url); |
65 | void setCachedResponse_History (iHistory *, const iGmResponse *response); | 66 | void setCachedResponse_History (iHistory *, const iGmResponse *response); |
66 | void setCachedDocument_History (iHistory *, iGmDocument *doc, iBool openedFromSidebar); | 67 | void setCachedDocument_History (iHistory *, iGmDocument *doc); |
67 | iBool goBack_History (iHistory *); | 68 | iBool goBack_History (iHistory *); |
68 | iBool goForward_History (iHistory *); | 69 | iBool goForward_History (iHistory *); |
69 | iBool preceding_History (iHistory *d, iRecentUrl *recent_out); | 70 | iRecentUrl *precedingLocked_History (iHistory *); /* requires manual lock/unlock! */ |
70 | //iBool following_History (iHistory *d, iRecentUrl *recent_out); | ||
71 | iRecentUrl *recentUrl_History (iHistory *, size_t pos); | 71 | iRecentUrl *recentUrl_History (iHistory *, size_t pos); |
72 | iRecentUrl *mostRecentUrl_History (iHistory *); | 72 | iRecentUrl *mostRecentUrl_History (iHistory *); |
73 | iRecentUrl *findUrl_History (iHistory *, const iString *url); | 73 | //iRecentUrl *findUrl_History (iHistory *, const iString *url, int timeDir); |
74 | 74 | ||
75 | void clearCache_History (iHistory *); | 75 | void clearCache_History (iHistory *); |
76 | size_t pruneLeastImportant_History (iHistory *); | 76 | size_t pruneLeastImportant_History (iHistory *); |
@@ -78,7 +78,7 @@ size_t pruneLeastImportantMemory_History (iHistory *); | |||
78 | void invalidateTheme_History (iHistory *); /* theme has changed, cached contents need updating */ | 78 | void invalidateTheme_History (iHistory *); /* theme has changed, cached contents need updating */ |
79 | void invalidateCachedLayout_History (iHistory *); | 79 | void invalidateCachedLayout_History (iHistory *); |
80 | 80 | ||
81 | iBool atLatest_History (const iHistory *); | 81 | iBool atNewest_History (const iHistory *); |
82 | iBool atOldest_History (const iHistory *); | 82 | iBool atOldest_History (const iHistory *); |
83 | 83 | ||
84 | const iStringArray * searchContents_History (const iHistory *, const iRegExp *pattern); /* chronologically ascending */ | 84 | const iStringArray * searchContents_History (const iHistory *, const iRegExp *pattern); /* chronologically ascending */ |
@@ -38,6 +38,8 @@ void playHapticEffect_iOS (enum iHapticEffect effect); | |||
38 | void exportDownloadedFile_iOS(const iString *path); | 38 | void exportDownloadedFile_iOS(const iString *path); |
39 | void pickFileForOpening_iOS (void); | 39 | void pickFileForOpening_iOS (void); |
40 | void pickFile_iOS (const char *command); /* ` path:%s` will be appended */ | 40 | void pickFile_iOS (const char *command); /* ` path:%s` will be appended */ |
41 | void openTextActivityView_iOS(const iString *text); | ||
42 | void openFileActivityView_iOS(const iString *path); | ||
41 | 43 | ||
42 | iBool isPhone_iOS (void); | 44 | iBool isPhone_iOS (void); |
43 | void safeAreaInsets_iOS (float *left, float *top, float *right, float *bottom); | 45 | void safeAreaInsets_iOS (float *left, float *top, float *right, float *bottom); |
@@ -61,3 +63,30 @@ iBool isPaused_AVFAudioPlayer (const iAVFAudioPlayer *); | |||
61 | 63 | ||
62 | void clearNowPlayingInfo_iOS (void); | 64 | void clearNowPlayingInfo_iOS (void); |
63 | void updateNowPlayingInfo_iOS (void); | 65 | void updateNowPlayingInfo_iOS (void); |
66 | |||
67 | /*----------------------------------------------------------------------------------------------*/ | ||
68 | |||
69 | enum iSystemTextInputFlags { | ||
70 | selectAll_SystemTextInputFlags = iBit(1), | ||
71 | multiLine_SystemTextInputFlags = iBit(2), | ||
72 | returnGo_SystemTextInputFlags = iBit(3), | ||
73 | returnSend_SystemTextInputFlags = iBit(4), | ||
74 | disableAutocorrect_SystemTextInputFlag = iBit(5), | ||
75 | disableAutocapitalize_SystemTextInputFlag = iBit(6), | ||
76 | alignRight_SystemTextInputFlag = iBit(7), | ||
77 | insertNewlines_SystemTextInputFlag = iBit(8), | ||
78 | extraPadding_SystemTextInputFlag = iBit(9), | ||
79 | }; | ||
80 | |||
81 | iDeclareType(SystemTextInput) | ||
82 | iDeclareTypeConstructionArgs(SystemTextInput, iRect rect, int flags) | ||
83 | |||
84 | void setRect_SystemTextInput (iSystemTextInput *, iRect rect); | ||
85 | void setText_SystemTextInput (iSystemTextInput *, const iString *text, iBool allowUndo); | ||
86 | void setFont_SystemTextInput (iSystemTextInput *, int fontId); | ||
87 | void setTextChangedFunc_SystemTextInput | ||
88 | (iSystemTextInput *, void (*textChangedFunc)(iSystemTextInput *, void *), void *); | ||
89 | void selectAll_SystemTextInput(iSystemTextInput *); | ||
90 | |||
91 | const iString * text_SystemTextInput (const iSystemTextInput *); | ||
92 | int preferredHeight_SystemTextInput (const iSystemTextInput *); | ||
@@ -25,6 +25,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
25 | #include "audio/player.h" | 25 | #include "audio/player.h" |
26 | #include "ui/command.h" | 26 | #include "ui/command.h" |
27 | #include "ui/window.h" | 27 | #include "ui/window.h" |
28 | #include "ui/touch.h" | ||
28 | 29 | ||
29 | #include <the_Foundation/file.h> | 30 | #include <the_Foundation/file.h> |
30 | #include <the_Foundation/fileinfo.h> | 31 | #include <the_Foundation/fileinfo.h> |
@@ -60,6 +61,9 @@ static UIViewController *viewController_(iWindow *window) { | |||
60 | return NULL; | 61 | return NULL; |
61 | } | 62 | } |
62 | 63 | ||
64 | static void notifyChange_SystemTextInput_(iSystemTextInput *); | ||
65 | static BOOL isNewlineAllowed_SystemTextInput_(const iSystemTextInput *); | ||
66 | |||
63 | /*----------------------------------------------------------------------------------------------*/ | 67 | /*----------------------------------------------------------------------------------------------*/ |
64 | 68 | ||
65 | API_AVAILABLE(ios(13.0)) | 69 | API_AVAILABLE(ios(13.0)) |
@@ -159,9 +163,10 @@ API_AVAILABLE(ios(13.0)) | |||
159 | 163 | ||
160 | /*----------------------------------------------------------------------------------------------*/ | 164 | /*----------------------------------------------------------------------------------------------*/ |
161 | 165 | ||
162 | @interface AppState : NSObject<UIDocumentPickerDelegate> { | 166 | @interface AppState : NSObject<UIDocumentPickerDelegate, UITextFieldDelegate, UITextViewDelegate> { |
163 | iString *fileBeingSaved; | 167 | iString *fileBeingSaved; |
164 | iString *pickFileCommand; | 168 | iString *pickFileCommand; |
169 | iSystemTextInput *sysCtrl; | ||
165 | } | 170 | } |
166 | @property (nonatomic, assign) BOOL isHapticsAvailable; | 171 | @property (nonatomic, assign) BOOL isHapticsAvailable; |
167 | @property (nonatomic, strong) NSObject *haptic; | 172 | @property (nonatomic, strong) NSObject *haptic; |
@@ -175,9 +180,18 @@ static AppState *appState_; | |||
175 | self = [super init]; | 180 | self = [super init]; |
176 | fileBeingSaved = NULL; | 181 | fileBeingSaved = NULL; |
177 | pickFileCommand = NULL; | 182 | pickFileCommand = NULL; |
183 | sysCtrl = NULL; | ||
178 | return self; | 184 | return self; |
179 | } | 185 | } |
180 | 186 | ||
187 | -(void)setSystemTextInput:(iSystemTextInput *)sys { | ||
188 | sysCtrl = sys; | ||
189 | } | ||
190 | |||
191 | -(iSystemTextInput *)systemTextInput { | ||
192 | return sysCtrl; | ||
193 | } | ||
194 | |||
181 | -(void)setPickFileCommand:(const char *)command { | 195 | -(void)setPickFileCommand:(const char *)command { |
182 | if (!pickFileCommand) { | 196 | if (!pickFileCommand) { |
183 | pickFileCommand = new_String(); | 197 | pickFileCommand = new_String(); |
@@ -256,6 +270,46 @@ didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls { | |||
256 | -(void)keyboardOffScreen:(NSNotification *)notification { | 270 | -(void)keyboardOffScreen:(NSNotification *)notification { |
257 | setKeyboardHeight_MainWindow(get_MainWindow(), 0); | 271 | setKeyboardHeight_MainWindow(get_MainWindow(), 0); |
258 | } | 272 | } |
273 | |||
274 | static void sendReturnKeyPress_(void) { | ||
275 | SDL_Event ev = { .type = SDL_KEYDOWN }; | ||
276 | ev.key.timestamp = SDL_GetTicks(); | ||
277 | ev.key.keysym.sym = SDLK_RETURN; | ||
278 | ev.key.state = SDL_PRESSED; | ||
279 | SDL_PushEvent(&ev); | ||
280 | ev.type = SDL_KEYUP; | ||
281 | ev.key.state = SDL_RELEASED; | ||
282 | SDL_PushEvent(&ev); | ||
283 | } | ||
284 | |||
285 | - (BOOL)textFieldShouldReturn:(UITextField *)textField { | ||
286 | sendReturnKeyPress_(); | ||
287 | return NO; | ||
288 | } | ||
289 | |||
290 | - (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range | ||
291 | replacementString:(NSString *)string { | ||
292 | iSystemTextInput *sysCtrl = [appState_ systemTextInput]; | ||
293 | notifyChange_SystemTextInput_(sysCtrl); | ||
294 | return YES; | ||
295 | } | ||
296 | |||
297 | - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range | ||
298 | replacementText:(NSString *)text { | ||
299 | if ([text isEqualToString:@"\n"]) { | ||
300 | if (!isNewlineAllowed_SystemTextInput_([appState_ systemTextInput])) { | ||
301 | sendReturnKeyPress_(); | ||
302 | return NO; | ||
303 | } | ||
304 | } | ||
305 | return YES; | ||
306 | } | ||
307 | |||
308 | - (void)textViewDidChange:(UITextView *)textView { | ||
309 | iSystemTextInput *sysCtrl = [appState_ systemTextInput]; | ||
310 | notifyChange_SystemTextInput_(sysCtrl); | ||
311 | } | ||
312 | |||
259 | @end | 313 | @end |
260 | 314 | ||
261 | static void enableMouse_(iBool yes) { | 315 | static void enableMouse_(iBool yes) { |
@@ -483,6 +537,35 @@ void pickFile_iOS(const char *command) { | |||
483 | [viewController_(get_Window()) presentViewController:picker animated:YES completion:nil]; | 537 | [viewController_(get_Window()) presentViewController:picker animated:YES completion:nil]; |
484 | } | 538 | } |
485 | 539 | ||
540 | static void openActivityView_(NSArray *activityItems) { | ||
541 | UIActivityViewController *actView = | ||
542 | [[UIActivityViewController alloc] | ||
543 | initWithActivityItems:activityItems | ||
544 | applicationActivities:nil]; | ||
545 | iWindow *win = get_Window(); | ||
546 | UIViewController *viewCtl = viewController_(win); | ||
547 | UIPopoverPresentationController *popover = [actView popoverPresentationController]; | ||
548 | if (popover) { | ||
549 | [popover setSourceView:[viewCtl view]]; | ||
550 | iInt2 tapPos = latestTapPosition_Touch(); | ||
551 | tapPos.x /= win->pixelRatio; | ||
552 | tapPos.y /= win->pixelRatio; | ||
553 | [popover setSourceRect:(CGRect){{tapPos.x - 10, tapPos.y - 10}, {20, 20}}]; | ||
554 | [popover setCanOverlapSourceViewRect:YES]; | ||
555 | } | ||
556 | [viewCtl presentViewController:actView animated:YES completion:nil]; | ||
557 | } | ||
558 | |||
559 | void openTextActivityView_iOS(const iString *text) { | ||
560 | openActivityView_(@[[NSString stringWithUTF8String:cstr_String(text)]]); | ||
561 | } | ||
562 | |||
563 | void openFileActivityView_iOS(const iString *path) { | ||
564 | NSURL *url = [NSURL fileURLWithPath:[[NSString alloc] initWithCString:cstr_String(path) | ||
565 | encoding:NSUTF8StringEncoding]]; | ||
566 | openActivityView_(@[url]); | ||
567 | } | ||
568 | |||
486 | /*----------------------------------------------------------------------------------------------*/ | 569 | /*----------------------------------------------------------------------------------------------*/ |
487 | 570 | ||
488 | enum iAVFAudioPlayerState { | 571 | enum iAVFAudioPlayerState { |
@@ -511,6 +594,10 @@ void init_AVFAudioPlayer(iAVFAudioPlayer *d) { | |||
511 | 594 | ||
512 | void deinit_AVFAudioPlayer(iAVFAudioPlayer *d) { | 595 | void deinit_AVFAudioPlayer(iAVFAudioPlayer *d) { |
513 | setInput_AVFAudioPlayer(d, NULL, NULL); | 596 | setInput_AVFAudioPlayer(d, NULL, NULL); |
597 | if (d->player) { | ||
598 | CFBridgingRelease(d->player); | ||
599 | d->player = nil; | ||
600 | } | ||
514 | } | 601 | } |
515 | 602 | ||
516 | static const char *cacheDir_ = "~/Library/Caches/Audio"; | 603 | static const char *cacheDir_ = "~/Library/Caches/Audio"; |
@@ -607,3 +694,233 @@ iBool isStarted_AVFAudioPlayer(const iAVFAudioPlayer *d) { | |||
607 | iBool isPaused_AVFAudioPlayer(const iAVFAudioPlayer *d) { | 694 | iBool isPaused_AVFAudioPlayer(const iAVFAudioPlayer *d) { |
608 | return d->state == paused_AVFAudioPlayerState; | 695 | return d->state == paused_AVFAudioPlayerState; |
609 | } | 696 | } |
697 | |||
698 | /*----------------------------------------------------------------------------------------------*/ | ||
699 | |||
700 | struct Impl_SystemTextInput { | ||
701 | int flags; | ||
702 | void *field; /* single-line text field */ | ||
703 | void *view; /* multi-line text view */ | ||
704 | void (*textChangedFunc)(iSystemTextInput *, void *); | ||
705 | void *textChangedContext; | ||
706 | }; | ||
707 | |||
708 | iDefineTypeConstructionArgs(SystemTextInput, (iRect rect, int flags), rect, flags) | ||
709 | |||
710 | #define REF_d_field (__bridge UITextField *)d->field | ||
711 | #define REF_d_view (__bridge UITextView *)d->view | ||
712 | |||
713 | static CGRect convertToCGRect_(const iRect *rect, iBool expanded) { | ||
714 | const iWindow *win = get_Window(); | ||
715 | CGRect frame; | ||
716 | // TODO: Convert coordinates properly! | ||
717 | frame.origin.x = rect->pos.x / win->pixelRatio; | ||
718 | frame.origin.y = (rect->pos.y - gap_UI + 1) / win->pixelRatio; | ||
719 | frame.size.width = rect->size.x / win->pixelRatio; | ||
720 | frame.size.height = rect->size.y / win->pixelRatio; | ||
721 | /* Some padding to account for insets. If we just zero out the insets, the insertion point | ||
722 | may be clipped at the edges. */ | ||
723 | if (expanded) { | ||
724 | const float inset = gap_UI / get_Window()->pixelRatio; | ||
725 | frame.origin.x -= inset + 1; | ||
726 | frame.origin.y -= inset + 1; | ||
727 | frame.size.width += 2 * inset + 2; | ||
728 | frame.size.height += inset + 1 + inset; | ||
729 | } | ||
730 | return frame; | ||
731 | } | ||
732 | |||
733 | static UIColor *makeUIColor_(enum iColorId colorId) { | ||
734 | iColor color = get_Color(colorId); | ||
735 | return [UIColor colorWithRed:color.r / 255.0 | ||
736 | green:color.g / 255.0 | ||
737 | blue:color.b / 255.0 | ||
738 | alpha:color.a / 255.0]; | ||
739 | } | ||
740 | |||
741 | void init_SystemTextInput(iSystemTextInput *d, iRect rect, int flags) { | ||
742 | d->flags = flags; | ||
743 | d->field = NULL; | ||
744 | d->view = NULL; | ||
745 | CGRect frame = convertToCGRect_(&rect, (flags & multiLine_SystemTextInputFlags) != 0); | ||
746 | if (flags & multiLine_SystemTextInputFlags) { | ||
747 | d->view = (void *) CFBridgingRetain([[UITextView alloc] initWithFrame:frame textContainer:nil]); | ||
748 | [[viewController_(get_Window()) view] addSubview:REF_d_view]; | ||
749 | } | ||
750 | else { | ||
751 | d->field = (void *) CFBridgingRetain([[UITextField alloc] initWithFrame:frame]); | ||
752 | [[viewController_(get_Window()) view] addSubview:REF_d_field]; | ||
753 | } | ||
754 | UIControl<UITextInputTraits> *traits = (UIControl<UITextInputTraits> *) (d->view ? REF_d_view : REF_d_field); | ||
755 | if (~flags & insertNewlines_SystemTextInputFlag) { | ||
756 | [traits setReturnKeyType:UIReturnKeyDone]; | ||
757 | } | ||
758 | if (flags & returnGo_SystemTextInputFlags) { | ||
759 | [traits setReturnKeyType:UIReturnKeyGo]; | ||
760 | } | ||
761 | if (flags & returnSend_SystemTextInputFlags) { | ||
762 | [traits setReturnKeyType:UIReturnKeySend]; | ||
763 | } | ||
764 | if (flags & disableAutocorrect_SystemTextInputFlag) { | ||
765 | [traits setAutocorrectionType:UITextAutocorrectionTypeNo]; | ||
766 | [traits setSpellCheckingType:UITextSpellCheckingTypeNo]; | ||
767 | } | ||
768 | if (flags & disableAutocapitalize_SystemTextInputFlag) { | ||
769 | [traits setAutocapitalizationType:UITextAutocapitalizationTypeNone]; | ||
770 | } | ||
771 | if (flags & alignRight_SystemTextInputFlag) { | ||
772 | if (d->field) { | ||
773 | [REF_d_field setTextAlignment:NSTextAlignmentRight]; | ||
774 | } | ||
775 | if (d->view) { | ||
776 | [REF_d_view setTextAlignment:NSTextAlignmentRight]; | ||
777 | } | ||
778 | } | ||
779 | UIColor *textColor = makeUIColor_(uiInputTextFocused_ColorId); | ||
780 | UIColor *backgroundColor = makeUIColor_(uiInputBackgroundFocused_ColorId); | ||
781 | UIColor *tintColor = makeUIColor_(uiInputFrameHover_ColorId); /* use the accent color */ //uiInputCursor_ColorId); | ||
782 | [appState_ setSystemTextInput:d]; | ||
783 | if (d->field) { | ||
784 | UITextField *field = REF_d_field; | ||
785 | [field setTextColor:textColor]; | ||
786 | [field setTintColor:tintColor]; | ||
787 | [field setDelegate:appState_]; | ||
788 | [field becomeFirstResponder]; | ||
789 | } | ||
790 | else { | ||
791 | UITextView *view = REF_d_view; | ||
792 | [view setBackgroundColor:[UIColor colorWithWhite:1.0f alpha:0.0f]]; | ||
793 | [view setTextColor:textColor]; | ||
794 | [view setTintColor:tintColor]; | ||
795 | if (flags & extraPadding_SystemTextInputFlag) { | ||
796 | [view setContentInset:(UIEdgeInsets){ 0, 0, 3 * gap_UI / get_Window()->pixelRatio, 0}]; | ||
797 | } | ||
798 | [view setEditable:YES]; | ||
799 | [view setDelegate:appState_]; | ||
800 | [view becomeFirstResponder]; | ||
801 | } | ||
802 | d->textChangedFunc = NULL; | ||
803 | d->textChangedContext = NULL; | ||
804 | } | ||
805 | |||
806 | void deinit_SystemTextInput(iSystemTextInput *d) { | ||
807 | [appState_ setSystemTextInput:nil]; | ||
808 | if (d->field) { | ||
809 | [REF_d_field removeFromSuperview]; | ||
810 | CFBridgingRelease(d->field); | ||
811 | d->field = nil; | ||
812 | } | ||
813 | if (d->view) { | ||
814 | [REF_d_view removeFromSuperview]; | ||
815 | CFBridgingRelease(d->view); | ||
816 | d->view = nil; | ||
817 | } | ||
818 | } | ||
819 | |||
820 | void selectAll_SystemTextInput(iSystemTextInput *d) { | ||
821 | if (d->field) { | ||
822 | [REF_d_field selectAll:nil]; | ||
823 | } | ||
824 | if (d->view) { | ||
825 | [REF_d_view selectAll:nil]; | ||
826 | } | ||
827 | } | ||
828 | |||
829 | void setText_SystemTextInput(iSystemTextInput *d, const iString *text, iBool allowUndo) { | ||
830 | NSString *str = [NSString stringWithUTF8String:cstr_String(text)]; | ||
831 | if (d->field) { | ||
832 | [REF_d_field setText:str]; | ||
833 | if (d->flags & selectAll_SystemTextInputFlags) { | ||
834 | [REF_d_field selectAll:nil]; | ||
835 | } | ||
836 | } | ||
837 | else { | ||
838 | UITextView *view = REF_d_view; | ||
839 | // if (allowUndo) { | ||
840 | // [view selectAll:nil]; | ||
841 | // if ([view shouldChangeTextInRange:[view selectedTextRange] replacementText:@""]) { | ||
842 | // [[view textStorage] beginEditing]; | ||
843 | // [[view textStorage] replaceCharactersInRange:[view selectedRange] withString:@""]; | ||
844 | // [[view textStorage] endEditing]; | ||
845 | // } | ||
846 | // } | ||
847 | // else { | ||
848 | // TODO: How to implement `allowUndo`, given that UITextView does not exist when unfocused? | ||
849 | // Maybe keep the UITextStorage (if it has the undo?)? | ||
850 | [view setText:str]; | ||
851 | // } | ||
852 | if (d->flags & selectAll_SystemTextInputFlags) { | ||
853 | [view selectAll:nil]; | ||
854 | } | ||
855 | } | ||
856 | } | ||
857 | |||
858 | int preferredHeight_SystemTextInput(const iSystemTextInput *d) { | ||
859 | if (d->view) { | ||
860 | CGRect usedRect = [[REF_d_view layoutManager] usedRectForTextContainer:[REF_d_view textContainer]]; | ||
861 | return usedRect.size.height * get_Window()->pixelRatio; | ||
862 | } | ||
863 | return 0; | ||
864 | } | ||
865 | |||
866 | void setFont_SystemTextInput(iSystemTextInput *d, int fontId) { | ||
867 | float height = lineHeight_Text(fontId) / get_Window()->pixelRatio; | ||
868 | UIFont *font; | ||
869 | // for (NSString *name in [UIFont familyNames]) { | ||
870 | // printf("family: %s\n", [name cStringUsingEncoding:NSUTF8StringEncoding]); | ||
871 | // } | ||
872 | if (fontId / maxVariants_Fonts * maxVariants_Fonts == monospace_FontId) { | ||
873 | // font = [UIFont monospacedSystemFontOfSize:0.8f * height weight:UIFontWeightRegular]; | ||
874 | // for (NSString *name in [UIFont fontNamesForFamilyName:@"Iosevka Term"]) { | ||
875 | // printf("fontname: %s\n", [name cStringUsingEncoding:NSUTF8StringEncoding]); | ||
876 | // } | ||
877 | font = [UIFont fontWithName:@"Iosevka-Term-Extended" size:height * 0.82f]; | ||
878 | } | ||
879 | else { | ||
880 | // font = [UIFont systemFontOfSize:0.65f * height]; | ||
881 | font = [UIFont fontWithName:@"SourceSans3-Regular" size:height * 0.7f]; | ||
882 | } | ||
883 | if (d->field) { | ||
884 | [REF_d_field setFont:font]; | ||
885 | } | ||
886 | if (d->view) { | ||
887 | [REF_d_view setFont:font]; | ||
888 | } | ||
889 | } | ||
890 | |||
891 | const iString *text_SystemTextInput(const iSystemTextInput *d) { | ||
892 | if (d->field) { | ||
893 | return collectNewCStr_String([[REF_d_field text] cStringUsingEncoding:NSUTF8StringEncoding]); | ||
894 | } | ||
895 | if (d->view) { | ||
896 | return collectNewCStr_String([[REF_d_view text] cStringUsingEncoding:NSUTF8StringEncoding]); | ||
897 | } | ||
898 | return NULL; | ||
899 | } | ||
900 | |||
901 | void setRect_SystemTextInput(iSystemTextInput *d, iRect rect) { | ||
902 | CGRect frame = convertToCGRect_(&rect, (d->flags & multiLine_SystemTextInputFlags) != 0); | ||
903 | if (d->field) { | ||
904 | [REF_d_field setFrame:frame]; | ||
905 | } | ||
906 | else { | ||
907 | [REF_d_view setFrame:frame]; | ||
908 | } | ||
909 | } | ||
910 | |||
911 | void setTextChangedFunc_SystemTextInput(iSystemTextInput *d, | ||
912 | void (*textChangedFunc)(iSystemTextInput *, void *), | ||
913 | void *context) { | ||
914 | d->textChangedFunc = textChangedFunc; | ||
915 | d->textChangedContext = context; | ||
916 | } | ||
917 | |||
918 | static void notifyChange_SystemTextInput_(iSystemTextInput *d) { | ||
919 | if (d && d->textChangedFunc) { | ||
920 | d->textChangedFunc(d, d->textChangedContext); | ||
921 | } | ||
922 | } | ||
923 | |||
924 | static BOOL isNewlineAllowed_SystemTextInput_(const iSystemTextInput *d) { | ||
925 | return (d->flags & insertNewlines_SystemTextInputFlag) != 0; | ||
926 | } | ||
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); | |||
39 | void insertMenuItems_MacOS (const char *menuLabel, int atIndex, const iMenuItem *items, size_t count); | 39 | void insertMenuItems_MacOS (const char *menuLabel, int atIndex, const iMenuItem *items, size_t count); |
40 | void removeMenu_MacOS (int atIndex); | 40 | void removeMenu_MacOS (int atIndex); |
41 | void enableMenu_MacOS (const char *menuLabel, iBool enable); | 41 | void enableMenu_MacOS (const char *menuLabel, iBool enable); |
42 | void enableMenuIndex_MacOS (int index, iBool enable); | ||
42 | void enableMenuItem_MacOS (const char *menuItemCommand, iBool enable); | 43 | void enableMenuItem_MacOS (const char *menuItemCommand, iBool enable); |
43 | void enableMenuItemsByKey_MacOS (int key, int kmods, iBool enable); | 44 | void enableMenuItemsByKey_MacOS (int key, int kmods, iBool enable); |
45 | void enableMenuItemsOnHomeRow_MacOS(iBool enable); | ||
44 | void handleCommand_MacOS (const char *cmd); | 46 | void handleCommand_MacOS (const char *cmd); |
45 | 47 | ||
46 | void showPopupMenu_MacOS (iWidget *source, iInt2 windowCoord, const iMenuItem *items, size_t n); | 48 | 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..4ad267c1 100644 --- a/src/macos.m +++ b/src/macos.m | |||
@@ -31,6 +31,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
31 | 31 | ||
32 | #include <SDL_timer.h> | 32 | #include <SDL_timer.h> |
33 | #include <SDL_syswm.h> | 33 | #include <SDL_syswm.h> |
34 | #include <the_Foundation/stringset.h> | ||
34 | 35 | ||
35 | #import <AppKit/AppKit.h> | 36 | #import <AppKit/AppKit.h> |
36 | 37 | ||
@@ -110,7 +111,7 @@ static void ignoreImmediateKeyDownEvents_(void) { | |||
110 | - (id)initWithIdentifier:(NSTouchBarItemIdentifier)identifier | 111 | - (id)initWithIdentifier:(NSTouchBarItemIdentifier)identifier |
111 | title:(NSString *)title | 112 | title:(NSString *)title |
112 | command:(NSString *)cmd { | 113 | command:(NSString *)cmd { |
113 | [super initWithIdentifier:identifier]; | 114 | self = [super initWithIdentifier:identifier]; |
114 | self.view = [NSButton buttonWithTitle:title target:self action:@selector(buttonPressed)]; | 115 | self.view = [NSButton buttonWithTitle:title target:self action:@selector(buttonPressed)]; |
115 | command = cmd; | 116 | command = cmd; |
116 | return self; | 117 | return self; |
@@ -120,7 +121,7 @@ static void ignoreImmediateKeyDownEvents_(void) { | |||
120 | image:(NSImage *)image | 121 | image:(NSImage *)image |
121 | widget:(iWidget *)widget | 122 | widget:(iWidget *)widget |
122 | command:(NSString *)cmd { | 123 | command:(NSString *)cmd { |
123 | [super initWithIdentifier:identifier]; | 124 | self = [super initWithIdentifier:identifier]; |
124 | self.view = [NSButton buttonWithImage:image target:self action:@selector(buttonPressed)]; | 125 | self.view = [NSButton buttonWithImage:image target:self action:@selector(buttonPressed)]; |
125 | command = cmd; | 126 | command = cmd; |
126 | return self; | 127 | return self; |
@@ -163,12 +164,13 @@ static void ignoreImmediateKeyDownEvents_(void) { | |||
163 | @implementation MenuCommands | 164 | @implementation MenuCommands |
164 | 165 | ||
165 | - (id)init { | 166 | - (id)init { |
167 | self = [super init]; | ||
166 | commands = [[NSMutableDictionary<NSString *, NSString *> alloc] init]; | 168 | commands = [[NSMutableDictionary<NSString *, NSString *> alloc] init]; |
167 | source = NULL; | 169 | source = NULL; |
168 | return self; | 170 | return self; |
169 | } | 171 | } |
170 | 172 | ||
171 | - (void)setCommand:(NSString *)command forMenuItem:(NSMenuItem *)menuItem { | 173 | - (void)setCommand:(NSString * __nonnull)command forMenuItem:(NSMenuItem * __nonnull)menuItem { |
172 | [commands setObject:command forKey:[menuItem title]]; | 174 | [commands setObject:command forKey:[menuItem title]]; |
173 | } | 175 | } |
174 | 176 | ||
@@ -220,7 +222,7 @@ static void ignoreImmediateKeyDownEvents_(void) { | |||
220 | @implementation MyDelegate | 222 | @implementation MyDelegate |
221 | 223 | ||
222 | - (id)initWithSDLDelegate:(NSObject<NSApplicationDelegate> *)sdl { | 224 | - (id)initWithSDLDelegate:(NSObject<NSApplicationDelegate> *)sdl { |
223 | [super init]; | 225 | self = [super init]; |
224 | currentAppearanceName = nil; | 226 | currentAppearanceName = nil; |
225 | menuCommands = [[MenuCommands alloc] init]; | 227 | menuCommands = [[MenuCommands alloc] init]; |
226 | touchBarVariant = default_TouchBarVariant; | 228 | touchBarVariant = default_TouchBarVariant; |
@@ -402,6 +404,131 @@ void registerURLHandler_MacOS(void) { | |||
402 | [handler release]; | 404 | [handler release]; |
403 | } | 405 | } |
404 | 406 | ||
407 | #if 0 | ||
408 | static iBool isTracking_; | ||
409 | |||
410 | static void trackSwipe_(NSEvent *event) { | ||
411 | if (isTracking_) { | ||
412 | return; | ||
413 | } | ||
414 | isTracking_ = iTrue; | ||
415 | [event trackSwipeEventWithOptions:NSEventSwipeTrackingLockDirection | ||
416 | dampenAmountThresholdMin:-1.0 | ||
417 | max:1.0 | ||
418 | usingHandler:^(CGFloat gestureAmount, NSEventPhase phase, | ||
419 | BOOL isComplete, BOOL *stop) { | ||
420 | printf("TRACK: amount:%f phase:%lu complete:%d\n", | ||
421 | gestureAmount, (unsigned long) phase, isComplete); | ||
422 | fflush(stdout); | ||
423 | if (isComplete) { | ||
424 | isTracking_ = iFalse; | ||
425 | } | ||
426 | } | ||
427 | ]; | ||
428 | } | ||
429 | #endif | ||
430 | |||
431 | static int swipeDir_ = 0; | ||
432 | static int preventTapGlitch_ = 0; | ||
433 | |||
434 | static iBool processScrollWheelEvent_(NSEvent *event) { | ||
435 | const iBool isPerPixel = (event.hasPreciseScrollingDeltas != 0); | ||
436 | const iBool isInertia = (event.momentumPhase & (NSEventPhaseBegan | NSEventPhaseChanged)) != 0; | ||
437 | const iBool isEnded = event.scrollingDeltaX == 0.0f && event.scrollingDeltaY == 0.0f && !isInertia; | ||
438 | const iWindow *win = &get_MainWindow()->base; | ||
439 | if (isPerPixel) { | ||
440 | /* On macOS 12.1, stopping ongoing inertia scroll with a tap seems to sometimes produce | ||
441 | spurious large scroll events. */ | ||
442 | switch (preventTapGlitch_) { | ||
443 | case 0: | ||
444 | if (isInertia && event.momentumPhase == NSEventPhaseChanged) { | ||
445 | preventTapGlitch_++; | ||
446 | } | ||
447 | else { | ||
448 | preventTapGlitch_ = 0; | ||
449 | } | ||
450 | break; | ||
451 | case 1: | ||
452 | if (event.scrollingDeltaY == 0 && event.momentumPhase == NSEventPhaseEnded) { | ||
453 | preventTapGlitch_++; | ||
454 | } | ||
455 | break; | ||
456 | case 2: | ||
457 | if (event.scrollingDeltaY == 0 && event.momentumPhase == 0 && isEnded) { | ||
458 | preventTapGlitch_++; | ||
459 | } | ||
460 | else { | ||
461 | preventTapGlitch_ = 0; | ||
462 | } | ||
463 | break; | ||
464 | case 3: | ||
465 | if (event.scrollingDeltaY != 0 && event.momentumPhase == 0 && !isInertia) { | ||
466 | preventTapGlitch_ = 0; | ||
467 | // printf("SPURIOUS\n"); fflush(stdout); | ||
468 | return iTrue; | ||
469 | } | ||
470 | preventTapGlitch_ = 0; | ||
471 | break; | ||
472 | } | ||
473 | } | ||
474 | /* Post corresponding MOUSEWHEEL events. */ | ||
475 | SDL_MouseWheelEvent e = { .type = SDL_MOUSEWHEEL }; | ||
476 | e.timestamp = SDL_GetTicks(); | ||
477 | e.which = isPerPixel ? 0 : 1; /* Distinction between trackpad and regular mouse. TODO: Still needed? */ | ||
478 | setPerPixel_MouseWheelEvent(&e, isPerPixel); | ||
479 | if (isPerPixel) { | ||
480 | setInertia_MouseWheelEvent(&e, isInertia); | ||
481 | setScrollFinished_MouseWheelEvent(&e, isEnded); | ||
482 | e.x = event.scrollingDeltaX * win->pixelRatio; | ||
483 | e.y = event.scrollingDeltaY * win->pixelRatio; | ||
484 | /* Only scroll on one axis at a time. */ | ||
485 | if (swipeDir_ == 0) { | ||
486 | swipeDir_ = iAbs(e.x) > iAbs(e.y) ? 1 : 2; | ||
487 | } | ||
488 | if (swipeDir_ == 1) { | ||
489 | e.y = 0; | ||
490 | } | ||
491 | else if (swipeDir_ == 2) { | ||
492 | e.x = 0; | ||
493 | } | ||
494 | if (isEnded) { | ||
495 | swipeDir_ = 0; | ||
496 | } | ||
497 | } | ||
498 | else { | ||
499 | /* Disregard wheel acceleration applied by the OS. */ | ||
500 | e.x = -event.scrollingDeltaX; | ||
501 | e.y = iSign(event.scrollingDeltaY); | ||
502 | } | ||
503 | // printf("#### [%d] dx:%d dy:%d phase:%ld inertia:%d end:%d\n", preventTapGlitch_, e.x, e.y, (long) event.momentumPhase, | ||
504 | // isInertia, isEnded); fflush(stdout); | ||
505 | SDL_PushEvent((SDL_Event *) &e); | ||
506 | #if 0 | ||
507 | /* On macOS, we handle both trackpad and mouse events. We expect SDL to identify | ||
508 | which device is sending the event. */ | ||
509 | if (ev.wheel.which == 0) { | ||
510 | /* Trackpad with precise scrolling w/inertia (points). */ | ||
511 | setPerPixel_MouseWheelEvent(&ev.wheel, iTrue); | ||
512 | ev.wheel.x *= -d->window->base.pixelRatio; | ||
513 | ev.wheel.y *= d->window->base.pixelRatio; | ||
514 | /* Only scroll on one axis at a time. */ | ||
515 | if (iAbs(ev.wheel.x) > iAbs(ev.wheel.y)) { | ||
516 | ev.wheel.y = 0; | ||
517 | } | ||
518 | else { | ||
519 | ev.wheel.x = 0; | ||
520 | } | ||
521 | } | ||
522 | else { | ||
523 | /* Disregard wheel acceleration applied by the OS. */ | ||
524 | ev.wheel.x = -ev.wheel.x; | ||
525 | ev.wheel.y = iSign(ev.wheel.y); | ||
526 | } | ||
527 | #endif | ||
528 | |||
529 | return iTrue; | ||
530 | } | ||
531 | |||
405 | void setupApplication_MacOS(void) { | 532 | void setupApplication_MacOS(void) { |
406 | NSApplication *app = [NSApplication sharedApplication]; | 533 | NSApplication *app = [NSApplication sharedApplication]; |
407 | [app setActivationPolicy:NSApplicationActivationPolicyRegular]; | 534 | [app setActivationPolicy:NSApplicationActivationPolicyRegular]; |
@@ -423,6 +550,21 @@ void setupApplication_MacOS(void) { | |||
423 | NSMenuItem *windowCloseItem = [windowMenu itemWithTitle:@"Close"]; | 550 | NSMenuItem *windowCloseItem = [windowMenu itemWithTitle:@"Close"]; |
424 | windowCloseItem.target = myDel; | 551 | windowCloseItem.target = myDel; |
425 | windowCloseItem.action = @selector(closeTab); | 552 | windowCloseItem.action = @selector(closeTab); |
553 | [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskScrollWheel | ||
554 | handler:^NSEvent*(NSEvent *event){ | ||
555 | // printf("event type: %lu\n", (unsigned long) event.type); | ||
556 | // fflush(stdout); | ||
557 | // if (event.type == NSEventTypeGesture) { | ||
558 | // trackSwipe_(event); | ||
559 | // printf("GESTURE phase:%lu\n", (unsigned long) event.phase); | ||
560 | //fflush(stdout); | ||
561 | // } | ||
562 | if (event.type == NSEventTypeScrollWheel && | ||
563 | processScrollWheelEvent_(event)) { | ||
564 | return nil; /* was eaten */ | ||
565 | } | ||
566 | return event; | ||
567 | }]; | ||
426 | } | 568 | } |
427 | 569 | ||
428 | void hideTitleBar_MacOS(iWindow *window) { | 570 | void hideTitleBar_MacOS(iWindow *window) { |
@@ -439,6 +581,13 @@ void enableMenu_MacOS(const char *menuLabel, iBool enable) { | |||
439 | [menuItem setEnabled:enable]; | 581 | [menuItem setEnabled:enable]; |
440 | } | 582 | } |
441 | 583 | ||
584 | void enableMenuIndex_MacOS(int index, iBool enable) { | ||
585 | NSApplication *app = [NSApplication sharedApplication]; | ||
586 | NSMenu *appMenu = [app mainMenu]; | ||
587 | NSMenuItem *menuItem = [appMenu itemAtIndex:index]; | ||
588 | [menuItem setEnabled:enable]; | ||
589 | } | ||
590 | |||
442 | void enableMenuItem_MacOS(const char *menuItemCommand, iBool enable) { | 591 | void enableMenuItem_MacOS(const char *menuItemCommand, iBool enable) { |
443 | NSApplication *app = [NSApplication sharedApplication]; | 592 | NSApplication *app = [NSApplication sharedApplication]; |
444 | NSMenu *appMenu = [app mainMenu]; | 593 | NSMenu *appMenu = [app mainMenu]; |
@@ -513,6 +662,47 @@ void enableMenuItemsByKey_MacOS(int key, int kmods, iBool enable) { | |||
513 | delete_String(keyEquiv); | 662 | delete_String(keyEquiv); |
514 | } | 663 | } |
515 | 664 | ||
665 | void enableMenuItemsOnHomeRow_MacOS(iBool enable) { | ||
666 | iStringSet *homeRowKeys = new_StringSet(); | ||
667 | const char *keys[] = { /* Note: another array in documentwidget.c */ | ||
668 | "f", "d", "s", "a", | ||
669 | "j", "k", "l", | ||
670 | "r", "e", "w", "q", | ||
671 | "u", "i", "o", "p", | ||
672 | "v", "c", "x", "z", | ||
673 | "m", "n", | ||
674 | "g", "h", | ||
675 | "b", | ||
676 | "t", "y" | ||
677 | }; | ||
678 | iForIndices(i, keys) { | ||
679 | iString str; | ||
680 | initCStr_String(&str, keys[i]); | ||
681 | insert_StringSet(homeRowKeys, &str); | ||
682 | deinit_String(&str); | ||
683 | } | ||
684 | NSApplication *app = [NSApplication sharedApplication]; | ||
685 | NSMenu *appMenu = [app mainMenu]; | ||
686 | for (NSMenuItem *mainMenuItem in appMenu.itemArray) { | ||
687 | NSMenu *menu = mainMenuItem.submenu; | ||
688 | if (menu) { | ||
689 | for (NSMenuItem *menuItem in menu.itemArray) { | ||
690 | if (menuItem.keyEquivalentModifierMask == 0) { | ||
691 | iString equiv; | ||
692 | initCStr_String(&equiv, [menuItem.keyEquivalent | ||
693 | cStringUsingEncoding:NSUTF8StringEncoding]); | ||
694 | if (contains_StringSet(homeRowKeys, &equiv)) { | ||
695 | [menuItem setEnabled:enable]; | ||
696 | [menu setAutoenablesItems:NO]; | ||
697 | } | ||
698 | deinit_String(&equiv); | ||
699 | } | ||
700 | } | ||
701 | } | ||
702 | } | ||
703 | iRelease(homeRowKeys); | ||
704 | } | ||
705 | |||
516 | static void setShortcut_NSMenuItem_(NSMenuItem *item, int key, int kmods) { | 706 | static void setShortcut_NSMenuItem_(NSMenuItem *item, int key, int kmods) { |
517 | NSEventModifierFlags modMask; | 707 | NSEventModifierFlags modMask; |
518 | iString *str = composeKeyEquivalent_(key, kmods, &modMask); | 708 | iString *str = composeKeyEquivalent_(key, kmods, &modMask); |
@@ -541,6 +731,29 @@ enum iColorId removeColorEscapes_String(iString *d) { | |||
541 | return color; | 731 | return color; |
542 | } | 732 | } |
543 | 733 | ||
734 | static NSString *cleanString_(const iString *ansiEscapedText) { | ||
735 | iString mod; | ||
736 | initCopy_String(&mod, ansiEscapedText); | ||
737 | iRegExp *ansi = makeAnsiEscapePattern_Text(); | ||
738 | replaceRegExp_String(&mod, ansi, "", NULL, NULL); | ||
739 | iRelease(ansi); | ||
740 | NSString *clean = [NSString stringWithUTF8String:cstr_String(&mod)]; | ||
741 | deinit_String(&mod); | ||
742 | return clean; | ||
743 | } | ||
744 | |||
745 | #if 0 | ||
746 | static NSAttributedString *makeAttributedString_(const iString *ansiEscapedText) { | ||
747 | iString mod; | ||
748 | initCopy_String(&mod, ansiEscapedText); | ||
749 | NSData *data = [NSData dataWithBytesNoCopy:data_Block(&mod.chars) length:size_String(&mod)]; | ||
750 | NSAttributedString *as = [[NSAttributedString alloc] initWithHTML:data | ||
751 | documentAttributes:nil]; | ||
752 | deinit_String(&mod); | ||
753 | return as; | ||
754 | } | ||
755 | #endif | ||
756 | |||
544 | /* returns the selected item, if any */ | 757 | /* returns the selected item, if any */ |
545 | static NSMenuItem *makeMenuItems_(NSMenu *menu, MenuCommands *commands, const iMenuItem *items, size_t n) { | 758 | static NSMenuItem *makeMenuItems_(NSMenu *menu, MenuCommands *commands, const iMenuItem *items, size_t n) { |
546 | NSMenuItem *selectedItem = nil; | 759 | NSMenuItem *selectedItem = nil; |
@@ -557,7 +770,7 @@ static NSMenuItem *makeMenuItems_(NSMenu *menu, MenuCommands *commands, const iM | |||
557 | isChecked = iTrue; | 770 | isChecked = iTrue; |
558 | label += 3; | 771 | label += 3; |
559 | } | 772 | } |
560 | else if (startsWith_CStr(label, "///")) { | 773 | else if (startsWith_CStr(label, "///") || startsWith_CStr(label, "```")) { |
561 | isDisabled = iTrue; | 774 | isDisabled = iTrue; |
562 | label += 3; | 775 | label += 3; |
563 | } | 776 | } |
@@ -567,9 +780,13 @@ static NSMenuItem *makeMenuItems_(NSMenu *menu, MenuCommands *commands, const iM | |||
567 | if (removeColorEscapes_String(&itemTitle) == uiTextCaution_ColorId) { | 780 | if (removeColorEscapes_String(&itemTitle) == uiTextCaution_ColorId) { |
568 | // prependCStr_String(&itemTitle, "\u26a0\ufe0f "); | 781 | // prependCStr_String(&itemTitle, "\u26a0\ufe0f "); |
569 | } | 782 | } |
570 | NSMenuItem *item = [menu addItemWithTitle:[NSString stringWithUTF8String:cstr_String(&itemTitle)] | 783 | NSMenuItem *item = [[NSMenuItem alloc] init]; |
571 | action:(hasCommand ? @selector(postMenuItemCommand:) : nil) | 784 | /* Use attributed string to allow newlines. */ |
572 | keyEquivalent:@""]; | 785 | NSAttributedString *title = [[NSAttributedString alloc] initWithString:cleanString_(&itemTitle)]; |
786 | item.attributedTitle = title; | ||
787 | [title release]; | ||
788 | item.action = (hasCommand ? @selector(postMenuItemCommand:) : nil); | ||
789 | [menu addItem:item]; | ||
573 | deinit_String(&itemTitle); | 790 | deinit_String(&itemTitle); |
574 | [item setTarget:commands]; | 791 | [item setTarget:commands]; |
575 | if (isChecked) { | 792 | if (isChecked) { |
@@ -67,6 +67,7 @@ int main(int argc, char **argv) { | |||
67 | "ECDHE-RSA-AES128-GCM-SHA256:" | 67 | "ECDHE-RSA-AES128-GCM-SHA256:" |
68 | "DHE-RSA-AES256-GCM-SHA384"); | 68 | "DHE-RSA-AES256-GCM-SHA384"); |
69 | SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1"); | 69 | SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1"); |
70 | SDL_EnableScreenSaver(); | ||
70 | SDL_SetHint(SDL_HINT_MAC_BACKGROUND_APP, "1"); | 71 | SDL_SetHint(SDL_HINT_MAC_BACKGROUND_APP, "1"); |
71 | SDL_SetHint(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, "1"); | 72 | SDL_SetHint(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, "1"); |
72 | #if SDL_VERSION_ATLEAST(2, 0, 8) | 73 | #if SDL_VERSION_ATLEAST(2, 0, 8) |
@@ -82,9 +83,6 @@ int main(int argc, char **argv) { | |||
82 | fprintf(stderr, "[SDL] init failed: %s\n", SDL_GetError()); | 83 | fprintf(stderr, "[SDL] init failed: %s\n", SDL_GetError()); |
83 | return -1; | 84 | return -1; |
84 | } | 85 | } |
85 | if (SDL_Init(SDL_INIT_AUDIO)) { | ||
86 | fprintf(stderr, "[SDL] audio init failed: %s\n", SDL_GetError()); | ||
87 | } | ||
88 | init_Updater(); | 86 | init_Updater(); |
89 | run_App(argc, argv); | 87 | run_App(argc, argv); |
90 | SDL_Quit(); | 88 | SDL_Quit(); |
diff --git a/src/media.c b/src/media.c index a3f381ec..4940c13e 100644 --- a/src/media.c +++ b/src/media.c | |||
@@ -144,7 +144,7 @@ void makeTexture_GmImage(iGmImage *d) { | |||
144 | } | 144 | } |
145 | else { | 145 | else { |
146 | imgData = stbi_load_from_memory( | 146 | imgData = stbi_load_from_memory( |
147 | constData_Block(data), size_Block(data), &d->size.x, &d->size.y, NULL, 4); | 147 | constData_Block(data), (int) size_Block(data), &d->size.x, &d->size.y, NULL, 4); |
148 | if (!imgData) { | 148 | if (!imgData) { |
149 | fprintf(stderr, "[media] image load failed: %s\n", stbi_failure_reason()); | 149 | fprintf(stderr, "[media] image load failed: %s\n", stbi_failure_reason()); |
150 | } | 150 | } |
@@ -629,6 +629,17 @@ void deinit_MediaRequest(iMediaRequest *d) { | |||
629 | iRelease(d->req); | 629 | iRelease(d->req); |
630 | } | 630 | } |
631 | 631 | ||
632 | iMediaRequest *newReused_MediaRequest(iDocumentWidget *doc, unsigned int linkId, | ||
633 | iGmRequest *request) { | ||
634 | iMediaRequest *d = new_Object(&Class_MediaRequest); | ||
635 | d->doc = doc; | ||
636 | d->linkId = linkId; | ||
637 | d->req = request; /* takes ownership */ | ||
638 | iConnect(GmRequest, d->req, updated, d, updated_MediaRequest_); | ||
639 | iConnect(GmRequest, d->req, finished, d, finished_MediaRequest_); | ||
640 | return d; | ||
641 | } | ||
642 | |||
632 | iDefineObjectConstructionArgs(MediaRequest, | 643 | iDefineObjectConstructionArgs(MediaRequest, |
633 | (iDocumentWidget *doc, unsigned int linkId, const iString *url, | 644 | (iDocumentWidget *doc, unsigned int linkId, const iString *url, |
634 | iBool enableFilters), | 645 | 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 { | |||
123 | 123 | ||
124 | iDeclareObjectConstructionArgs(MediaRequest, iDocumentWidget *doc, unsigned int linkId, | 124 | iDeclareObjectConstructionArgs(MediaRequest, iDocumentWidget *doc, unsigned int linkId, |
125 | const iString *url, iBool enableFilters) | 125 | const iString *url, iBool enableFilters) |
126 | |||
127 | iMediaRequest * newReused_MediaRequest (iDocumentWidget *doc, unsigned int linkId, | ||
128 | iGmRequest *request); | ||
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 * | |||
142 | appendCStr_String(&out, cstr_Lang("feeds.atom.translated")); | 142 | appendCStr_String(&out, cstr_Lang("feeds.atom.translated")); |
143 | appendCStr_String(&out, "\n\n"); | 143 | appendCStr_String(&out, "\n\n"); |
144 | iRegExp *datePattern = | 144 | iRegExp *datePattern = |
145 | iClob(new_RegExp("^([0-9][0-9][0-9][0-9]-[0-1][0-9]-[0-3][0-9])(T|\\s).*", caseSensitive_RegExpOption)); | 145 | iClob(new_RegExp("^\\s*([0-9][0-9][0-9][0-9]-[0-1][0-9]-[0-3][0-9])(T|\\s).*", |
146 | caseSensitive_RegExpOption)); | ||
146 | iBeginCollect(); | 147 | iBeginCollect(); |
147 | iConstForEach(PtrArray, i, &feed->children) { | 148 | iConstForEach(PtrArray, i, &feed->children) { |
148 | iEndCollect(); | 149 | iEndCollect(); |
diff --git a/src/periodic.c b/src/periodic.c index ef3d8033..b4f51ed3 100644 --- a/src/periodic.c +++ b/src/periodic.c | |||
@@ -57,6 +57,25 @@ iDefineTypeConstructionArgs(PeriodicCommand, (iAny *ctx, const char *cmd), ctx, | |||
57 | 57 | ||
58 | static const uint32_t postingInterval_Periodic_ = 500; | 58 | static const uint32_t postingInterval_Periodic_ = 500; |
59 | 59 | ||
60 | static uint32_t postEvent_Periodic_(uint32_t interval, void *context) { | ||
61 | iUnused(context); | ||
62 | SDL_UserEvent ev = { .type = SDL_USEREVENT, | ||
63 | .timestamp = SDL_GetTicks(), | ||
64 | .code = periodic_UserEventCode }; | ||
65 | SDL_PushEvent((SDL_Event *) &ev); | ||
66 | return interval; | ||
67 | } | ||
68 | |||
69 | static void startOrStopWakeupTimer_Periodic_(iPeriodic *d, iBool start) { | ||
70 | if (start && !d->wakeupTimer) { | ||
71 | d->wakeupTimer = SDL_AddTimer(postingInterval_Periodic_, postEvent_Periodic_, d); | ||
72 | } | ||
73 | else if (!start && d->wakeupTimer) { | ||
74 | SDL_RemoveTimer(d->wakeupTimer); | ||
75 | d->wakeupTimer = 0; | ||
76 | } | ||
77 | } | ||
78 | |||
60 | static void removePending_Periodic_(iPeriodic *d) { | 79 | static void removePending_Periodic_(iPeriodic *d) { |
61 | iForEach(PtrSet, i, &d->pendingRemoval) { | 80 | iForEach(PtrSet, i, &d->pendingRemoval) { |
62 | size_t pos; | 81 | size_t pos; |
@@ -68,6 +87,9 @@ static void removePending_Periodic_(iPeriodic *d) { | |||
68 | } | 87 | } |
69 | } | 88 | } |
70 | clear_PtrSet(&d->pendingRemoval); | 89 | clear_PtrSet(&d->pendingRemoval); |
90 | if (isEmpty_SortedArray(&d->commands)) { | ||
91 | startOrStopWakeupTimer_Periodic_(d, iFalse); | ||
92 | } | ||
71 | } | 93 | } |
72 | 94 | ||
73 | static iBool isDispatching_; | 95 | static iBool isDispatching_; |
@@ -109,9 +131,11 @@ void init_Periodic(iPeriodic *d) { | |||
109 | init_SortedArray(&d->commands, sizeof(iPeriodicCommand), cmp_PeriodicCommand_); | 131 | init_SortedArray(&d->commands, sizeof(iPeriodicCommand), cmp_PeriodicCommand_); |
110 | d->lastPostTime = 0; | 132 | d->lastPostTime = 0; |
111 | init_PtrSet(&d->pendingRemoval); | 133 | init_PtrSet(&d->pendingRemoval); |
134 | d->wakeupTimer = 0; | ||
112 | } | 135 | } |
113 | 136 | ||
114 | void deinit_Periodic(iPeriodic *d) { | 137 | void deinit_Periodic(iPeriodic *d) { |
138 | startOrStopWakeupTimer_Periodic_(d, iFalse); | ||
115 | deinit_PtrSet(&d->pendingRemoval); | 139 | deinit_PtrSet(&d->pendingRemoval); |
116 | iForEach(Array, i, &d->commands.values) { | 140 | iForEach(Array, i, &d->commands.values) { |
117 | deinit_PeriodicCommand(i.value); | 141 | deinit_PeriodicCommand(i.value); |
@@ -121,6 +145,7 @@ void deinit_Periodic(iPeriodic *d) { | |||
121 | } | 145 | } |
122 | 146 | ||
123 | void add_Periodic(iPeriodic *d, iAny *context, const char *command) { | 147 | void add_Periodic(iPeriodic *d, iAny *context, const char *command) { |
148 | iAssert(isInstance_Object(context, &Class_Widget)); | ||
124 | lock_Mutex(d->mutex); | 149 | lock_Mutex(d->mutex); |
125 | size_t pos; | 150 | size_t pos; |
126 | iPeriodicCommand key = { .context = context }; | 151 | iPeriodicCommand key = { .context = context }; |
@@ -133,6 +158,7 @@ void add_Periodic(iPeriodic *d, iAny *context, const char *command) { | |||
133 | init_PeriodicCommand(&pc, context, command); | 158 | init_PeriodicCommand(&pc, context, command); |
134 | insert_SortedArray(&d->commands, &pc); | 159 | insert_SortedArray(&d->commands, &pc); |
135 | } | 160 | } |
161 | startOrStopWakeupTimer_Periodic_(d, iTrue); | ||
136 | unlock_Mutex(d->mutex); | 162 | unlock_Mutex(d->mutex); |
137 | } | 163 | } |
138 | 164 | ||
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 { | |||
35 | iSortedArray commands; | 35 | iSortedArray commands; |
36 | uint32_t lastPostTime; | 36 | uint32_t lastPostTime; |
37 | iPtrSet pendingRemoval; /* contexts */ | 37 | iPtrSet pendingRemoval; /* contexts */ |
38 | int wakeupTimer; /* running while there are pending periodic commands */ | ||
38 | }; | 39 | }; |
39 | 40 | ||
40 | void init_Periodic (iPeriodic *); | 41 | void init_Periodic (iPeriodic *); |
diff --git a/src/prefs.c b/src/prefs.c index 10df9ade..6164ca25 100644 --- a/src/prefs.c +++ b/src/prefs.c | |||
@@ -23,6 +23,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
23 | #include "prefs.h" | 23 | #include "prefs.h" |
24 | 24 | ||
25 | #include <the_Foundation/fileinfo.h> | 25 | #include <the_Foundation/fileinfo.h> |
26 | #include <assert.h> | ||
27 | |||
28 | static_assert(offsetof(iPrefs, plainTextWrap) == offsetof(iPrefs, bools[plainTextWrap_PrefsBool]), | ||
29 | "memory layout mismatch (needs struct packing?)"); | ||
26 | 30 | ||
27 | void init_Prefs(iPrefs *d) { | 31 | void init_Prefs(iPrefs *d) { |
28 | iForIndices(i, d->strings) { | 32 | iForIndices(i, d->strings) { |
@@ -40,8 +44,20 @@ void init_Prefs(iPrefs *d) { | |||
40 | d->uiAnimations = iTrue; | 44 | d->uiAnimations = iTrue; |
41 | d->uiScale = 1.0f; /* default set elsewhere */ | 45 | d->uiScale = 1.0f; /* default set elsewhere */ |
42 | d->zoomPercent = 100; | 46 | d->zoomPercent = 100; |
47 | d->navbarActions[0] = back_ToolbarAction; | ||
48 | d->navbarActions[1] = forward_ToolbarAction; | ||
49 | d->navbarActions[2] = sidebar_ToolbarAction; | ||
50 | d->navbarActions[3] = home_ToolbarAction; | ||
51 | #if defined (iPlatformAndroidMobile) | ||
52 | /* Android has a system-wide back button so no need to have a duplicate. */ | ||
53 | d->toolbarActions[0] = closeTab_ToolbarAction; | ||
54 | #else | ||
55 | d->toolbarActions[0] = back_ToolbarAction; | ||
56 | #endif | ||
57 | d->toolbarActions[1] = forward_ToolbarAction; | ||
43 | d->sideIcon = iTrue; | 58 | d->sideIcon = iTrue; |
44 | d->hideToolbarOnScroll = iTrue; | 59 | d->hideToolbarOnScroll = iTrue; |
60 | d->blinkingCursor = iTrue; | ||
45 | d->pinSplit = 1; | 61 | d->pinSplit = 1; |
46 | d->time24h = iTrue; | 62 | d->time24h = iTrue; |
47 | d->returnKey = default_ReturnKeyBehavior; | 63 | d->returnKey = default_ReturnKeyBehavior; |
diff --git a/src/prefs.h b/src/prefs.h index 2fbff9de..ea864f51 100644 --- a/src/prefs.h +++ b/src/prefs.h | |||
@@ -33,76 +33,150 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
33 | iDeclareType(Prefs) | 33 | iDeclareType(Prefs) |
34 | 34 | ||
35 | enum iPrefsString { | 35 | enum iPrefsString { |
36 | /* General */ | ||
36 | uiLanguage_PrefsString, | 37 | uiLanguage_PrefsString, |
37 | downloadDir_PrefsString, | 38 | downloadDir_PrefsString, |
38 | searchUrl_PrefsString, | 39 | searchUrl_PrefsString, |
40 | |||
39 | /* Network */ | 41 | /* Network */ |
40 | caFile_PrefsString, | 42 | caFile_PrefsString, |
41 | caPath_PrefsString, | 43 | caPath_PrefsString, |
42 | geminiProxy_PrefsString, | 44 | geminiProxy_PrefsString, |
43 | gopherProxy_PrefsString, | 45 | gopherProxy_PrefsString, |
44 | httpProxy_PrefsString, | 46 | httpProxy_PrefsString, |
47 | |||
45 | /* Style */ | 48 | /* Style */ |
46 | uiFont_PrefsString, | 49 | uiFont_PrefsString, |
47 | headingFont_PrefsString, | 50 | headingFont_PrefsString, |
48 | bodyFont_PrefsString, | 51 | bodyFont_PrefsString, |
49 | monospaceFont_PrefsString, | 52 | monospaceFont_PrefsString, |
50 | monospaceDocumentFont_PrefsString, | 53 | monospaceDocumentFont_PrefsString, |
54 | |||
55 | /* Meta */ | ||
51 | max_PrefsString | 56 | max_PrefsString |
52 | }; | 57 | }; |
53 | 58 | ||
54 | /* TODO: Refactor at least the boolean values into an array for easier manipulation. | 59 | /* Note: These match match the array/struct in Prefs. */ |
55 | Then they can be (de)serialized as a group. Need to use a systematic command naming | 60 | enum iPrefsBool { |
56 | convention for notifications. */ | 61 | /* Window and User Interface */ |
62 | useSystemTheme_PrefsBool, | ||
63 | customFrame_PrefsBool, | ||
64 | retainWindowSize_PrefsBool, | ||
65 | uiAnimations_PrefsBool, | ||
66 | hideToolbarOnScroll_PrefsBool, | ||
67 | |||
68 | blinkingCursor_PrefsBool, | ||
69 | |||
70 | /* Document presentation */ | ||
71 | sideIcon_PrefsBool, | ||
72 | time24h_PrefsBool, | ||
73 | |||
74 | /* Behavior */ | ||
75 | hoverLink_PrefsBool, | ||
76 | smoothScrolling_PrefsBool, | ||
77 | loadImageInsteadOfScrolling_PrefsBool, | ||
78 | collapsePreOnLoad_PrefsBool, | ||
79 | openArchiveIndexPages_PrefsBool, | ||
80 | |||
81 | addBookmarksToBottom_PrefsBool, | ||
82 | warnAboutMissingGlyphs_PrefsBool, | ||
83 | |||
84 | /* Network */ | ||
85 | decodeUserVisibleURLs_PrefsBool, | ||
86 | |||
87 | /* Style */ | ||
88 | monospaceGemini_PrefsBool, | ||
89 | monospaceGopher_PrefsBool, | ||
90 | boldLinkVisited_PrefsBool, | ||
91 | boldLinkDark_PrefsBool, | ||
92 | boldLinkLight_PrefsBool, | ||
93 | |||
94 | fontSmoothing_PrefsBool, | ||
95 | bigFirstParagraph_PrefsBool, | ||
96 | quoteIcon_PrefsBool, | ||
97 | centerShortDocs_PrefsBool, | ||
98 | plainTextWrap_PrefsBool, | ||
99 | |||
100 | /* Meta */ | ||
101 | max_PrefsBool | ||
102 | }; | ||
103 | |||
104 | #define maxNavbarActions_Prefs 4 | ||
105 | |||
106 | /* TODO: Use a systematic command naming convention for notifications. */ | ||
107 | |||
57 | struct Impl_Prefs { | 108 | struct Impl_Prefs { |
58 | iString strings[max_PrefsString]; | 109 | iString strings[max_PrefsString]; |
59 | /* UI state */ | 110 | union { |
111 | iBool bools[max_PrefsBool]; | ||
112 | /* For convenience, contents of the array are accessible also via these members. */ | ||
113 | struct { | ||
114 | /* Window and User Interface */ | ||
115 | iBool useSystemTheme; | ||
116 | iBool customFrame; /* when LAGRANGE_ENABLE_CUSTOM_FRAME is defined */ | ||
117 | iBool retainWindowSize; | ||
118 | iBool uiAnimations; | ||
119 | iBool hideToolbarOnScroll; | ||
120 | |||
121 | iBool blinkingCursor; | ||
122 | |||
123 | /* Document presentation */ | ||
124 | iBool sideIcon; | ||
125 | iBool time24h; | ||
126 | |||
127 | /* Behavior */ | ||
128 | iBool hoverLink; | ||
129 | iBool smoothScrolling; | ||
130 | iBool loadImageInsteadOfScrolling; | ||
131 | iBool collapsePreOnLoad; | ||
132 | iBool openArchiveIndexPages; | ||
133 | |||
134 | iBool addBookmarksToBottom; | ||
135 | iBool warnAboutMissingGlyphs; | ||
136 | |||
137 | /* Network */ | ||
138 | iBool decodeUserVisibleURLs; | ||
139 | |||
140 | /* Style */ | ||
141 | iBool monospaceGemini; | ||
142 | iBool monospaceGopher; | ||
143 | iBool boldLinkVisited; | ||
144 | iBool boldLinkDark; | ||
145 | iBool boldLinkLight; | ||
146 | |||
147 | iBool fontSmoothing; | ||
148 | iBool bigFirstParagraph; | ||
149 | iBool quoteIcon; | ||
150 | iBool centerShortDocs; | ||
151 | iBool plainTextWrap; | ||
152 | }; | ||
153 | }; | ||
154 | /* UI state (belongs to state.lgr...) */ | ||
60 | int dialogTab; | 155 | int dialogTab; |
61 | int langFrom; | 156 | int langFrom; |
62 | int langTo; | 157 | int langTo; |
63 | /* Window */ | 158 | /* Colors */ |
64 | iBool useSystemTheme; | ||
65 | enum iColorTheme systemPreferredColorTheme[2]; /* dark, light */ | 159 | enum iColorTheme systemPreferredColorTheme[2]; /* dark, light */ |
66 | enum iColorTheme theme; | 160 | enum iColorTheme theme; |
67 | enum iColorAccent accent; | 161 | enum iColorAccent accent; |
68 | iBool customFrame; /* when LAGRANGE_ENABLE_CUSTOM_FRAME is defined */ | 162 | /* Window and User Interface */ |
69 | iBool retainWindowSize; | ||
70 | iBool uiAnimations; | ||
71 | float uiScale; | 163 | float uiScale; |
164 | enum iToolbarAction navbarActions[maxNavbarActions_Prefs]; | ||
165 | enum iToolbarAction toolbarActions[2]; | ||
166 | /* Document presentation */ | ||
72 | int zoomPercent; | 167 | int zoomPercent; |
73 | iBool sideIcon; | ||
74 | iBool hideToolbarOnScroll; | ||
75 | int pinSplit; /* 0: no pinning, 1: left doc, 2: right doc */ | ||
76 | iBool time24h; | ||
77 | /* Behavior */ | 168 | /* Behavior */ |
169 | int pinSplit; /* 0: no pinning, 1: left doc, 2: right doc */ | ||
78 | int returnKey; | 170 | int returnKey; |
79 | iBool hoverLink; | ||
80 | iBool smoothScrolling; | ||
81 | int smoothScrollSpeed[max_ScrollType]; | 171 | int smoothScrollSpeed[max_ScrollType]; |
82 | iBool loadImageInsteadOfScrolling; | ||
83 | iBool collapsePreOnLoad; | ||
84 | iBool openArchiveIndexPages; | ||
85 | iBool addBookmarksToBottom; | ||
86 | iBool warnAboutMissingGlyphs; | ||
87 | /* Network */ | 172 | /* Network */ |
88 | iBool decodeUserVisibleURLs; | ||
89 | int maxCacheSize; /* MB */ | 173 | int maxCacheSize; /* MB */ |
90 | int maxMemorySize; /* MB */ | 174 | int maxMemorySize; /* MB */ |
91 | /* Style */ | 175 | /* Style */ |
92 | iStringSet * disabledFontPacks; | 176 | iStringSet * disabledFontPacks; |
93 | iBool fontSmoothing; | ||
94 | int gemtextAnsiEscapes; | 177 | int gemtextAnsiEscapes; |
95 | iBool monospaceGemini; | ||
96 | iBool monospaceGopher; | ||
97 | iBool boldLinkVisited; | ||
98 | iBool boldLinkDark; | ||
99 | iBool boldLinkLight; | ||
100 | int lineWidth; | 178 | int lineWidth; |
101 | float lineSpacing; | 179 | float lineSpacing; |
102 | iBool bigFirstParagraph; | ||
103 | iBool quoteIcon; | ||
104 | iBool centerShortDocs; | ||
105 | iBool plainTextWrap; | ||
106 | enum iImageStyle imageStyle; | 180 | enum iImageStyle imageStyle; |
107 | /* Colors */ | 181 | /* Colors */ |
108 | enum iGmDocumentTheme docThemeDark; | 182 | enum iGmDocumentTheme docThemeDark; |
diff --git a/src/resources.c b/src/resources.c index 03ca7cbb..ae85463a 100644 --- a/src/resources.c +++ b/src/resources.c | |||
@@ -28,7 +28,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
28 | #include <SDL_rwops.h> | 28 | #include <SDL_rwops.h> |
29 | 29 | ||
30 | static iArchive *archive_; | 30 | static iArchive *archive_; |
31 | 31 | ||
32 | iBlock blobAbout_Resources; | 32 | iBlock blobAbout_Resources; |
33 | iBlock blobHelp_Resources; | 33 | iBlock blobHelp_Resources; |
34 | iBlock blobLagrange_Resources; | 34 | iBlock blobLagrange_Resources; |
@@ -66,10 +66,18 @@ static struct { | |||
66 | const char *archivePath; | 66 | const char *archivePath; |
67 | } entries_[] = { | 67 | } entries_[] = { |
68 | { &blobAbout_Resources, "about/about.gmi" }, | 68 | { &blobAbout_Resources, "about/about.gmi" }, |
69 | { &blobHelp_Resources, "about/help.gmi" }, | ||
70 | { &blobLagrange_Resources, "about/lagrange.gmi" }, | 69 | { &blobLagrange_Resources, "about/lagrange.gmi" }, |
71 | { &blobLicense_Resources, "about/license.gmi" }, | 70 | { &blobLicense_Resources, "about/license.gmi" }, |
71 | #if defined (iPlatformAppleMobile) | ||
72 | { &blobHelp_Resources, "about/ios-help.gmi" }, | ||
73 | { &blobVersion_Resources, "about/ios-version.gmi" }, | ||
74 | #elif defined (iPlatformAndroidMobile) | ||
75 | { &blobHelp_Resources, "about/android-help.gmi" }, | ||
76 | { &blobVersion_Resources, "about/android-version.gmi" }, | ||
77 | #else | ||
78 | { &blobHelp_Resources, "about/help.gmi" }, | ||
72 | { &blobVersion_Resources, "about/version.gmi" }, | 79 | { &blobVersion_Resources, "about/version.gmi" }, |
80 | #endif | ||
73 | { &blobArghelp_Resources, "arg-help.txt" }, | 81 | { &blobArghelp_Resources, "arg-help.txt" }, |
74 | { &blobCs_Resources, "lang/cs.bin" }, | 82 | { &blobCs_Resources, "lang/cs.bin" }, |
75 | { &blobDe_Resources, "lang/de.bin" }, | 83 | { &blobDe_Resources, "lang/de.bin" }, |
diff --git a/src/sitespec.c b/src/sitespec.c index 6f4546f0..fe80ad13 100644 --- a/src/sitespec.c +++ b/src/sitespec.c | |||
@@ -25,6 +25,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
25 | #include <the_Foundation/file.h> | 25 | #include <the_Foundation/file.h> |
26 | #include <the_Foundation/path.h> | 26 | #include <the_Foundation/path.h> |
27 | #include <the_Foundation/stringhash.h> | 27 | #include <the_Foundation/stringhash.h> |
28 | #include <the_Foundation/stringarray.h> | ||
28 | #include <the_Foundation/toml.h> | 29 | #include <the_Foundation/toml.h> |
29 | 30 | ||
30 | iDeclareClass(SiteParams) | 31 | iDeclareClass(SiteParams) |
@@ -35,6 +36,7 @@ struct Impl_SiteParams { | |||
35 | uint16_t titanPort; | 36 | uint16_t titanPort; |
36 | iString titanIdentity; /* fingerprint */ | 37 | iString titanIdentity; /* fingerprint */ |
37 | int dismissWarnings; | 38 | int dismissWarnings; |
39 | iStringArray usedIdentities; /* fingerprints; latest ones at the end */ | ||
38 | /* TODO: theme seed, style settings */ | 40 | /* TODO: theme seed, style settings */ |
39 | }; | 41 | }; |
40 | 42 | ||
@@ -42,12 +44,23 @@ void init_SiteParams(iSiteParams *d) { | |||
42 | d->titanPort = 0; /* undefined */ | 44 | d->titanPort = 0; /* undefined */ |
43 | init_String(&d->titanIdentity); | 45 | init_String(&d->titanIdentity); |
44 | d->dismissWarnings = 0; | 46 | d->dismissWarnings = 0; |
47 | init_StringArray(&d->usedIdentities); | ||
45 | } | 48 | } |
46 | 49 | ||
47 | void deinit_SiteParams(iSiteParams *d) { | 50 | void deinit_SiteParams(iSiteParams *d) { |
51 | deinit_StringArray(&d->usedIdentities); | ||
48 | deinit_String(&d->titanIdentity); | 52 | deinit_String(&d->titanIdentity); |
49 | } | 53 | } |
50 | 54 | ||
55 | static size_t findUsedIdentity_SiteParams_(const iSiteParams *d, const iString *fingerprint) { | ||
56 | iConstForEach(StringArray, i, &d->usedIdentities) { | ||
57 | if (equal_String(i.value, fingerprint)) { | ||
58 | return index_StringArrayConstIterator(&i); | ||
59 | } | ||
60 | } | ||
61 | return iInvalidPos; | ||
62 | } | ||
63 | |||
51 | iDefineClass(SiteParams) | 64 | iDefineClass(SiteParams) |
52 | iDefineObjectConstruction(SiteParams) | 65 | iDefineObjectConstruction(SiteParams) |
53 | 66 | ||
@@ -128,7 +141,13 @@ static void handleIniKeyValue_SiteSpec_(void *context, const iString *table, con | |||
128 | set_String(&d->loadParams->titanIdentity, value->value.string); | 141 | set_String(&d->loadParams->titanIdentity, value->value.string); |
129 | } | 142 | } |
130 | else if (!cmp_String(key, "dismissWarnings") && value->type == int64_TomlType) { | 143 | else if (!cmp_String(key, "dismissWarnings") && value->type == int64_TomlType) { |
131 | d->loadParams->dismissWarnings = value->value.int64; | 144 | d->loadParams->dismissWarnings = (int) value->value.int64; |
145 | } | ||
146 | else if (!cmp_String(key, "usedIdentities") && value->type == string_TomlType) { | ||
147 | iRangecc seg = iNullRange; | ||
148 | while (nextSplit_Rangecc(range_String(value->value.string), " ", &seg)) { | ||
149 | pushBack_StringArray(&d->loadParams->usedIdentities, collectNewRange_String(seg)); | ||
150 | } | ||
132 | } | 151 | } |
133 | } | 152 | } |
134 | 153 | ||
@@ -151,6 +170,7 @@ static void save_SiteSpec_(iSiteSpec *d) { | |||
151 | if (open_File(f, writeOnly_FileMode | text_FileMode)) { | 170 | if (open_File(f, writeOnly_FileMode | text_FileMode)) { |
152 | iString *buf = new_String(); | 171 | iString *buf = new_String(); |
153 | iConstForEach(StringHash, i, &d->sites) { | 172 | iConstForEach(StringHash, i, &d->sites) { |
173 | iBeginCollect(); | ||
154 | const iBlock * key = &i.value->keyBlock; | 174 | const iBlock * key = &i.value->keyBlock; |
155 | const iSiteParams *params = i.value->object; | 175 | const iSiteParams *params = i.value->object; |
156 | format_String(buf, "[%s]\n", cstr_Block(key)); | 176 | format_String(buf, "[%s]\n", cstr_Block(key)); |
@@ -164,8 +184,15 @@ static void save_SiteSpec_(iSiteSpec *d) { | |||
164 | if (params->dismissWarnings) { | 184 | if (params->dismissWarnings) { |
165 | appendFormat_String(buf, "dismissWarnings = 0x%x\n", params->dismissWarnings); | 185 | appendFormat_String(buf, "dismissWarnings = 0x%x\n", params->dismissWarnings); |
166 | } | 186 | } |
187 | if (!isEmpty_StringArray(¶ms->usedIdentities)) { | ||
188 | appendFormat_String( | ||
189 | buf, | ||
190 | "usedIdentities = \"%s\"\n", | ||
191 | cstrCollect_String(joinCStr_StringArray(¶ms->usedIdentities, " "))); | ||
192 | } | ||
167 | appendCStr_String(buf, "\n"); | 193 | appendCStr_String(buf, "\n"); |
168 | write_File(f, utf8_String(buf)); | 194 | write_File(f, utf8_String(buf)); |
195 | iEndCollect(); | ||
169 | } | 196 | } |
170 | delete_String(buf); | 197 | delete_String(buf); |
171 | } | 198 | } |
@@ -188,14 +215,19 @@ void deinit_SiteSpec(void) { | |||
188 | deinit_String(&d->saveDir); | 215 | deinit_String(&d->saveDir); |
189 | } | 216 | } |
190 | 217 | ||
191 | void setValue_SiteSpec(const iString *site, enum iSiteSpecKey key, int value) { | 218 | static iSiteParams *findParams_SiteSpec_(iSiteSpec *d, const iString *site) { |
192 | iSiteSpec *d = &siteSpec_; | ||
193 | const iString *hashKey = collect_String(lower_String(site)); | 219 | const iString *hashKey = collect_String(lower_String(site)); |
194 | iSiteParams *params = value_StringHash(&d->sites, hashKey); | 220 | iSiteParams *params = value_StringHash(&d->sites, hashKey); |
195 | if (!params) { | 221 | if (!params) { |
196 | params = new_SiteParams(); | 222 | params = new_SiteParams(); |
197 | insert_StringHash(&d->sites, hashKey, params); | 223 | insert_StringHash(&d->sites, hashKey, params); |
198 | } | 224 | } |
225 | return params; | ||
226 | } | ||
227 | |||
228 | void setValue_SiteSpec(const iString *site, enum iSiteSpecKey key, int value) { | ||
229 | iSiteSpec *d = &siteSpec_; | ||
230 | iSiteParams *params = findParams_SiteSpec_(d, site); | ||
199 | iBool needSave = iFalse; | 231 | iBool needSave = iFalse; |
200 | switch (key) { | 232 | switch (key) { |
201 | case titanPort_SiteSpecKey: | 233 | case titanPort_SiteSpecKey: |
@@ -216,12 +248,7 @@ void setValue_SiteSpec(const iString *site, enum iSiteSpecKey key, int value) { | |||
216 | 248 | ||
217 | void setValueString_SiteSpec(const iString *site, enum iSiteSpecKey key, const iString *value) { | 249 | void setValueString_SiteSpec(const iString *site, enum iSiteSpecKey key, const iString *value) { |
218 | iSiteSpec *d = &siteSpec_; | 250 | iSiteSpec *d = &siteSpec_; |
219 | const iString *hashKey = collect_String(lower_String(site)); | 251 | iSiteParams *params = findParams_SiteSpec_(d, site); |
220 | iSiteParams *params = value_StringHash(&d->sites, hashKey); | ||
221 | if (!params) { | ||
222 | params = new_SiteParams(); | ||
223 | insert_StringHash(&d->sites, hashKey, params); | ||
224 | } | ||
225 | iBool needSave = iFalse; | 252 | iBool needSave = iFalse; |
226 | switch (key) { | 253 | switch (key) { |
227 | case titanIdentity_SiteSpecKey: | 254 | case titanIdentity_SiteSpecKey: |
@@ -238,6 +265,44 @@ void setValueString_SiteSpec(const iString *site, enum iSiteSpecKey key, const i | |||
238 | } | 265 | } |
239 | } | 266 | } |
240 | 267 | ||
268 | static void insertOrRemoveString_SiteSpec_(iSiteSpec *d, const iString *site, enum iSiteSpecKey key, | ||
269 | const iString *value, iBool doInsert) { | ||
270 | iSiteParams *params = findParams_SiteSpec_(d, site); | ||
271 | iBool needSave = iFalse; | ||
272 | switch (key) { | ||
273 | case usedIdentities_SiteSpecKey: { | ||
274 | const size_t index = findUsedIdentity_SiteParams_(params, value); | ||
275 | if (doInsert && index == iInvalidPos) { | ||
276 | pushBack_StringArray(¶ms->usedIdentities, value); | ||
277 | needSave = iTrue; | ||
278 | } | ||
279 | else if (!doInsert && index != iInvalidPos) { | ||
280 | remove_StringArray(¶ms->usedIdentities, index); | ||
281 | needSave = iTrue; | ||
282 | } | ||
283 | break; | ||
284 | } | ||
285 | default: | ||
286 | break; | ||
287 | } | ||
288 | if (needSave) { | ||
289 | save_SiteSpec_(d); | ||
290 | } | ||
291 | } | ||
292 | |||
293 | void insertString_SiteSpec(const iString *site, enum iSiteSpecKey key, const iString *value) { | ||
294 | insertOrRemoveString_SiteSpec_(&siteSpec_, site, key, value, iTrue); | ||
295 | } | ||
296 | |||
297 | void removeString_SiteSpec(const iString *site, enum iSiteSpecKey key, const iString *value) { | ||
298 | insertOrRemoveString_SiteSpec_(&siteSpec_, site, key, value, iFalse); | ||
299 | } | ||
300 | |||
301 | const iStringArray *strings_SiteSpec(const iString *site, enum iSiteSpecKey key) { | ||
302 | const iSiteParams *params = findParams_SiteSpec_(&siteSpec_, site); | ||
303 | return ¶ms->usedIdentities; | ||
304 | } | ||
305 | |||
241 | int value_SiteSpec(const iString *site, enum iSiteSpecKey key) { | 306 | int value_SiteSpec(const iString *site, enum iSiteSpecKey key) { |
242 | iSiteSpec *d = &siteSpec_; | 307 | iSiteSpec *d = &siteSpec_; |
243 | const iSiteParams *params = constValue_StringHash(&d->sites, collect_String(lower_String(site))); | 308 | 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. */ | |||
22 | 22 | ||
23 | #pragma once | 23 | #pragma once |
24 | 24 | ||
25 | #include <the_Foundation/string.h> | 25 | #include <the_Foundation/stringarray.h> |
26 | 26 | ||
27 | iDeclareType(SiteSpec) | 27 | iDeclareType(SiteSpec) |
28 | 28 | ||
29 | enum iSiteSpecKey { | 29 | enum iSiteSpecKey { |
30 | titanPort_SiteSpecKey, | 30 | titanPort_SiteSpecKey, /* int */ |
31 | titanIdentity_SiteSpecKey, | 31 | titanIdentity_SiteSpecKey, /* String */ |
32 | dismissWarnings_SiteSpecKey, | 32 | dismissWarnings_SiteSpecKey, /* int */ |
33 | usedIdentities_SiteSpecKey, /* StringArray */ | ||
33 | }; | 34 | }; |
34 | 35 | ||
35 | void init_SiteSpec (const char *saveDir); | 36 | void init_SiteSpec (const char *saveDir); |
36 | void deinit_SiteSpec (void); | 37 | void deinit_SiteSpec (void); |
37 | 38 | ||
38 | /* changes saved immediately */ | 39 | /* changes saved immediately */ |
39 | void setValue_SiteSpec (const iString *site, enum iSiteSpecKey key, int value); | 40 | void setValue_SiteSpec (const iString *site, enum iSiteSpecKey key, int value); |
40 | void setValueString_SiteSpec (const iString *site, enum iSiteSpecKey key, const iString *value); | 41 | void setValueString_SiteSpec (const iString *site, enum iSiteSpecKey key, const iString *value); |
42 | void insertString_SiteSpec (const iString *site, enum iSiteSpecKey key, const iString *value); | ||
43 | void removeString_SiteSpec (const iString *site, enum iSiteSpecKey key, const iString *value); | ||
41 | 44 | ||
42 | int value_SiteSpec (const iString *site, enum iSiteSpecKey key); | 45 | int value_SiteSpec (const iString *site, enum iSiteSpecKey key); |
43 | const iString * valueString_SiteSpec (const iString *site, enum iSiteSpecKey key); | 46 | const iString * valueString_SiteSpec (const iString *site, enum iSiteSpecKey key); |
47 | const iStringArray *strings_SiteSpec (const iString *site, enum iSiteSpecKey key); | ||
diff --git a/src/ui/banner.c b/src/ui/banner.c index 7ec189a4..11ae1574 100644 --- a/src/ui/banner.c +++ b/src/ui/banner.c | |||
@@ -76,7 +76,6 @@ static void updateHeight_Banner_(iBanner *d) { | |||
76 | } | 76 | } |
77 | const size_t numItems = size_Array(&d->items); | 77 | const size_t numItems = size_Array(&d->items); |
78 | if (numItems) { | 78 | if (numItems) { |
79 | const int innerPad = gap_UI; | ||
80 | iConstForEach(Array, i, &d->items) { | 79 | iConstForEach(Array, i, &d->items) { |
81 | const iBannerItem *item = i.value; | 80 | const iBannerItem *item = i.value; |
82 | d->rect.size.y += item->height; | 81 | d->rect.size.y += item->height; |
@@ -161,6 +160,13 @@ void setSite_Banner(iBanner *d, iRangecc site, iChar icon) { | |||
161 | 160 | ||
162 | void add_Banner(iBanner *d, enum iBannerType type, enum iGmStatusCode code, | 161 | void add_Banner(iBanner *d, enum iBannerType type, enum iGmStatusCode code, |
163 | const iString *message, const iString *details) { | 162 | const iString *message, const iString *details) { |
163 | /* If there already is a matching item, don't add a second one. */ | ||
164 | iConstForEach(Array, i, &d->items) { | ||
165 | const iBannerItem *item = i.value; | ||
166 | if (item->type == type && item->code == code) { | ||
167 | return; | ||
168 | } | ||
169 | } | ||
164 | iBannerItem item; | 170 | iBannerItem item; |
165 | init_BannerItem(&item); | 171 | init_BannerItem(&item); |
166 | item.type = type; | 172 | item.type = type; |
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) { | |||
143 | item->isWaitingForEvent = iTrue; | 143 | item->isWaitingForEvent = iTrue; |
144 | invalidateItem_ListWidget(d->list, d->activePos); | 144 | invalidateItem_ListWidget(d->list, d->activePos); |
145 | } | 145 | } |
146 | #if defined (iPlatformAppleDesktop) | 146 | #if defined (iPlatformAppleDesktop) && defined (iHaveNativeContextMenus) |
147 | /* Native menus must be disabled while grabbing keys so the shortcuts don't trigger. */ | 147 | /* Native menus must be disabled while grabbing keys so the shortcuts don't trigger. */ |
148 | const iBool enableNativeMenus = (d->activePos == iInvalidPos); | 148 | const iBool enableNativeMenus = (d->activePos == iInvalidPos); |
149 | enableMenu_MacOS("${menu.title.file}", enableNativeMenus); | ||
149 | enableMenu_MacOS("${menu.title.edit}", enableNativeMenus); | 150 | enableMenu_MacOS("${menu.title.edit}", enableNativeMenus); |
150 | enableMenu_MacOS("${menu.title.view}", enableNativeMenus); | 151 | enableMenu_MacOS("${menu.title.view}", enableNativeMenus); |
152 | enableMenu_MacOS("${menu.title.bookmarks}", enableNativeMenus); | ||
151 | enableMenu_MacOS("${menu.title.identity}", enableNativeMenus); | 153 | enableMenu_MacOS("${menu.title.identity}", enableNativeMenus); |
154 | enableMenuIndex_MacOS(6, enableNativeMenus); | ||
155 | enableMenuIndex_MacOS(7, enableNativeMenus); | ||
152 | #endif | 156 | #endif |
153 | } | 157 | } |
154 | 158 | ||
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) { | |||
145 | else { | 145 | else { |
146 | /* This should behave similar to sheets. */ | 146 | /* This should behave similar to sheets. */ |
147 | useSheetStyle_Widget(w); | 147 | useSheetStyle_Widget(w); |
148 | addChildFlags_Widget( | 148 | addDialogTitle_Widget(w, "${heading.certimport}", NULL); |
149 | w, | ||
150 | iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.certimport}", NULL)), | ||
151 | frameless_WidgetFlag); | ||
152 | d->info = addChildFlags_Widget(w, iClob(new_LabelWidget(infoText_, NULL)), frameless_WidgetFlag); | 149 | d->info = addChildFlags_Widget(w, iClob(new_LabelWidget(infoText_, NULL)), frameless_WidgetFlag); |
153 | addChild_Widget(w, iClob(makePadding_Widget(gap_UI))); | 150 | addChild_Widget(w, iClob(makePadding_Widget(gap_UI))); |
154 | d->crtLabel = new_LabelWidget("", NULL); { | 151 | d->crtLabel = new_LabelWidget("", NULL); { |
diff --git a/src/ui/certlistwidget.c b/src/ui/certlistwidget.c new file mode 100644 index 00000000..2a7562d8 --- /dev/null +++ b/src/ui/certlistwidget.c | |||
@@ -0,0 +1,490 @@ | |||
1 | /* Copyright 2021 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
23 | #include "certlistwidget.h" | ||
24 | |||
25 | #include "documentwidget.h" | ||
26 | #include "command.h" | ||
27 | #include "labelwidget.h" | ||
28 | #include "listwidget.h" | ||
29 | #include "../gmcerts.h" | ||
30 | #include "../app.h" | ||
31 | |||
32 | #include <SDL_clipboard.h> | ||
33 | |||
34 | iDeclareType(CertItem) | ||
35 | typedef iListItemClass iCertItemClass; | ||
36 | |||
37 | struct Impl_CertItem { | ||
38 | iListItem listItem; | ||
39 | uint32_t id; | ||
40 | int indent; | ||
41 | iChar icon; | ||
42 | iBool isBold; | ||
43 | iString label; | ||
44 | iString meta; | ||
45 | // iString url; | ||
46 | }; | ||
47 | |||
48 | void init_CertItem(iCertItem *d) { | ||
49 | init_ListItem(&d->listItem); | ||
50 | d->id = 0; | ||
51 | d->indent = 0; | ||
52 | d->icon = 0; | ||
53 | d->isBold = iFalse; | ||
54 | init_String(&d->label); | ||
55 | init_String(&d->meta); | ||
56 | // init_String(&d->url); | ||
57 | } | ||
58 | |||
59 | void deinit_CertItem(iCertItem *d) { | ||
60 | // deinit_String(&d->url); | ||
61 | deinit_String(&d->meta); | ||
62 | deinit_String(&d->label); | ||
63 | } | ||
64 | |||
65 | static void draw_CertItem_(const iCertItem *d, iPaint *p, iRect itemRect, const iListWidget *list); | ||
66 | |||
67 | iBeginDefineSubclass(CertItem, ListItem) | ||
68 | .draw = (iAny *) draw_CertItem_, | ||
69 | iEndDefineSubclass(CertItem) | ||
70 | |||
71 | iDefineObjectConstruction(CertItem) | ||
72 | |||
73 | /*----------------------------------------------------------------------------------------------*/ | ||
74 | |||
75 | struct Impl_CertListWidget { | ||
76 | iListWidget list; | ||
77 | int itemFonts[2]; | ||
78 | iWidget *menu; /* context menu for an item */ | ||
79 | iCertItem *contextItem; /* list item accessed in the context menu */ | ||
80 | size_t contextIndex; /* index of list item accessed in the context menu */ | ||
81 | }; | ||
82 | |||
83 | iDefineObjectConstruction(CertListWidget) | ||
84 | |||
85 | static iGmIdentity *menuIdentity_CertListWidget_(const iCertListWidget *d) { | ||
86 | if (d->contextItem) { | ||
87 | return identity_GmCerts(certs_App(), d->contextItem->id); | ||
88 | } | ||
89 | return NULL; | ||
90 | } | ||
91 | |||
92 | static void updateContextMenu_CertListWidget_(iCertListWidget *d) { | ||
93 | iArray *items = collectNew_Array(sizeof(iMenuItem)); | ||
94 | const iString *docUrl = url_DocumentWidget(document_App()); | ||
95 | size_t firstIndex = 0; | ||
96 | if (deviceType_App() != desktop_AppDeviceType && !isEmpty_String(docUrl)) { | ||
97 | pushBack_Array(items, &(iMenuItem){ format_CStr("```%s", cstr_String(docUrl)) }); | ||
98 | firstIndex = 1; | ||
99 | } | ||
100 | const iMenuItem ctxItems[] = { | ||
101 | { person_Icon " ${ident.use}", 0, 0, "ident.use arg:1" }, | ||
102 | { close_Icon " ${ident.stopuse}", 0, 0, "ident.use arg:0" }, | ||
103 | { close_Icon " ${ident.stopuse.all}", 0, 0, "ident.use arg:0 clear:1" }, | ||
104 | { "---", 0, 0, NULL }, | ||
105 | { edit_Icon " ${menu.edit.notes}", 0, 0, "ident.edit" }, | ||
106 | { "${ident.fingerprint}", 0, 0, "ident.fingerprint" }, | ||
107 | #if defined (iPlatformAppleDesktop) | ||
108 | { magnifyingGlass_Icon " ${menu.reveal.macos}", 0, 0, "ident.reveal" }, | ||
109 | #endif | ||
110 | #if defined (iPlatformLinux) | ||
111 | { magnifyingGlass_Icon " ${menu.reveal.filemgr}", 0, 0, "ident.reveal" }, | ||
112 | #endif | ||
113 | { export_Icon " ${ident.export}", 0, 0, "ident.export" }, | ||
114 | { "---", 0, 0, NULL }, | ||
115 | { delete_Icon " " uiTextCaution_ColorEscape "${ident.delete}", 0, 0, "ident.delete confirm:1" }, | ||
116 | }; | ||
117 | pushBackN_Array(items, ctxItems, iElemCount(ctxItems)); | ||
118 | /* Used URLs. */ | ||
119 | const iGmIdentity *ident = menuIdentity_CertListWidget_(d); | ||
120 | if (ident) { | ||
121 | size_t insertPos = firstIndex + 3; | ||
122 | if (!isEmpty_StringSet(ident->useUrls)) { | ||
123 | insert_Array(items, insertPos++, &(iMenuItem){ "---", 0, 0, NULL }); | ||
124 | } | ||
125 | iBool usedOnCurrentPage = iFalse; | ||
126 | iConstForEach(StringSet, i, ident->useUrls) { | ||
127 | const iString *url = i.value; | ||
128 | usedOnCurrentPage |= startsWithCase_String(docUrl, cstr_String(url)); | ||
129 | iRangecc urlStr = range_String(url); | ||
130 | if (startsWith_Rangecc(urlStr, "gemini://")) { | ||
131 | urlStr.start += 9; /* omit the default scheme */ | ||
132 | } | ||
133 | insert_Array(items, | ||
134 | insertPos++, | ||
135 | &(iMenuItem){ format_CStr(globe_Icon " %s", cstr_Rangecc(urlStr)), | ||
136 | 0, | ||
137 | 0, | ||
138 | format_CStr("!open url:%s", cstr_String(url)) }); | ||
139 | } | ||
140 | if (!usedOnCurrentPage) { | ||
141 | remove_Array(items, firstIndex + 1); | ||
142 | } | ||
143 | else { | ||
144 | remove_Array(items, firstIndex); | ||
145 | } | ||
146 | } | ||
147 | destroy_Widget(d->menu); | ||
148 | d->menu = makeMenu_Widget(as_Widget(d), data_Array(items), size_Array(items)); | ||
149 | } | ||
150 | |||
151 | static void itemClicked_CertListWidget_(iCertListWidget *d, iCertItem *item, size_t itemIndex) { | ||
152 | iWidget *w = as_Widget(d); | ||
153 | setFocus_Widget(NULL); | ||
154 | d->contextItem = item; | ||
155 | if (d->contextIndex != iInvalidPos) { | ||
156 | invalidateItem_ListWidget(&d->list, d->contextIndex); | ||
157 | } | ||
158 | d->contextIndex = itemIndex; | ||
159 | if (itemIndex < numItems_ListWidget(&d->list)) { | ||
160 | updateContextMenu_CertListWidget_(d); | ||
161 | arrange_Widget(d->menu); | ||
162 | openMenu_Widget(d->menu, | ||
163 | bounds_Widget(w).pos.x < mid_Rect(rect_Root(w->root)).x | ||
164 | ? topRight_Rect(itemRect_ListWidget(&d->list, itemIndex)) | ||
165 | : addX_I2(topLeft_Rect(itemRect_ListWidget(&d->list, itemIndex)), | ||
166 | -width_Widget(d->menu))); | ||
167 | } | ||
168 | } | ||
169 | |||
170 | static iBool processEvent_CertListWidget_(iCertListWidget *d, const SDL_Event *ev) { | ||
171 | iWidget *w = as_Widget(d); | ||
172 | /* Handle commands. */ | ||
173 | if (ev->type == SDL_USEREVENT && ev->user.code == command_UserEventCode) { | ||
174 | const char *cmd = command_UserEvent(ev); | ||
175 | if (equal_Command(cmd, "idents.changed")) { | ||
176 | updateItems_CertListWidget(d); | ||
177 | } | ||
178 | else if (isCommand_Widget(w, ev, "list.clicked")) { | ||
179 | itemClicked_CertListWidget_( | ||
180 | d, pointerLabel_Command(cmd, "item"), argU32Label_Command(cmd, "arg")); | ||
181 | return iTrue; | ||
182 | } | ||
183 | else if (isCommand_Widget(w, ev, "ident.use")) { | ||
184 | iGmIdentity *ident = menuIdentity_CertListWidget_(d); | ||
185 | const iString *tabUrl = urlQueryStripped_String(url_DocumentWidget(document_App())); | ||
186 | if (ident) { | ||
187 | if (argLabel_Command(cmd, "clear")) { | ||
188 | clearUse_GmIdentity(ident); | ||
189 | } | ||
190 | else if (arg_Command(cmd)) { | ||
191 | signIn_GmCerts(certs_App(), ident, tabUrl); | ||
192 | postCommand_App("navigate.reload"); | ||
193 | } | ||
194 | else { | ||
195 | signOut_GmCerts(certs_App(), tabUrl); | ||
196 | postCommand_App("navigate.reload"); | ||
197 | } | ||
198 | saveIdentities_GmCerts(certs_App()); | ||
199 | updateItems_CertListWidget(d); | ||
200 | } | ||
201 | return iTrue; | ||
202 | } | ||
203 | else if (isCommand_Widget(w, ev, "ident.edit")) { | ||
204 | const iGmIdentity *ident = menuIdentity_CertListWidget_(d); | ||
205 | if (ident) { | ||
206 | makeValueInput_Widget(get_Root()->widget, | ||
207 | &ident->notes, | ||
208 | uiHeading_ColorEscape "${heading.ident.notes}", | ||
209 | format_CStr(cstr_Lang("dlg.ident.notes"), cstr_String(name_GmIdentity(ident))), | ||
210 | uiTextAction_ColorEscape "${dlg.default}", | ||
211 | format_CStr("!ident.setnotes ident:%p ptr:%p", ident, d)); | ||
212 | } | ||
213 | return iTrue; | ||
214 | } | ||
215 | else if (isCommand_Widget(w, ev, "ident.fingerprint")) { | ||
216 | const iGmIdentity *ident = menuIdentity_CertListWidget_(d); | ||
217 | if (ident) { | ||
218 | const iString *fps = collect_String( | ||
219 | hexEncode_Block(collect_Block(fingerprint_TlsCertificate(ident->cert)))); | ||
220 | SDL_SetClipboardText(cstr_String(fps)); | ||
221 | } | ||
222 | return iTrue; | ||
223 | } | ||
224 | else if (isCommand_Widget(w, ev, "ident.export")) { | ||
225 | const iGmIdentity *ident = menuIdentity_CertListWidget_(d); | ||
226 | if (ident) { | ||
227 | iString *pem = collect_String(pem_TlsCertificate(ident->cert)); | ||
228 | append_String(pem, collect_String(privateKeyPem_TlsCertificate(ident->cert))); | ||
229 | iDocumentWidget *expTab = newTab_App(NULL, iTrue); | ||
230 | setUrlAndSource_DocumentWidget( | ||
231 | expTab, | ||
232 | collectNewFormat_String("file:%s.pem", cstr_String(name_GmIdentity(ident))), | ||
233 | collectNewCStr_String("text/plain"), | ||
234 | utf8_String(pem)); | ||
235 | } | ||
236 | return iTrue; | ||
237 | } | ||
238 | else if (isCommand_Widget(w, ev, "ident.setnotes")) { | ||
239 | iGmIdentity *ident = pointerLabel_Command(cmd, "ident"); | ||
240 | if (ident) { | ||
241 | setCStr_String(&ident->notes, suffixPtr_Command(cmd, "value")); | ||
242 | updateItems_CertListWidget(d); | ||
243 | } | ||
244 | return iTrue; | ||
245 | } | ||
246 | else if (isCommand_Widget(w, ev, "ident.pickicon")) { | ||
247 | return iTrue; | ||
248 | } | ||
249 | else if (isCommand_Widget(w, ev, "ident.reveal")) { | ||
250 | const iGmIdentity *ident = menuIdentity_CertListWidget_(d); | ||
251 | if (ident) { | ||
252 | const iString *crtPath = certificatePath_GmCerts(certs_App(), ident); | ||
253 | if (crtPath) { | ||
254 | postCommandf_App("reveal path:%s", cstr_String(crtPath)); | ||
255 | } | ||
256 | } | ||
257 | return iTrue; | ||
258 | } | ||
259 | else if (isCommand_Widget(w, ev, "ident.delete")) { | ||
260 | iCertItem *item = d->contextItem; | ||
261 | if (argLabel_Command(cmd, "confirm")) { | ||
262 | makeQuestion_Widget( | ||
263 | uiTextCaution_ColorEscape "${heading.ident.delete}", | ||
264 | format_CStr(cstr_Lang("dlg.confirm.ident.delete"), | ||
265 | uiTextAction_ColorEscape, | ||
266 | cstr_String(&item->label), | ||
267 | uiText_ColorEscape), | ||
268 | (iMenuItem[]){ { "${cancel}", 0, 0, NULL }, | ||
269 | { uiTextCaution_ColorEscape "${dlg.ident.delete}", | ||
270 | 0, | ||
271 | 0, | ||
272 | format_CStr("!ident.delete confirm:0 ptr:%p", d) } }, | ||
273 | 2); | ||
274 | return iTrue; | ||
275 | } | ||
276 | deleteIdentity_GmCerts(certs_App(), menuIdentity_CertListWidget_(d)); | ||
277 | postCommand_App("idents.changed"); | ||
278 | return iTrue; | ||
279 | } | ||
280 | } | ||
281 | if (ev->type == SDL_MOUSEMOTION && !isVisible_Widget(d->menu)) { | ||
282 | const iInt2 mouse = init_I2(ev->motion.x, ev->motion.y); | ||
283 | /* Update cursor. */ | ||
284 | if (contains_Widget(w, mouse)) { | ||
285 | setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW); | ||
286 | } | ||
287 | else if (d->contextIndex != iInvalidPos) { | ||
288 | invalidateItem_ListWidget(&d->list, d->contextIndex); | ||
289 | d->contextIndex = iInvalidPos; | ||
290 | } | ||
291 | } | ||
292 | /* Update context menu items. */ | ||
293 | if (ev->type == SDL_MOUSEBUTTONDOWN && ev->button.button == SDL_BUTTON_RIGHT) { | ||
294 | d->contextItem = NULL; | ||
295 | if (!isVisible_Widget(d->menu)) { | ||
296 | updateMouseHover_ListWidget(&d->list); | ||
297 | } | ||
298 | if (constHoverItem_ListWidget(&d->list) || isVisible_Widget(d->menu)) { | ||
299 | d->contextItem = hoverItem_ListWidget(&d->list); | ||
300 | /* Context is drawn in hover state. */ | ||
301 | if (d->contextIndex != iInvalidPos) { | ||
302 | invalidateItem_ListWidget(&d->list, d->contextIndex); | ||
303 | } | ||
304 | d->contextIndex = hoverItemIndex_ListWidget(&d->list); | ||
305 | updateContextMenu_CertListWidget_(d); | ||
306 | /* TODO: Some callback-based mechanism would be nice for updating menus right | ||
307 | before they open? At least move these to `updateContextMenu_ */ | ||
308 | const iGmIdentity *ident = constHoverIdentity_CertListWidget(d); | ||
309 | const iString * docUrl = url_DocumentWidget(document_App()); | ||
310 | iForEach(ObjectList, i, children_Widget(d->menu)) { | ||
311 | if (isInstance_Object(i.object, &Class_LabelWidget)) { | ||
312 | iLabelWidget *menuItem = i.object; | ||
313 | const char * cmdItem = cstr_String(command_LabelWidget(menuItem)); | ||
314 | if (equal_Command(cmdItem, "ident.use")) { | ||
315 | const iBool cmdUse = arg_Command(cmdItem) != 0; | ||
316 | const iBool cmdClear = argLabel_Command(cmdItem, "clear") != 0; | ||
317 | setFlags_Widget( | ||
318 | as_Widget(menuItem), | ||
319 | disabled_WidgetFlag, | ||
320 | (cmdClear && !isUsed_GmIdentity(ident)) || | ||
321 | (!cmdClear && cmdUse && isUsedOn_GmIdentity(ident, docUrl)) || | ||
322 | (!cmdClear && !cmdUse && !isUsedOn_GmIdentity(ident, docUrl))); | ||
323 | } | ||
324 | } | ||
325 | } | ||
326 | } | ||
327 | if (hoverItem_ListWidget(&d->list) || isVisible_Widget(d->menu)) { | ||
328 | processContextMenuEvent_Widget(d->menu, ev, {}); | ||
329 | } | ||
330 | } | ||
331 | return ((iWidgetClass *) class_Widget(w)->super)->processEvent(w, ev); | ||
332 | } | ||
333 | |||
334 | static void draw_CertListWidget_(const iCertListWidget *d) { | ||
335 | const iWidget *w = constAs_Widget(d); | ||
336 | ((iWidgetClass *) class_Widget(w)->super)->draw(w); | ||
337 | } | ||
338 | |||
339 | static void draw_CertItem_(const iCertItem *d, iPaint *p, iRect itemRect, | ||
340 | const iListWidget *list) { | ||
341 | const iCertListWidget *certList = (const iCertListWidget *) list; | ||
342 | const iBool isMenuVisible = isVisible_Widget(certList->menu); | ||
343 | const iBool isDragging = constDragItem_ListWidget(list) == d; | ||
344 | const iBool isPressing = isMouseDown_ListWidget(list) && !isDragging; | ||
345 | const iBool isHover = | ||
346 | (!isMenuVisible && | ||
347 | isHover_Widget(constAs_Widget(list)) && | ||
348 | constHoverItem_ListWidget(list) == d) || | ||
349 | (isMenuVisible && certList->contextItem == d) || | ||
350 | isDragging; | ||
351 | const int itemHeight = height_Rect(itemRect); | ||
352 | const int iconColor = isHover ? (isPressing ? uiTextPressed_ColorId : uiIconHover_ColorId) | ||
353 | : uiIcon_ColorId; | ||
354 | const int altIconColor = isPressing ? uiTextPressed_ColorId : uiTextCaution_ColorId; | ||
355 | const int font = certList->itemFonts[d->isBold ? 1 : 0]; | ||
356 | int bg = uiBackgroundSidebar_ColorId; | ||
357 | if (isHover) { | ||
358 | bg = isPressing ? uiBackgroundPressed_ColorId | ||
359 | : uiBackgroundFramelessHover_ColorId; | ||
360 | fillRect_Paint(p, itemRect, bg); | ||
361 | } | ||
362 | else if (d->listItem.isSelected) { | ||
363 | bg = uiBackgroundUnfocusedSelection_ColorId; | ||
364 | fillRect_Paint(p, itemRect, bg); | ||
365 | } | ||
366 | // iInt2 pos = itemRect.pos; | ||
367 | const int fg = isHover ? (isPressing ? uiTextPressed_ColorId : uiTextFramelessHover_ColorId) | ||
368 | : uiTextStrong_ColorId; | ||
369 | const iBool isUsedOnDomain = (d->indent != 0); | ||
370 | iString icon; | ||
371 | initUnicodeN_String(&icon, &d->icon, 1); | ||
372 | iInt2 cPos = topLeft_Rect(itemRect); | ||
373 | const int indent = 1.4f * lineHeight_Text(font); | ||
374 | addv_I2(&cPos, | ||
375 | init_I2(3 * gap_UI, | ||
376 | (itemHeight - lineHeight_Text(uiLabel_FontId) * 2 - lineHeight_Text(font)) / | ||
377 | 2)); | ||
378 | const int metaFg = isHover ? permanent_ColorId | (isPressing ? uiTextPressed_ColorId | ||
379 | : uiTextFramelessHover_ColorId) | ||
380 | : uiTextDim_ColorId; | ||
381 | if (!d->listItem.isSelected && !isUsedOnDomain) { | ||
382 | drawOutline_Text(font, cPos, metaFg, none_ColorId, range_String(&icon)); | ||
383 | } | ||
384 | drawRange_Text(font, | ||
385 | cPos, | ||
386 | d->listItem.isSelected ? iconColor | ||
387 | : isUsedOnDomain ? altIconColor | ||
388 | : uiBackgroundSidebar_ColorId, | ||
389 | range_String(&icon)); | ||
390 | deinit_String(&icon); | ||
391 | drawRange_Text(d->listItem.isSelected ? certList->itemFonts[1] : font, | ||
392 | add_I2(cPos, init_I2(indent, 0)), | ||
393 | fg, | ||
394 | range_String(&d->label)); | ||
395 | drawRange_Text(uiLabel_FontId, | ||
396 | add_I2(cPos, init_I2(indent, lineHeight_Text(font))), | ||
397 | metaFg, | ||
398 | range_String(&d->meta)); | ||
399 | } | ||
400 | |||
401 | void init_CertListWidget(iCertListWidget *d) { | ||
402 | iWidget *w = as_Widget(d); | ||
403 | init_ListWidget(&d->list); | ||
404 | setId_Widget(w, "certlist"); | ||
405 | setBackgroundColor_Widget(w, none_ColorId); | ||
406 | d->itemFonts[0] = uiContent_FontId; | ||
407 | d->itemFonts[1] = uiContentBold_FontId; | ||
408 | #if defined (iPlatformMobile) | ||
409 | if (deviceType_App() == phone_AppDeviceType) { | ||
410 | d->itemFonts[0] = uiLabelBig_FontId; | ||
411 | d->itemFonts[1] = uiLabelBigBold_FontId; | ||
412 | } | ||
413 | #endif | ||
414 | updateItemHeight_CertListWidget(d); | ||
415 | d->menu = NULL; | ||
416 | d->contextItem = NULL; | ||
417 | d->contextIndex = iInvalidPos; | ||
418 | } | ||
419 | |||
420 | void updateItemHeight_CertListWidget(iCertListWidget *d) { | ||
421 | setItemHeight_ListWidget(&d->list, 3.5f * lineHeight_Text(d->itemFonts[0])); | ||
422 | } | ||
423 | |||
424 | iBool updateItems_CertListWidget(iCertListWidget *d) { | ||
425 | clear_ListWidget(&d->list); | ||
426 | destroy_Widget(d->menu); | ||
427 | d->menu = NULL; | ||
428 | const iString *tabUrl = url_DocumentWidget(document_App()); | ||
429 | const iRangecc tabHost = urlHost_String(tabUrl); | ||
430 | iBool haveItems = iFalse; | ||
431 | iConstForEach(PtrArray, i, identities_GmCerts(certs_App())) { | ||
432 | const iGmIdentity *ident = i.ptr; | ||
433 | iCertItem *item = new_CertItem(); | ||
434 | item->id = (uint32_t) index_PtrArrayConstIterator(&i); | ||
435 | item->icon = 0x1f464; /* person */ | ||
436 | set_String(&item->label, name_GmIdentity(ident)); | ||
437 | iDate until; | ||
438 | validUntil_TlsCertificate(ident->cert, &until); | ||
439 | const iBool isActive = isUsedOn_GmIdentity(ident, tabUrl); | ||
440 | format_String(&item->meta, | ||
441 | "%s", | ||
442 | isActive ? cstr_Lang("ident.using") | ||
443 | : isUsed_GmIdentity(ident) | ||
444 | ? formatCStrs_Lang("ident.usedonurls.n", size_StringSet(ident->useUrls)) | ||
445 | : cstr_Lang("ident.notused")); | ||
446 | const char *expiry = | ||
447 | ident->flags & temporary_GmIdentityFlag | ||
448 | ? cstr_Lang("ident.temporary") | ||
449 | : cstrCollect_String(format_Date(&until, cstr_Lang("ident.expiry"))); | ||
450 | if (isEmpty_String(&ident->notes)) { | ||
451 | appendFormat_String(&item->meta, "\n%s", expiry); | ||
452 | } | ||
453 | else { | ||
454 | appendFormat_String(&item->meta, | ||
455 | " \u2014 %s\n%s%s", | ||
456 | expiry, | ||
457 | escape_Color(uiHeading_ColorId), | ||
458 | cstr_String(&ident->notes)); | ||
459 | } | ||
460 | item->listItem.isSelected = isActive; | ||
461 | if (!isActive && isUsedOnDomain_GmIdentity(ident, tabHost)) { | ||
462 | item->indent = 1; /* will be highlighted */ | ||
463 | } | ||
464 | addItem_ListWidget(&d->list, item); | ||
465 | haveItems = iTrue; | ||
466 | iRelease(item); | ||
467 | } | ||
468 | return haveItems; | ||
469 | } | ||
470 | |||
471 | void deinit_CertListWidget(iCertListWidget *d) { | ||
472 | iUnused(d); | ||
473 | } | ||
474 | |||
475 | const iGmIdentity *constHoverIdentity_CertListWidget(const iCertListWidget *d) { | ||
476 | const iCertItem *hoverItem = constHoverItem_ListWidget(&d->list); | ||
477 | if (hoverItem) { | ||
478 | return identity_GmCerts(certs_App(), hoverItem->id); | ||
479 | } | ||
480 | return NULL; | ||
481 | } | ||
482 | |||
483 | iGmIdentity *hoverIdentity_CertListWidget(const iCertListWidget *d) { | ||
484 | return iConstCast(iGmIdentity *, constHoverIdentity_CertListWidget(d)); | ||
485 | } | ||
486 | |||
487 | iBeginDefineSubclass(CertListWidget, ListWidget) | ||
488 | .processEvent = (iAny *) processEvent_CertListWidget_, | ||
489 | .draw = (iAny *) draw_CertListWidget_, | ||
490 | 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 @@ | |||
1 | /* Copyright 2021 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
23 | #pragma once | ||
24 | |||
25 | #include "listwidget.h" | ||
26 | |||
27 | iDeclareType(CertListWidget) | ||
28 | |||
29 | typedef iListWidgetClass iCertListWidgetClass; | ||
30 | extern iCertListWidgetClass Class_CertListWidget; | ||
31 | |||
32 | iDeclareObjectConstruction(CertListWidget) | ||
33 | |||
34 | iDeclareType(GmIdentity) | ||
35 | |||
36 | const iGmIdentity * constHoverIdentity_CertListWidget(const iCertListWidget *); | ||
37 | iGmIdentity * hoverIdentity_CertListWidget (const iCertListWidget *); | ||
38 | |||
39 | iBool updateItems_CertListWidget (iCertListWidget *); /* returns False is empty */ | ||
40 | void updateItemHeight_CertListWidget (iCertListWidget *); | ||
diff --git a/src/ui/color.c b/src/ui/color.c index 3ea98d8c..3c2f0339 100644 --- a/src/ui/color.c +++ b/src/ui/color.c | |||
@@ -90,8 +90,8 @@ void setThemePalette_Color(enum iColorTheme theme) { | |||
90 | const int accentLo = (prefs->accent == cyan_ColorAccent ? teal_ColorId : brown_ColorId); | 90 | const int accentLo = (prefs->accent == cyan_ColorAccent ? teal_ColorId : brown_ColorId); |
91 | const int altAccentHi = (prefs->accent == cyan_ColorAccent ? orange_ColorId : cyan_ColorId); | 91 | const int altAccentHi = (prefs->accent == cyan_ColorAccent ? orange_ColorId : cyan_ColorId); |
92 | const int altAccentLo = (prefs->accent == cyan_ColorAccent ? brown_ColorId : teal_ColorId); | 92 | const int altAccentLo = (prefs->accent == cyan_ColorAccent ? brown_ColorId : teal_ColorId); |
93 | const iColor accentMid = mix_Color(get_Color(accentHi), get_Color(accentLo), 0.5f); | 93 | //const iColor accentMid = mix_Color(get_Color(accentHi), get_Color(accentLo), 0.5f); |
94 | const iColor altAccentMid = mix_Color(get_Color(altAccentHi), get_Color(altAccentLo), 0.5f); | 94 | //const iColor altAccentMid = mix_Color(get_Color(altAccentHi), get_Color(altAccentLo), 0.5f); |
95 | switch (theme) { | 95 | switch (theme) { |
96 | case pureBlack_ColorTheme: { | 96 | case pureBlack_ColorTheme: { |
97 | copy_(uiBackground_ColorId, black_ColorId); | 97 | copy_(uiBackground_ColorId, black_ColorId); |
@@ -832,7 +832,7 @@ void ansiColors_Color(iRangecc escapeSequence, int fgDefault, int bgDefault, | |||
832 | int rgb[3] = { 0, 0, 0 }; | 832 | int rgb[3] = { 0, 0, 0 }; |
833 | iForIndices(i, rgb) { | 833 | iForIndices(i, rgb) { |
834 | if (ch >= escapeSequence.end) break; | 834 | if (ch >= escapeSequence.end) break; |
835 | rgb[i] = strtoul(ch + 1, &endPtr, 10); | 835 | rgb[i] = (int) strtoul(ch + 1, &endPtr, 10); |
836 | ch = endPtr; | 836 | ch = endPtr; |
837 | } | 837 | } |
838 | dst->r = iClamp(rgb[0], 0, 255); | 838 | dst->r = iClamp(rgb[0], 0, 255); |
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 { | |||
136 | tmBackgroundAltText_ColorId, /* derived from other theme colors */ | 136 | tmBackgroundAltText_ColorId, /* derived from other theme colors */ |
137 | tmFrameAltText_ColorId, /* derived from other theme colors */ | 137 | tmFrameAltText_ColorId, /* derived from other theme colors */ |
138 | tmBackgroundOpenLink_ColorId, /* derived from other theme colors */ | 138 | tmBackgroundOpenLink_ColorId, /* derived from other theme colors */ |
139 | tmFrameOpenLink_ColorId, /* derived from other theme colors */ | 139 | tmLinkFeedEntryDate_ColorId, /* derived from other theme colors */ |
140 | tmLinkCustomIconVisited_ColorId, /* derived from other theme colors */ | 140 | tmLinkCustomIconVisited_ColorId, /* derived from other theme colors */ |
141 | tmBadLink_ColorId, | 141 | tmBadLink_ColorId, |
142 | 142 | ||
diff --git a/src/ui/command.c b/src/ui/command.c index 3ae0f0c9..a4868ca9 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. */ | |||
26 | #include <the_Foundation/string.h> | 26 | #include <the_Foundation/string.h> |
27 | #include <ctype.h> | 27 | #include <ctype.h> |
28 | 28 | ||
29 | iDeclareType(Token) | ||
30 | |||
31 | #define maxLen_Token 64 | ||
32 | |||
33 | struct Impl_Token { | ||
34 | char buf[64]; | ||
35 | size_t size; | ||
36 | }; | ||
37 | |||
38 | static void init_Token(iToken *d, const char *label) { | ||
39 | const size_t len = strlen(label); | ||
40 | iAssert(len < sizeof(d->buf) - 3); | ||
41 | d->buf[0] = ' '; | ||
42 | memcpy(d->buf + 1, label, len); | ||
43 | d->buf[1 + len] = ':'; | ||
44 | d->buf[1 + len + 1] = 0; | ||
45 | d->size = len + 2; | ||
46 | } | ||
47 | |||
48 | static iRangecc find_Token(const iToken *d, const char *cmd) { | ||
49 | iRangecc range = iNullRange; | ||
50 | range.start = strstr(cmd, d->buf); | ||
51 | if (range.start) { | ||
52 | range.end = range.start + d->size; | ||
53 | } | ||
54 | return range; | ||
55 | } | ||
56 | |||
29 | iBool equal_Command(const char *cmdWithArgs, const char *cmd) { | 57 | iBool equal_Command(const char *cmdWithArgs, const char *cmd) { |
30 | if (strchr(cmdWithArgs, ':')) { | 58 | if (strchr(cmdWithArgs, ':')) { |
31 | return startsWith_CStr(cmdWithArgs, cmd) && cmdWithArgs[strlen(cmd)] == ' '; | 59 | return startsWith_CStr(cmdWithArgs, cmd) && cmdWithArgs[strlen(cmd)] == ' '; |
@@ -33,15 +61,12 @@ iBool equal_Command(const char *cmdWithArgs, const char *cmd) { | |||
33 | return equal_CStr(cmdWithArgs, cmd); | 61 | return equal_CStr(cmdWithArgs, cmd); |
34 | } | 62 | } |
35 | 63 | ||
36 | static const iString *tokenString_(const char *label) { | ||
37 | return collectNewFormat_String(" %s:", label); | ||
38 | } | ||
39 | |||
40 | int argLabel_Command(const char *cmd, const char *label) { | 64 | int argLabel_Command(const char *cmd, const char *label) { |
41 | const iString *tok = tokenString_(label); | 65 | iToken tok; |
42 | const char *ptr = strstr(cmd, cstr_String(tok)); | 66 | init_Token(&tok, label); |
43 | if (ptr) { | 67 | iRangecc ptr = find_Token(&tok, cmd); |
44 | return atoi(ptr + size_String(tok)); | 68 | if (ptr.start) { |
69 | return atoi(ptr.end); | ||
45 | } | 70 | } |
46 | return 0; | 71 | return 0; |
47 | } | 72 | } |
@@ -51,19 +76,21 @@ int arg_Command(const char *cmd) { | |||
51 | } | 76 | } |
52 | 77 | ||
53 | uint32_t argU32Label_Command(const char *cmd, const char *label) { | 78 | uint32_t argU32Label_Command(const char *cmd, const char *label) { |
54 | const iString *tok = tokenString_(label); | 79 | iToken tok; |
55 | const char *ptr = strstr(cmd, cstr_String(tok)); | 80 | init_Token(&tok, label); |
56 | if (ptr) { | 81 | const iRangecc ptr = find_Token(&tok, cmd); |
57 | return strtoul(ptr + size_String(tok), NULL, 10); | 82 | if (ptr.start) { |
83 | return (uint32_t) strtoul(ptr.end, NULL, 10); | ||
58 | } | 84 | } |
59 | return 0; | 85 | return 0; |
60 | } | 86 | } |
61 | 87 | ||
62 | float argfLabel_Command(const char *cmd, const char *label) { | 88 | float argfLabel_Command(const char *cmd, const char *label) { |
63 | const iString *tok = tokenString_(label); | 89 | iToken tok; |
64 | const char *ptr = strstr(cmd, cstr_String(tok)); | 90 | init_Token(&tok, label); |
65 | if (ptr) { | 91 | const iRangecc ptr = find_Token(&tok, cmd); |
66 | return strtof(ptr + size_String(tok), NULL); | 92 | if (ptr.start) { |
93 | return strtof(ptr.end, NULL); | ||
67 | } | 94 | } |
68 | return 0.0f; | 95 | return 0.0f; |
69 | } | 96 | } |
@@ -77,11 +104,12 @@ float argf_Command(const char *cmd) { | |||
77 | } | 104 | } |
78 | 105 | ||
79 | void *pointerLabel_Command(const char *cmd, const char *label) { | 106 | void *pointerLabel_Command(const char *cmd, const char *label) { |
80 | const iString *tok = tokenString_(label); | 107 | iToken tok; |
81 | const char *ptr = strstr(cmd, cstr_String(tok)); | 108 | init_Token(&tok, label); |
82 | if (ptr) { | 109 | const iRangecc ptr = find_Token(&tok, cmd); |
110 | if (ptr.start) { | ||
83 | void *val = NULL; | 111 | void *val = NULL; |
84 | sscanf(ptr + size_String(tok), "%p", &val); | 112 | sscanf(ptr.end, "%p", &val); |
85 | return val; | 113 | return val; |
86 | } | 114 | } |
87 | return NULL; | 115 | return NULL; |
@@ -92,10 +120,11 @@ void *pointer_Command(const char *cmd) { | |||
92 | } | 120 | } |
93 | 121 | ||
94 | const char *suffixPtr_Command(const char *cmd, const char *label) { | 122 | const char *suffixPtr_Command(const char *cmd, const char *label) { |
95 | const iString *tok = tokenString_(label); | 123 | iToken tok; |
96 | const char *ptr = strstr(cmd, cstr_String(tok)); | 124 | init_Token(&tok, label); |
97 | if (ptr) { | 125 | const iRangecc ptr = find_Token(&tok, cmd); |
98 | return ptr + size_String(tok); | 126 | if (ptr.start) { |
127 | return ptr.end; | ||
99 | } | 128 | } |
100 | return NULL; | 129 | return NULL; |
101 | } | 130 | } |
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 46af5fcd..fdb55232 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 | |||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | 20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | 21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ |
22 | 22 | ||
23 | /* TODO: This file is a little (!) too large. DocumentWidget could be split into | 23 | /* TODO: Move DocumentView into a source file of its own. Consider cleaning up the network |
24 | a couple of smaller objects. One for rendering the document, for instance. */ | 24 | request handling. */ |
25 | 25 | ||
26 | #include "documentwidget.h" | 26 | #include "documentwidget.h" |
27 | 27 | ||
@@ -41,6 +41,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
41 | #include "inputwidget.h" | 41 | #include "inputwidget.h" |
42 | #include "keys.h" | 42 | #include "keys.h" |
43 | #include "labelwidget.h" | 43 | #include "labelwidget.h" |
44 | #include "linkinfo.h" | ||
44 | #include "media.h" | 45 | #include "media.h" |
45 | #include "paint.h" | 46 | #include "paint.h" |
46 | #include "periodic.h" | 47 | #include "periodic.h" |
@@ -55,6 +56,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
55 | #include "visbuf.h" | 56 | #include "visbuf.h" |
56 | #include "visited.h" | 57 | #include "visited.h" |
57 | 58 | ||
59 | #if defined (iPlatformAppleDesktop) | ||
60 | # include "macos.h" | ||
61 | #endif | ||
58 | #if defined (iPlatformAppleMobile) | 62 | #if defined (iPlatformAppleMobile) |
59 | # include "ios.h" | 63 | # include "ios.h" |
60 | #endif | 64 | #endif |
@@ -161,10 +165,10 @@ enum iDrawBufsFlag { | |||
161 | }; | 165 | }; |
162 | 166 | ||
163 | struct Impl_DrawBufs { | 167 | struct Impl_DrawBufs { |
164 | int flags; | 168 | int flags; |
165 | SDL_Texture * sideIconBuf; | 169 | SDL_Texture *sideIconBuf; |
166 | iTextBuf * timestampBuf; | 170 | iTextBuf *timestampBuf; |
167 | uint32_t lastRenderTime; | 171 | uint32_t lastRenderTime; |
168 | }; | 172 | }; |
169 | 173 | ||
170 | static void init_DrawBufs(iDrawBufs *d) { | 174 | static void init_DrawBufs(iDrawBufs *d) { |
@@ -198,16 +202,6 @@ static void visBufInvalidated_(iVisBuf *d, size_t index) { | |||
198 | 202 | ||
199 | /*----------------------------------------------------------------------------------------------*/ | 203 | /*----------------------------------------------------------------------------------------------*/ |
200 | 204 | ||
201 | static void animate_DocumentWidget_ (void *ticker); | ||
202 | static void animateMedia_DocumentWidget_ (iDocumentWidget *d); | ||
203 | static void updateSideIconBuf_DocumentWidget_ (const iDocumentWidget *d); | ||
204 | static void prerender_DocumentWidget_ (iAny *); | ||
205 | static void scrollBegan_DocumentWidget_ (iAnyObject *, int, uint32_t); | ||
206 | |||
207 | static const int smoothDuration_DocumentWidget_(enum iScrollType type) { | ||
208 | return 600 /* milliseconds */ * scrollSpeedFactor_Prefs(prefs_App(), type); | ||
209 | } | ||
210 | |||
211 | enum iRequestState { | 205 | enum iRequestState { |
212 | blank_RequestState, | 206 | blank_RequestState, |
213 | fetching_RequestState, | 207 | fetching_RequestState, |
@@ -229,8 +223,14 @@ enum iDocumentWidgetFlag { | |||
229 | movingSelectMarkEnd_DocumentWidgetFlag = iBit(11), | 223 | movingSelectMarkEnd_DocumentWidgetFlag = iBit(11), |
230 | otherRootByDefault_DocumentWidgetFlag = iBit(12), /* links open to other root by default */ | 224 | otherRootByDefault_DocumentWidgetFlag = iBit(12), /* links open to other root by default */ |
231 | urlChanged_DocumentWidgetFlag = iBit(13), | 225 | urlChanged_DocumentWidgetFlag = iBit(13), |
232 | openedFromSidebar_DocumentWidgetFlag = iBit(14), | 226 | drawDownloadCounter_DocumentWidgetFlag = iBit(14), |
233 | drawDownloadCounter_DocumentWidgetFlag = iBit(15), | 227 | fromCache_DocumentWidgetFlag = iBit(15), /* don't write anything to cache */ |
228 | animationPlaceholder_DocumentWidgetFlag = iBit(16), /* avoid slow operations */ | ||
229 | invalidationPending_DocumentWidgetFlag = iBit(17), /* invalidate as soon as convenient */ | ||
230 | leftWheelSwipe_DocumentWidgetFlag = iBit(18), /* swipe state flags are used on desktop */ | ||
231 | rightWheelSwipe_DocumentWidgetFlag = iBit(19), | ||
232 | eitherWheelSwipe_DocumentWidgetFlag = leftWheelSwipe_DocumentWidgetFlag | | ||
233 | rightWheelSwipe_DocumentWidgetFlag, | ||
234 | }; | 234 | }; |
235 | 235 | ||
236 | enum iDocumentLinkOrdinalMode { | 236 | enum iDocumentLinkOrdinalMode { |
@@ -238,10 +238,44 @@ enum iDocumentLinkOrdinalMode { | |||
238 | homeRow_DocumentLinkOrdinalMode, | 238 | homeRow_DocumentLinkOrdinalMode, |
239 | }; | 239 | }; |
240 | 240 | ||
241 | enum iWheelSwipeState { | ||
242 | none_WheelSwipeState, | ||
243 | direct_WheelSwipeState, | ||
244 | }; | ||
245 | |||
246 | /* TODO: DocumentView is supposed to be useful on its own; move to a separate source file. */ | ||
247 | iDeclareType(DocumentView) | ||
248 | |||
249 | struct Impl_DocumentView { | ||
250 | iDocumentWidget *owner; /* TODO: Convert to an abstract provider of metrics? */ | ||
251 | iGmDocument * doc; | ||
252 | int pageMargin; | ||
253 | iSmoothScroll scrollY; | ||
254 | iAnim sideOpacity; | ||
255 | iAnim altTextOpacity; | ||
256 | iGmRunRange visibleRuns; | ||
257 | iPtrArray visibleLinks; | ||
258 | iPtrArray visiblePre; | ||
259 | iPtrArray visibleMedia; /* currently playing audio / ongoing downloads */ | ||
260 | iPtrArray visibleWideRuns; /* scrollable blocks; TODO: merge into `visiblePre` */ | ||
261 | const iGmRun * hoverPre; /* for clicking */ | ||
262 | const iGmRun * hoverAltPre; /* for drawing alt text */ | ||
263 | const iGmRun * hoverLink; | ||
264 | iArray wideRunOffsets; | ||
265 | iAnim animWideRunOffset; | ||
266 | uint16_t animWideRunId; | ||
267 | iGmRunRange animWideRunRange; | ||
268 | iDrawBufs * drawBufs; /* dynamic state for drawing */ | ||
269 | iVisBuf * visBuf; | ||
270 | iVisBufMeta * visBufMeta; | ||
271 | iGmRunRange renderRuns; | ||
272 | iPtrSet * invalidRuns; | ||
273 | }; | ||
274 | |||
241 | struct Impl_DocumentWidget { | 275 | struct Impl_DocumentWidget { |
242 | iWidget widget; | 276 | iWidget widget; |
243 | int flags; /* internal behavior, see enum iDocumentWidgetFlag */ | 277 | int flags; /* internal behavior, see enum iDocumentWidgetFlag */ |
244 | 278 | ||
245 | /* User interface: */ | 279 | /* User interface: */ |
246 | enum iDocumentLinkOrdinalMode ordinalMode; | 280 | enum iDocumentLinkOrdinalMode ordinalMode; |
247 | size_t ordinalBase; | 281 | size_t ordinalBase; |
@@ -251,19 +285,22 @@ struct Impl_DocumentWidget { | |||
251 | const iGmRun * grabbedPlayer; /* currently adjusting volume in a player */ | 285 | const iGmRun * grabbedPlayer; /* currently adjusting volume in a player */ |
252 | float grabbedStartVolume; | 286 | float grabbedStartVolume; |
253 | int mediaTimer; | 287 | int mediaTimer; |
254 | const iGmRun * hoverPre; /* for clicking */ | ||
255 | const iGmRun * hoverAltPre; /* for drawing alt text */ | ||
256 | const iGmRun * hoverLink; | ||
257 | const iGmRun * contextLink; | 288 | const iGmRun * contextLink; |
258 | iClick click; | 289 | iClick click; |
259 | iInt2 contextPos; /* coordinates of latest right click */ | 290 | iInt2 contextPos; /* coordinates of latest right click */ |
260 | int pinchZoomInitial; | 291 | int pinchZoomInitial; |
261 | int pinchZoomPosted; | 292 | int pinchZoomPosted; |
293 | float swipeSpeed; /* points/sec */ | ||
294 | uint32_t lastSwipeTime; | ||
295 | int wheelSwipeDistance; | ||
296 | enum iWheelSwipeState wheelSwipeState; | ||
262 | iString pendingGotoHeading; | 297 | iString pendingGotoHeading; |
263 | 298 | iString linePrecedingLink; | |
299 | |||
264 | /* Network request: */ | 300 | /* Network request: */ |
265 | enum iRequestState state; | 301 | enum iRequestState state; |
266 | iGmRequest * request; | 302 | iGmRequest * request; |
303 | iGmLinkId requestLinkId; /* ID of the link that initiated the current request */ | ||
267 | iAtomicInt isRequestUpdated; /* request has new content, need to parse it */ | 304 | iAtomicInt isRequestUpdated; /* request has new content, need to parse it */ |
268 | int certFlags; | 305 | int certFlags; |
269 | iBlock * certFingerprint; | 306 | iBlock * certFingerprint; |
@@ -271,7 +308,7 @@ struct Impl_DocumentWidget { | |||
271 | iString * certSubject; | 308 | iString * certSubject; |
272 | int redirectCount; | 309 | int redirectCount; |
273 | iObjectList * media; /* inline media requests */ | 310 | iObjectList * media; /* inline media requests */ |
274 | 311 | ||
275 | /* Document: */ | 312 | /* Document: */ |
276 | iPersistentDocumentState mod; | 313 | iPersistentDocumentState mod; |
277 | iString * titleUser; | 314 | iString * titleUser; |
@@ -281,31 +318,14 @@ struct Impl_DocumentWidget { | |||
281 | iBlock sourceContent; /* original content as received, for saving; set on request finish */ | 318 | iBlock sourceContent; /* original content as received, for saving; set on request finish */ |
282 | iTime sourceTime; | 319 | iTime sourceTime; |
283 | iGempub * sourceGempub; /* NULL unless the page is Gempub content */ | 320 | iGempub * sourceGempub; /* NULL unless the page is Gempub content */ |
284 | iGmDocument * doc; | ||
285 | iBanner * banner; | 321 | iBanner * banner; |
286 | |||
287 | /* Rendering: */ | ||
288 | int pageMargin; | ||
289 | float initNormScrollY; | 322 | float initNormScrollY; |
290 | iSmoothScroll scrollY; | 323 | |
291 | iAnim sideOpacity; | 324 | /* Rendering: */ |
292 | iAnim altTextOpacity; | 325 | iDocumentView view; |
293 | iGmRunRange visibleRuns; | 326 | iLinkInfo * linkInfo; |
294 | iPtrArray visibleLinks; | 327 | |
295 | iPtrArray visiblePre; | 328 | /* Widget structure: */ |
296 | iPtrArray visibleMedia; /* currently playing audio / ongoing downloads */ | ||
297 | iPtrArray visibleWideRuns; /* scrollable blocks; TODO: merge into `visiblePre` */ | ||
298 | iArray wideRunOffsets; | ||
299 | iAnim animWideRunOffset; | ||
300 | uint16_t animWideRunId; | ||
301 | iGmRunRange animWideRunRange; | ||
302 | iDrawBufs * drawBufs; /* dynamic state for drawing */ | ||
303 | iVisBuf * visBuf; | ||
304 | iVisBufMeta * visBufMeta; | ||
305 | iGmRunRange renderRuns; | ||
306 | iPtrSet * invalidRuns; | ||
307 | |||
308 | /* Widget structure: */ | ||
309 | iScrollWidget *scroll; | 329 | iScrollWidget *scroll; |
310 | iWidget * footerButtons; | 330 | iWidget * footerButtons; |
311 | iWidget * menu; | 331 | iWidget * menu; |
@@ -317,46 +337,159 @@ struct Impl_DocumentWidget { | |||
317 | 337 | ||
318 | iDefineObjectConstruction(DocumentWidget) | 338 | iDefineObjectConstruction(DocumentWidget) |
319 | 339 | ||
340 | /* Sorted by proximity to F and J. */ | ||
341 | static const int homeRowKeys_[] = { | ||
342 | 'f', 'd', 's', 'a', | ||
343 | 'j', 'k', 'l', | ||
344 | 'r', 'e', 'w', 'q', | ||
345 | 'u', 'i', 'o', 'p', | ||
346 | 'v', 'c', 'x', 'z', | ||
347 | 'm', 'n', | ||
348 | 'g', 'h', | ||
349 | 'b', | ||
350 | 't', 'y', | ||
351 | }; | ||
320 | static int docEnum_ = 0; | 352 | static int docEnum_ = 0; |
321 | 353 | ||
322 | void init_DocumentWidget(iDocumentWidget *d) { | 354 | static void animate_DocumentWidget_ (void *ticker); |
323 | iWidget *w = as_Widget(d); | 355 | static void animateMedia_DocumentWidget_ (iDocumentWidget *d); |
324 | init_Widget(w); | 356 | static void updateSideIconBuf_DocumentWidget_ (const iDocumentWidget *d); |
325 | setId_Widget(w, format_CStr("document%03d", ++docEnum_)); | 357 | static void prerender_DocumentWidget_ (iAny *); |
326 | setFlags_Widget(w, hover_WidgetFlag | noBackground_WidgetFlag, iTrue); | 358 | static void scrollBegan_DocumentWidget_ (iAnyObject *, int, uint32_t); |
327 | if (deviceType_App() != desktop_AppDeviceType) { | 359 | static void refreshWhileScrolling_DocumentWidget_ (iAny *); |
328 | setFlags_Widget(w, leftEdgeDraggable_WidgetFlag | rightEdgeDraggable_WidgetFlag | | 360 | |
329 | horizontalOffset_WidgetFlag, iTrue); | 361 | /* TODO: The following methods are called from DocumentView, which goes the wrong way. */ |
362 | |||
363 | static iRangecc selectMark_DocumentWidget_(const iDocumentWidget *d) { | ||
364 | /* Normalize so start < end. */ | ||
365 | iRangecc norm = d->selectMark; | ||
366 | if (norm.start > norm.end) { | ||
367 | iSwap(const char *, norm.start, norm.end); | ||
330 | } | 368 | } |
331 | init_PersistentDocumentState(&d->mod); | 369 | return norm; |
332 | d->flags = 0; | 370 | } |
333 | d->phoneToolbar = NULL; | 371 | |
334 | d->footerButtons = NULL; | 372 | static int phoneToolbarHeight_DocumentWidget_(const iDocumentWidget *d) { |
335 | iZap(d->certExpiry); | 373 | if (!d->phoneToolbar) { |
336 | d->certFingerprint = new_Block(0); | 374 | return 0; |
337 | d->certFlags = 0; | 375 | } |
338 | d->certSubject = new_String(); | 376 | const iWidget *w = constAs_Widget(d); |
339 | d->state = blank_RequestState; | 377 | return bottom_Rect(rect_Root(w->root)) - top_Rect(boundsWithoutVisualOffset_Widget(d->phoneToolbar)); |
340 | d->titleUser = new_String(); | 378 | } |
341 | d->request = NULL; | 379 | |
342 | d->isRequestUpdated = iFalse; | 380 | static int footerHeight_DocumentWidget_(const iDocumentWidget *d) { |
343 | d->media = new_ObjectList(); | 381 | int hgt = height_Widget(d->footerButtons); |
382 | if (isPortraitPhone_App()) { | ||
383 | hgt += phoneToolbarHeight_DocumentWidget_(d); | ||
384 | } | ||
385 | return hgt; | ||
386 | } | ||
387 | |||
388 | static iBool isHoverAllowed_DocumentWidget_(const iDocumentWidget *d) { | ||
389 | if (!isHover_Widget(d)) { | ||
390 | return iFalse; | ||
391 | } | ||
392 | if (!(d->state == ready_RequestState || d->state == receivedPartialResponse_RequestState)) { | ||
393 | return iFalse; | ||
394 | } | ||
395 | if (d->flags & (noHoverWhileScrolling_DocumentWidgetFlag | | ||
396 | drawDownloadCounter_DocumentWidgetFlag)) { | ||
397 | return iFalse; | ||
398 | } | ||
399 | if (d->flags & pinchZoom_DocumentWidgetFlag) { | ||
400 | return iFalse; | ||
401 | } | ||
402 | if (flags_Widget(constAs_Widget(d)) & touchDrag_WidgetFlag) { | ||
403 | return iFalse; | ||
404 | } | ||
405 | if (flags_Widget(constAs_Widget(d->scroll)) & pressed_WidgetFlag) { | ||
406 | return iFalse; | ||
407 | } | ||
408 | return iTrue; | ||
409 | } | ||
410 | |||
411 | static iMediaRequest *findMediaRequest_DocumentWidget_(const iDocumentWidget *d, iGmLinkId linkId) { | ||
412 | iConstForEach(ObjectList, i, d->media) { | ||
413 | const iMediaRequest *req = (const iMediaRequest *) i.object; | ||
414 | if (req->linkId == linkId) { | ||
415 | return iConstCast(iMediaRequest *, req); | ||
416 | } | ||
417 | } | ||
418 | return NULL; | ||
419 | } | ||
420 | |||
421 | static size_t linkOrdinalFromKey_DocumentWidget_(const iDocumentWidget *d, int key) { | ||
422 | size_t ord = iInvalidPos; | ||
423 | if (d->ordinalMode == numbersAndAlphabet_DocumentLinkOrdinalMode) { | ||
424 | if (key >= '1' && key <= '9') { | ||
425 | return key - '1'; | ||
426 | } | ||
427 | if (key < 'a' || key > 'z') { | ||
428 | return iInvalidPos; | ||
429 | } | ||
430 | ord = key - 'a' + 9; | ||
431 | #if defined (iPlatformApple) | ||
432 | /* Skip keys that would conflict with default system shortcuts: hide, minimize, quit, close. */ | ||
433 | if (key == 'h' || key == 'm' || key == 'q' || key == 'w') { | ||
434 | return iInvalidPos; | ||
435 | } | ||
436 | if (key > 'h') ord--; | ||
437 | if (key > 'm') ord--; | ||
438 | if (key > 'q') ord--; | ||
439 | if (key > 'w') ord--; | ||
440 | #endif | ||
441 | } | ||
442 | else { | ||
443 | iForIndices(i, homeRowKeys_) { | ||
444 | if (homeRowKeys_[i] == key) { | ||
445 | return i; | ||
446 | } | ||
447 | } | ||
448 | } | ||
449 | return ord; | ||
450 | } | ||
451 | |||
452 | static iChar linkOrdinalChar_DocumentWidget_(const iDocumentWidget *d, size_t ord) { | ||
453 | if (d->ordinalMode == numbersAndAlphabet_DocumentLinkOrdinalMode) { | ||
454 | if (ord < 9) { | ||
455 | return '1' + ord; | ||
456 | } | ||
457 | #if defined (iPlatformApple) | ||
458 | if (ord < 9 + 22) { | ||
459 | int key = 'a' + ord - 9; | ||
460 | if (key >= 'h') key++; | ||
461 | if (key >= 'm') key++; | ||
462 | if (key >= 'q') key++; | ||
463 | if (key >= 'w') key++; | ||
464 | return 'A' + key - 'a'; | ||
465 | } | ||
466 | #else | ||
467 | if (ord < 9 + 26) { | ||
468 | return 'A' + ord - 9; | ||
469 | } | ||
470 | #endif | ||
471 | } | ||
472 | else { | ||
473 | if (ord < iElemCount(homeRowKeys_)) { | ||
474 | return 'A' + homeRowKeys_[ord] - 'a'; | ||
475 | } | ||
476 | } | ||
477 | return 0; | ||
478 | } | ||
479 | |||
480 | /*----------------------------------------------------------------------------------------------*/ | ||
481 | |||
482 | void init_DocumentView(iDocumentView *d) { | ||
483 | d->owner = NULL; | ||
344 | d->doc = new_GmDocument(); | 484 | d->doc = new_GmDocument(); |
345 | d->banner = new_Banner(); | 485 | d->invalidRuns = new_PtrSet(); |
346 | setOwner_Banner(d->banner, d); | 486 | d->drawBufs = new_DrawBufs(); |
347 | d->redirectCount = 0; | 487 | d->pageMargin = 5; |
348 | d->ordinalBase = 0; | 488 | d->hoverPre = NULL; |
349 | d->initNormScrollY = 0; | 489 | d->hoverAltPre = NULL; |
350 | init_SmoothScroll(&d->scrollY, w, scrollBegan_DocumentWidget_); | 490 | d->hoverLink = NULL; |
351 | d->animWideRunId = 0; | 491 | d->animWideRunId = 0; |
352 | init_Anim(&d->animWideRunOffset, 0); | 492 | init_Anim(&d->animWideRunOffset, 0); |
353 | d->selectMark = iNullRange; | ||
354 | d->foundMark = iNullRange; | ||
355 | d->pageMargin = 5; | ||
356 | d->hoverPre = NULL; | ||
357 | d->hoverAltPre = NULL; | ||
358 | d->hoverLink = NULL; | ||
359 | d->contextLink = NULL; | ||
360 | iZap(d->renderRuns); | 493 | iZap(d->renderRuns); |
361 | iZap(d->visibleRuns); | 494 | iZap(d->visibleRuns); |
362 | d->visBuf = new_VisBuf(); { | 495 | d->visBuf = new_VisBuf(); { |
@@ -367,212 +500,105 @@ void init_DocumentWidget(iDocumentWidget *d) { | |||
367 | d->visBuf->buffers[i].user = d->visBufMeta + i; | 500 | d->visBuf->buffers[i].user = d->visBufMeta + i; |
368 | } | 501 | } |
369 | } | 502 | } |
370 | d->invalidRuns = new_PtrSet(); | ||
371 | init_Anim(&d->sideOpacity, 0); | 503 | init_Anim(&d->sideOpacity, 0); |
372 | init_Anim(&d->altTextOpacity, 0); | 504 | init_Anim(&d->altTextOpacity, 0); |
373 | d->sourceStatus = none_GmStatusCode; | ||
374 | init_String(&d->sourceHeader); | ||
375 | init_String(&d->sourceMime); | ||
376 | init_Block(&d->sourceContent, 0); | ||
377 | iZap(d->sourceTime); | ||
378 | d->sourceGempub = NULL; | ||
379 | init_PtrArray(&d->visibleLinks); | 505 | init_PtrArray(&d->visibleLinks); |
380 | init_PtrArray(&d->visiblePre); | 506 | init_PtrArray(&d->visiblePre); |
381 | init_PtrArray(&d->visibleWideRuns); | 507 | init_PtrArray(&d->visibleWideRuns); |
382 | init_Array(&d->wideRunOffsets, sizeof(int)); | 508 | init_Array(&d->wideRunOffsets, sizeof(int)); |
383 | init_PtrArray(&d->visibleMedia); | 509 | init_PtrArray(&d->visibleMedia); |
384 | d->grabbedPlayer = NULL; | ||
385 | d->mediaTimer = 0; | ||
386 | init_String(&d->pendingGotoHeading); | ||
387 | init_Click(&d->click, d, SDL_BUTTON_LEFT); | ||
388 | addChild_Widget(w, iClob(d->scroll = new_ScrollWidget())); | ||
389 | d->menu = NULL; /* created when clicking */ | ||
390 | d->playerMenu = NULL; | ||
391 | d->copyMenu = NULL; | ||
392 | d->drawBufs = new_DrawBufs(); | ||
393 | d->translation = NULL; | ||
394 | addChildFlags_Widget(w, | ||
395 | iClob(new_IndicatorWidget()), | ||
396 | resizeToParentWidth_WidgetFlag | resizeToParentHeight_WidgetFlag); | ||
397 | #if !defined (iPlatformAppleDesktop) /* in system menu */ | ||
398 | addAction_Widget(w, reload_KeyShortcut, "navigate.reload"); | ||
399 | addAction_Widget(w, closeTab_KeyShortcut, "tabs.close"); | ||
400 | addAction_Widget(w, SDLK_d, KMOD_PRIMARY, "bookmark.add"); | ||
401 | addAction_Widget(w, subscribeToPage_KeyModifier, "feeds.subscribe"); | ||
402 | #endif | ||
403 | addAction_Widget(w, navigateBack_KeyShortcut, "navigate.back"); | ||
404 | addAction_Widget(w, navigateForward_KeyShortcut, "navigate.forward"); | ||
405 | addAction_Widget(w, navigateParent_KeyShortcut, "navigate.parent"); | ||
406 | addAction_Widget(w, navigateRoot_KeyShortcut, "navigate.root"); | ||
407 | } | 510 | } |
408 | 511 | ||
409 | void cancelAllRequests_DocumentWidget(iDocumentWidget *d) { | 512 | void deinit_DocumentView(iDocumentView *d) { |
410 | iForEach(ObjectList, i, d->media) { | ||
411 | iMediaRequest *mr = i.object; | ||
412 | cancel_GmRequest(mr->req); | ||
413 | } | ||
414 | if (d->request) { | ||
415 | cancel_GmRequest(d->request); | ||
416 | } | ||
417 | } | ||
418 | |||
419 | void deinit_DocumentWidget(iDocumentWidget *d) { | ||
420 | cancelAllRequests_DocumentWidget(d); | ||
421 | pauseAllPlayers_Media(media_GmDocument(d->doc), iTrue); | ||
422 | removeTicker_App(animate_DocumentWidget_, d); | ||
423 | removeTicker_App(prerender_DocumentWidget_, d); | ||
424 | remove_Periodic(periodic_App(), d); | ||
425 | delete_Translation(d->translation); | ||
426 | delete_DrawBufs(d->drawBufs); | 513 | delete_DrawBufs(d->drawBufs); |
427 | delete_VisBuf(d->visBuf); | 514 | delete_VisBuf(d->visBuf); |
428 | free(d->visBufMeta); | 515 | free(d->visBufMeta); |
429 | delete_PtrSet(d->invalidRuns); | 516 | delete_PtrSet(d->invalidRuns); |
430 | iRelease(d->media); | ||
431 | iRelease(d->request); | ||
432 | delete_Gempub(d->sourceGempub); | ||
433 | deinit_String(&d->pendingGotoHeading); | ||
434 | deinit_Block(&d->sourceContent); | ||
435 | deinit_String(&d->sourceMime); | ||
436 | deinit_String(&d->sourceHeader); | ||
437 | delete_Banner(d->banner); | ||
438 | iRelease(d->doc); | ||
439 | if (d->mediaTimer) { | ||
440 | SDL_RemoveTimer(d->mediaTimer); | ||
441 | } | ||
442 | deinit_Array(&d->wideRunOffsets); | 517 | deinit_Array(&d->wideRunOffsets); |
443 | deinit_PtrArray(&d->visibleMedia); | 518 | deinit_PtrArray(&d->visibleMedia); |
444 | deinit_PtrArray(&d->visibleWideRuns); | 519 | deinit_PtrArray(&d->visibleWideRuns); |
445 | deinit_PtrArray(&d->visiblePre); | 520 | deinit_PtrArray(&d->visiblePre); |
446 | deinit_PtrArray(&d->visibleLinks); | 521 | deinit_PtrArray(&d->visibleLinks); |
447 | delete_Block(d->certFingerprint); | 522 | iReleasePtr(&d->doc); |
448 | delete_String(d->certSubject); | ||
449 | delete_String(d->titleUser); | ||
450 | deinit_PersistentDocumentState(&d->mod); | ||
451 | } | ||
452 | |||
453 | static iRangecc selectMark_DocumentWidget_(const iDocumentWidget *d) { | ||
454 | /* Normalize so start < end. */ | ||
455 | iRangecc norm = d->selectMark; | ||
456 | if (norm.start > norm.end) { | ||
457 | iSwap(const char *, norm.start, norm.end); | ||
458 | } | ||
459 | return norm; | ||
460 | } | 523 | } |
461 | 524 | ||
462 | static void enableActions_DocumentWidget_(iDocumentWidget *d, iBool enable) { | 525 | static void setOwner_DocumentView_(iDocumentView *d, iDocumentWidget *doc) { |
463 | /* Actions are invisible child widgets of the DocumentWidget. */ | 526 | d->owner = doc; |
464 | iForEach(ObjectList, i, children_Widget(d)) { | 527 | init_SmoothScroll(&d->scrollY, as_Widget(doc), scrollBegan_DocumentWidget_); |
465 | if (isAction_Widget(i.object)) { | 528 | if (deviceType_App() != desktop_AppDeviceType) { |
466 | setFlags_Widget(i.object, disabled_WidgetFlag, !enable); | 529 | d->scrollY.flags |= pullDownAction_SmoothScrollFlag; /* pull to refresh */ |
467 | } | ||
468 | } | ||
469 | } | ||
470 | |||
471 | static void setLinkNumberMode_DocumentWidget_(iDocumentWidget *d, iBool set) { | ||
472 | iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, set); | ||
473 | /* Children have priority when handling events. */ | ||
474 | enableActions_DocumentWidget_(d, !set); | ||
475 | if (d->menu) { | ||
476 | setFlags_Widget(d->menu, disabled_WidgetFlag, set); | ||
477 | } | 530 | } |
478 | } | 531 | } |
479 | 532 | ||
480 | static void resetWideRuns_DocumentWidget_(iDocumentWidget *d) { | 533 | static void resetWideRuns_DocumentView_(iDocumentView *d) { |
481 | clear_Array(&d->wideRunOffsets); | 534 | clear_Array(&d->wideRunOffsets); |
482 | d->animWideRunId = 0; | 535 | d->animWideRunId = 0; |
483 | init_Anim(&d->animWideRunOffset, 0); | 536 | init_Anim(&d->animWideRunOffset, 0); |
484 | iZap(d->animWideRunRange); | 537 | iZap(d->animWideRunRange); |
485 | } | 538 | } |
486 | 539 | ||
487 | static void requestUpdated_DocumentWidget_(iAnyObject *obj) { | 540 | static int documentWidth_DocumentView_(const iDocumentView *d) { |
488 | iDocumentWidget *d = obj; | 541 | const iWidget *w = constAs_Widget(d->owner); |
489 | const int wasUpdated = exchange_Atomic(&d->isRequestUpdated, iTrue); | ||
490 | if (!wasUpdated) { | ||
491 | postCommand_Widget(obj, | ||
492 | "document.request.updated doc:%p reqid:%u request:%p", | ||
493 | d, | ||
494 | id_GmRequest(d->request), | ||
495 | d->request); | ||
496 | } | ||
497 | } | ||
498 | |||
499 | static void requestFinished_DocumentWidget_(iAnyObject *obj) { | ||
500 | iDocumentWidget *d = obj; | ||
501 | postCommand_Widget(obj, | ||
502 | "document.request.finished doc:%p reqid:%u request:%p", | ||
503 | d, | ||
504 | id_GmRequest(d->request), | ||
505 | d->request); | ||
506 | } | ||
507 | |||
508 | static int documentWidth_DocumentWidget_(const iDocumentWidget *d) { | ||
509 | const iWidget *w = constAs_Widget(d); | ||
510 | const iRect bounds = bounds_Widget(w); | 542 | const iRect bounds = bounds_Widget(w); |
511 | const iPrefs * prefs = prefs_App(); | 543 | const iPrefs * prefs = prefs_App(); |
512 | const int minWidth = 50 * gap_UI; /* lines must fit a word at least */ | 544 | const int minWidth = 50 * gap_UI; /* lines must fit a word at least */ |
513 | const float adjust = iClamp((float) bounds.size.x / gap_UI / 11 - 12, | 545 | const float adjust = iClamp((float) bounds.size.x / gap_UI / 11 - 12, |
514 | -1.0f, 10.0f); /* adapt to width */ | 546 | -1.0f, 10.0f); /* adapt to width */ |
515 | //printf("%f\n", adjust); fflush(stdout); | 547 | //printf("%f\n", adjust); fflush(stdout); |
516 | return iMini(iMax(minWidth, bounds.size.x - gap_UI * (d->pageMargin + adjust) * 2), | 548 | return iMini(iMax(minWidth, bounds.size.x - gap_UI * (d->pageMargin + adjust) * 2), |
517 | fontSize_UI * //emRatio_Text(paragraph_FontId) * /* dependent on avg. glyph width */ | 549 | fontSize_UI * //emRatio_Text(paragraph_FontId) * /* dependent on avg. glyph width */ |
518 | prefs->lineWidth * prefs->zoomPercent / 100); | 550 | prefs->lineWidth * prefs->zoomPercent / 100); |
519 | } | 551 | } |
520 | 552 | ||
521 | static int documentTopPad_DocumentWidget_(const iDocumentWidget *d) { | 553 | static int documentTopPad_DocumentView_(const iDocumentView *d) { |
522 | /* Amount of space between banner and top of the document. */ | 554 | /* Amount of space between banner and top of the document. */ |
523 | return isEmpty_Banner(d->banner) ? 0 : lineHeight_Text(paragraph_FontId); | 555 | return isEmpty_Banner(d->owner->banner) ? 0 : lineHeight_Text(paragraph_FontId); |
524 | } | ||
525 | |||
526 | static int documentTopMargin_DocumentWidget_(const iDocumentWidget *d) { | ||
527 | return (isEmpty_Banner(d->banner) ? d->pageMargin * gap_UI : height_Banner(d->banner)) + | ||
528 | documentTopPad_DocumentWidget_(d); | ||
529 | } | 556 | } |
530 | 557 | ||
531 | static int pageHeight_DocumentWidget_(const iDocumentWidget *d) { | 558 | static int documentTopMargin_DocumentView_(const iDocumentView *d) { |
532 | return height_Banner(d->banner) + documentTopPad_DocumentWidget_(d) + size_GmDocument(d->doc).y; | 559 | return (isEmpty_Banner(d->owner->banner) ? d->pageMargin * gap_UI : height_Banner(d->owner->banner)) + |
560 | documentTopPad_DocumentView_(d); | ||
533 | } | 561 | } |
534 | 562 | ||
535 | static int footerButtonsHeight_DocumentWidget_(const iDocumentWidget *d) { | 563 | static int pageHeight_DocumentView_(const iDocumentView *d) { |
536 | int height = height_Widget(d->footerButtons); | 564 | return height_Banner(d->owner->banner) + documentTopPad_DocumentView_(d) + size_GmDocument(d->doc).y; |
537 | // if (height) { | ||
538 | // height += 3 * gap_UI; /* padding */ | ||
539 | // } | ||
540 | return height; | ||
541 | } | 565 | } |
542 | 566 | ||
543 | static iRect documentBounds_DocumentWidget_(const iDocumentWidget *d) { | 567 | static iRect documentBounds_DocumentView_(const iDocumentView *d) { |
544 | const iRect bounds = bounds_Widget(constAs_Widget(d)); | 568 | const iRect bounds = bounds_Widget(constAs_Widget(d->owner)); |
545 | const int margin = gap_UI * d->pageMargin; | 569 | const int margin = gap_UI * d->pageMargin; |
546 | iRect rect; | 570 | iRect rect; |
547 | rect.size.x = documentWidth_DocumentWidget_(d); | 571 | rect.size.x = documentWidth_DocumentView_(d); |
548 | rect.pos.x = mid_Rect(bounds).x - rect.size.x / 2; | 572 | rect.pos.x = mid_Rect(bounds).x - rect.size.x / 2; |
549 | rect.pos.y = top_Rect(bounds) + margin; | 573 | rect.pos.y = top_Rect(bounds) + margin; |
550 | rect.size.y = height_Rect(bounds) - margin; | 574 | rect.size.y = height_Rect(bounds) - margin; |
551 | iBool wasCentered = iFalse; | 575 | iBool wasCentered = iFalse; |
552 | if (d->flags & centerVertically_DocumentWidgetFlag) { | 576 | /* TODO: Further separation of View and Widget: configure header and footer heights |
577 | without involving the widget here. */ | ||
578 | if (d->owner->flags & centerVertically_DocumentWidgetFlag) { | ||
553 | const int docSize = size_GmDocument(d->doc).y + | 579 | const int docSize = size_GmDocument(d->doc).y + |
554 | documentTopMargin_DocumentWidget_(d); | 580 | documentTopMargin_DocumentView_(d); |
555 | if (size_GmDocument(d->doc).y == 0) { | 581 | if (size_GmDocument(d->doc).y == 0) { |
556 | /* Document is empty; maybe just showing an error banner. */ | 582 | /* Document is empty; maybe just showing an error banner. */ |
557 | rect.pos.y = top_Rect(bounds) + height_Rect(bounds) / 2 - | 583 | rect.pos.y = top_Rect(bounds) + height_Rect(bounds) / 2 - |
558 | documentTopPad_DocumentWidget_(d) - height_Banner(d->banner) / 2; | 584 | documentTopPad_DocumentView_(d) - height_Banner(d->owner->banner) / 2; |
559 | rect.size.y = 0; | 585 | rect.size.y = 0; |
560 | wasCentered = iTrue; | 586 | wasCentered = iTrue; |
561 | } | 587 | } |
562 | else if (docSize < rect.size.y - footerButtonsHeight_DocumentWidget_(d)) { | 588 | else if (docSize < rect.size.y - footerHeight_DocumentWidget_(d->owner)) { |
563 | /* TODO: Phone toolbar? */ | 589 | /* TODO: Phone toolbar? */ |
564 | /* Center vertically when the document is short. */ | 590 | /* Center vertically when the document is short. */ |
565 | const int relMidY = (height_Rect(bounds) - footerButtonsHeight_DocumentWidget_(d)) / 2; | 591 | const int relMidY = (height_Rect(bounds) - footerHeight_DocumentWidget_(d->owner)) / 2; |
566 | const int visHeight = size_GmDocument(d->doc).y; | 592 | const int visHeight = size_GmDocument(d->doc).y; |
567 | const int offset = -height_Banner(d->banner) - documentTopPad_DocumentWidget_(d); | 593 | const int offset = -height_Banner(d->owner->banner) - documentTopPad_DocumentView_(d); |
568 | rect.pos.y = top_Rect(bounds) + iMaxi(0, relMidY - visHeight / 2 + offset); | 594 | rect.pos.y = top_Rect(bounds) + iMaxi(0, relMidY - visHeight / 2 + offset); |
569 | rect.size.y = size_GmDocument(d->doc).y + documentTopMargin_DocumentWidget_(d); | 595 | rect.size.y = size_GmDocument(d->doc).y + documentTopMargin_DocumentView_(d); |
570 | wasCentered = iTrue; | 596 | wasCentered = iTrue; |
571 | } | 597 | } |
572 | } | 598 | } |
573 | if (!wasCentered) { | 599 | if (!wasCentered) { |
574 | /* The banner overtakes the top margin. */ | 600 | /* The banner overtakes the top margin. */ |
575 | if (!isEmpty_Banner(d->banner)) { | 601 | if (!isEmpty_Banner(d->owner->banner)) { |
576 | rect.pos.y -= margin; | 602 | rect.pos.y -= margin; |
577 | } | 603 | } |
578 | else { | 604 | else { |
@@ -582,38 +608,28 @@ static iRect documentBounds_DocumentWidget_(const iDocumentWidget *d) { | |||
582 | return rect; | 608 | return rect; |
583 | } | 609 | } |
584 | 610 | ||
585 | static int viewPos_DocumentWidget_(const iDocumentWidget *d) { | 611 | static int viewPos_DocumentView_(const iDocumentView *d) { |
586 | return height_Banner(d->banner) + documentTopPad_DocumentWidget_(d) - pos_SmoothScroll(&d->scrollY); | 612 | return height_Banner(d->owner->banner) + documentTopPad_DocumentView_(d) - |
587 | } | 613 | pos_SmoothScroll(&d->scrollY); |
588 | |||
589 | #if 0 | ||
590 | static iRect siteBannerRect_DocumentWidget_(const iDocumentWidget *d) { | ||
591 | const iGmRun *banner = siteBanner_GmDocument(d->doc); | ||
592 | if (!banner) { | ||
593 | return zero_Rect(); | ||
594 | } | ||
595 | const iRect docBounds = documentBounds_DocumentWidget_(d); | ||
596 | const iInt2 origin = addY_I2(topLeft_Rect(docBounds), -pos_SmoothScroll(&d->scrollY)); | ||
597 | return moved_Rect(banner->visBounds, origin); | ||
598 | } | 614 | } |
599 | #endif | ||
600 | 615 | ||
601 | static iInt2 documentPos_DocumentWidget_(const iDocumentWidget *d, iInt2 pos) { | 616 | static iInt2 documentPos_DocumentView_(const iDocumentView *d, iInt2 pos) { |
602 | return addY_I2(sub_I2(pos, topLeft_Rect(documentBounds_DocumentWidget_(d))), | 617 | return addY_I2(sub_I2(pos, topLeft_Rect(documentBounds_DocumentView_(d))), |
603 | -viewPos_DocumentWidget_(d)); | 618 | -viewPos_DocumentView_(d)); |
604 | } | 619 | } |
605 | 620 | ||
606 | static iRangei visibleRange_DocumentWidget_(const iDocumentWidget *d) { | 621 | static iRangei visibleRange_DocumentView_(const iDocumentView *d) { |
607 | int top = pos_SmoothScroll(&d->scrollY) - height_Banner(d->banner) - documentTopPad_DocumentWidget_(d); | 622 | int top = pos_SmoothScroll(&d->scrollY) - height_Banner(d->owner->banner) - |
608 | if (isEmpty_Banner(d->banner)) { | 623 | documentTopPad_DocumentView_(d); |
624 | if (isEmpty_Banner(d->owner->banner)) { | ||
609 | /* Top padding is not collapsed. */ | 625 | /* Top padding is not collapsed. */ |
610 | top -= d->pageMargin * gap_UI; | 626 | top -= d->pageMargin * gap_UI; |
611 | } | 627 | } |
612 | return (iRangei){ top, top + height_Rect(bounds_Widget(constAs_Widget(d))) }; | 628 | return (iRangei){ top, top + height_Rect(bounds_Widget(constAs_Widget(d->owner))) }; |
613 | } | 629 | } |
614 | 630 | ||
615 | static void addVisible_DocumentWidget_(void *context, const iGmRun *run) { | 631 | static void addVisible_DocumentView_(void *context, const iGmRun *run) { |
616 | iDocumentWidget *d = context; | 632 | iDocumentView *d = context; |
617 | if (~run->flags & decoration_GmRunFlag && !run->mediaId) { | 633 | if (~run->flags & decoration_GmRunFlag && !run->mediaId) { |
618 | if (!d->visibleRuns.start) { | 634 | if (!d->visibleRuns.start) { |
619 | d->visibleRuns.start = run; | 635 | d->visibleRuns.start = run; |
@@ -636,7 +652,7 @@ static void addVisible_DocumentWidget_(void *context, const iGmRun *run) { | |||
636 | } | 652 | } |
637 | } | 653 | } |
638 | 654 | ||
639 | static const iGmRun *lastVisibleLink_DocumentWidget_(const iDocumentWidget *d) { | 655 | static const iGmRun *lastVisibleLink_DocumentView_(const iDocumentView *d) { |
640 | iReverseConstForEach(PtrArray, i, &d->visibleLinks) { | 656 | iReverseConstForEach(PtrArray, i, &d->visibleLinks) { |
641 | const iGmRun *run = i.ptr; | 657 | const iGmRun *run = i.ptr; |
642 | if (run->flags & decoration_GmRunFlag && run->linkId) { | 658 | if (run->flags & decoration_GmRunFlag && run->linkId) { |
@@ -646,23 +662,24 @@ static const iGmRun *lastVisibleLink_DocumentWidget_(const iDocumentWidget *d) { | |||
646 | return NULL; | 662 | return NULL; |
647 | } | 663 | } |
648 | 664 | ||
649 | static float normScrollPos_DocumentWidget_(const iDocumentWidget *d) { | 665 | static float normScrollPos_DocumentView_(const iDocumentView *d) { |
650 | const int docSize = pageHeight_DocumentWidget_(d); // size_GmDocument(d->doc).y; | 666 | const int docSize = pageHeight_DocumentView_(d); |
651 | if (docSize) { | 667 | if (docSize) { |
652 | return pos_SmoothScroll(&d->scrollY) / (float) docSize; | 668 | float pos = pos_SmoothScroll(&d->scrollY) / (float) docSize; |
669 | return iMax(pos, 0.0f); | ||
653 | } | 670 | } |
654 | return 0; | 671 | return 0; |
655 | } | 672 | } |
656 | 673 | ||
657 | static int scrollMax_DocumentWidget_(const iDocumentWidget *d) { | 674 | static int scrollMax_DocumentView_(const iDocumentView *d) { |
658 | const iWidget *w = constAs_Widget(d); | 675 | const iWidget *w = constAs_Widget(d->owner); |
659 | int sm = pageHeight_DocumentWidget_(d) - height_Rect(bounds_Widget(w)) + | 676 | int sm = pageHeight_DocumentView_(d) + |
660 | (isEmpty_Banner(d->banner) ? 2 : 1) * d->pageMargin * gap_UI + /* top and bottom margins */ | 677 | (isEmpty_Banner(d->owner->banner) ? 2 : 1) * d->pageMargin * gap_UI + /* top and bottom margins */ |
661 | iMax(height_Widget(d->phoneToolbar), height_Widget(d->footerButtons)); | 678 | footerHeight_DocumentWidget_(d->owner) - height_Rect(bounds_Widget(w)); |
662 | return sm; | 679 | return sm; |
663 | } | 680 | } |
664 | 681 | ||
665 | static void invalidateLink_DocumentWidget_(iDocumentWidget *d, iGmLinkId id) { | 682 | static void invalidateLink_DocumentView_(iDocumentView *d, iGmLinkId id) { |
666 | /* A link has multiple runs associated with it. */ | 683 | /* A link has multiple runs associated with it. */ |
667 | iConstForEach(PtrArray, i, &d->visibleLinks) { | 684 | iConstForEach(PtrArray, i, &d->visibleLinks) { |
668 | const iGmRun *run = i.ptr; | 685 | const iGmRun *run = i.ptr; |
@@ -672,7 +689,7 @@ static void invalidateLink_DocumentWidget_(iDocumentWidget *d, iGmLinkId id) { | |||
672 | } | 689 | } |
673 | } | 690 | } |
674 | 691 | ||
675 | static void invalidateVisibleLinks_DocumentWidget_(iDocumentWidget *d) { | 692 | static void invalidateVisibleLinks_DocumentView_(iDocumentView *d) { |
676 | iConstForEach(PtrArray, i, &d->visibleLinks) { | 693 | iConstForEach(PtrArray, i, &d->visibleLinks) { |
677 | const iGmRun *run = i.ptr; | 694 | const iGmRun *run = i.ptr; |
678 | if (run->linkId) { | 695 | if (run->linkId) { |
@@ -681,7 +698,7 @@ static void invalidateVisibleLinks_DocumentWidget_(iDocumentWidget *d) { | |||
681 | } | 698 | } |
682 | } | 699 | } |
683 | 700 | ||
684 | static int runOffset_DocumentWidget_(const iDocumentWidget *d, const iGmRun *run) { | 701 | static int runOffset_DocumentView_(const iDocumentView *d, const iGmRun *run) { |
685 | if (preId_GmRun(run) && run->flags & wide_GmRunFlag) { | 702 | if (preId_GmRun(run) && run->flags & wide_GmRunFlag) { |
686 | if (d->animWideRunId == preId_GmRun(run)) { | 703 | if (d->animWideRunId == preId_GmRun(run)) { |
687 | return -value_Anim(&d->animWideRunOffset); | 704 | return -value_Anim(&d->animWideRunOffset); |
@@ -695,56 +712,24 @@ static int runOffset_DocumentWidget_(const iDocumentWidget *d, const iGmRun *run | |||
695 | return 0; | 712 | return 0; |
696 | } | 713 | } |
697 | 714 | ||
698 | static void invalidateWideRunsWithNonzeroOffset_DocumentWidget_(iDocumentWidget *d) { | 715 | static void invalidateWideRunsWithNonzeroOffset_DocumentView_(iDocumentView *d) { |
699 | iConstForEach(PtrArray, i, &d->visibleWideRuns) { | 716 | iConstForEach(PtrArray, i, &d->visibleWideRuns) { |
700 | const iGmRun *run = i.ptr; | 717 | const iGmRun *run = i.ptr; |
701 | if (runOffset_DocumentWidget_(d, run)) { | 718 | if (runOffset_DocumentView_(d, run)) { |
702 | insert_PtrSet(d->invalidRuns, run); | 719 | insert_PtrSet(d->invalidRuns, run); |
703 | } | 720 | } |
704 | } | 721 | } |
705 | } | 722 | } |
706 | 723 | ||
707 | static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse); | 724 | static void updateHover_DocumentView_(iDocumentView *d, iInt2 mouse) { |
708 | 725 | const iWidget *w = constAs_Widget(d->owner); | |
709 | static void animate_DocumentWidget_(void *ticker) { | 726 | const iRect docBounds = documentBounds_DocumentView_(d); |
710 | iDocumentWidget *d = ticker; | ||
711 | refresh_Widget(d); | ||
712 | if (!isFinished_Anim(&d->sideOpacity) || !isFinished_Anim(&d->altTextOpacity)) { | ||
713 | addTicker_App(animate_DocumentWidget_, d); | ||
714 | } | ||
715 | } | ||
716 | |||
717 | static iBool isHoverAllowed_DocumentWidget_(const iDocumentWidget *d) { | ||
718 | if (!isHover_Widget(d)) { | ||
719 | return iFalse; | ||
720 | } | ||
721 | if (!(d->state == ready_RequestState || d->state == receivedPartialResponse_RequestState)) { | ||
722 | return iFalse; | ||
723 | } | ||
724 | if (d->flags & noHoverWhileScrolling_DocumentWidgetFlag) { | ||
725 | return iFalse; | ||
726 | } | ||
727 | if (d->flags & pinchZoom_DocumentWidgetFlag) { | ||
728 | return iFalse; | ||
729 | } | ||
730 | if (flags_Widget(constAs_Widget(d)) & touchDrag_WidgetFlag) { | ||
731 | return iFalse; | ||
732 | } | ||
733 | if (flags_Widget(constAs_Widget(d->scroll)) & pressed_WidgetFlag) { | ||
734 | return iFalse; | ||
735 | } | ||
736 | return iTrue; | ||
737 | } | ||
738 | |||
739 | static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) { | ||
740 | const iWidget *w = constAs_Widget(d); | ||
741 | const iRect docBounds = documentBounds_DocumentWidget_(d); | ||
742 | const iGmRun * oldHoverLink = d->hoverLink; | 727 | const iGmRun * oldHoverLink = d->hoverLink; |
743 | d->hoverPre = NULL; | 728 | d->hoverPre = NULL; |
744 | d->hoverLink = NULL; | 729 | d->hoverLink = NULL; |
745 | const iInt2 hoverPos = addY_I2(sub_I2(mouse, topLeft_Rect(docBounds)), | 730 | const iInt2 hoverPos = addY_I2(sub_I2(mouse, topLeft_Rect(docBounds)), |
746 | -viewPos_DocumentWidget_(d)); | 731 | -viewPos_DocumentView_(d)); |
747 | if (isHoverAllowed_DocumentWidget_(d)) { | 732 | if (isHoverAllowed_DocumentWidget_(d->owner)) { |
748 | iConstForEach(PtrArray, i, &d->visibleLinks) { | 733 | iConstForEach(PtrArray, i, &d->visibleLinks) { |
749 | const iGmRun *run = i.ptr; | 734 | const iGmRun *run = i.ptr; |
750 | /* Click targets are slightly expanded so there are no gaps between links. */ | 735 | /* Click targets are slightly expanded so there are no gaps between links. */ |
@@ -756,15 +741,21 @@ static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) { | |||
756 | } | 741 | } |
757 | if (d->hoverLink != oldHoverLink) { | 742 | if (d->hoverLink != oldHoverLink) { |
758 | if (oldHoverLink) { | 743 | if (oldHoverLink) { |
759 | invalidateLink_DocumentWidget_(d, oldHoverLink->linkId); | 744 | invalidateLink_DocumentView_(d, oldHoverLink->linkId); |
760 | } | 745 | } |
761 | if (d->hoverLink) { | 746 | if (d->hoverLink) { |
762 | invalidateLink_DocumentWidget_(d, d->hoverLink->linkId); | 747 | invalidateLink_DocumentView_(d, d->hoverLink->linkId); |
748 | } | ||
749 | if (update_LinkInfo(d->owner->linkInfo, | ||
750 | d->doc, | ||
751 | d->hoverLink ? d->hoverLink->linkId : 0, | ||
752 | width_Widget(w))) { | ||
753 | animate_DocumentWidget_(d->owner); | ||
763 | } | 754 | } |
764 | refresh_Widget(w); | 755 | refresh_Widget(w); |
765 | } | 756 | } |
766 | /* Hovering over preformatted blocks. */ | 757 | /* Hovering over preformatted blocks. */ |
767 | if (isHoverAllowed_DocumentWidget_(d)) { | 758 | if (isHoverAllowed_DocumentWidget_(d->owner)) { |
768 | iConstForEach(PtrArray, j, &d->visiblePre) { | 759 | iConstForEach(PtrArray, j, &d->visiblePre) { |
769 | const iGmRun *run = j.ptr; | 760 | const iGmRun *run = j.ptr; |
770 | if (contains_Rect(run->bounds, hoverPos)) { | 761 | if (contains_Rect(run->bounds, hoverPos)) { |
@@ -777,18 +768,18 @@ static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) { | |||
777 | if (!d->hoverPre) { | 768 | if (!d->hoverPre) { |
778 | setValueSpeed_Anim(&d->altTextOpacity, 0.0f, 1.5f); | 769 | setValueSpeed_Anim(&d->altTextOpacity, 0.0f, 1.5f); |
779 | if (!isFinished_Anim(&d->altTextOpacity)) { | 770 | if (!isFinished_Anim(&d->altTextOpacity)) { |
780 | animate_DocumentWidget_(d); | 771 | animate_DocumentWidget_(d->owner); |
781 | } | 772 | } |
782 | } | 773 | } |
783 | else if (d->hoverPre && | 774 | else if (d->hoverPre && |
784 | preHasAltText_GmDocument(d->doc, preId_GmRun(d->hoverPre)) && | 775 | preHasAltText_GmDocument(d->doc, preId_GmRun(d->hoverPre)) && |
785 | ~d->flags & noHoverWhileScrolling_DocumentWidgetFlag) { | 776 | ~d->owner->flags & noHoverWhileScrolling_DocumentWidgetFlag) { |
786 | setValueSpeed_Anim(&d->altTextOpacity, 1.0f, 1.5f); | 777 | setValueSpeed_Anim(&d->altTextOpacity, 1.0f, 1.5f); |
787 | if (!isFinished_Anim(&d->altTextOpacity)) { | 778 | if (!isFinished_Anim(&d->altTextOpacity)) { |
788 | animate_DocumentWidget_(d); | 779 | animate_DocumentWidget_(d->owner); |
789 | } | 780 | } |
790 | } | 781 | } |
791 | if (isHover_Widget(w) && !contains_Widget(constAs_Widget(d->scroll), mouse)) { | 782 | if (isHover_Widget(w) && !contains_Widget(constAs_Widget(d->owner->scroll), mouse)) { |
792 | setCursor_Window(get_Window(), | 783 | setCursor_Window(get_Window(), |
793 | d->hoverLink || d->hoverPre ? SDL_SYSTEM_CURSOR_HAND | 784 | d->hoverLink || d->hoverPre ? SDL_SYSTEM_CURSOR_HAND |
794 | : SDL_SYSTEM_CURSOR_IBEAM); | 785 | : SDL_SYSTEM_CURSOR_IBEAM); |
@@ -799,15 +790,1198 @@ static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) { | |||
799 | } | 790 | } |
800 | } | 791 | } |
801 | 792 | ||
802 | static void updateSideOpacity_DocumentWidget_(iDocumentWidget *d, iBool isAnimated) { | 793 | static void updateSideOpacity_DocumentView_(iDocumentView *d, iBool isAnimated) { |
803 | float opacity = 0.0f; | 794 | float opacity = 0.0f; |
804 | // const iGmRun *banner = siteBanner_GmDocument(d->doc); | 795 | if (!isEmpty_Banner(d->owner->banner) && |
805 | if (!isEmpty_Banner(d->banner) && height_Banner(d->banner) < pos_SmoothScroll(&d->scrollY)) { | 796 | height_Banner(d->owner->banner) < pos_SmoothScroll(&d->scrollY)) { |
806 | // if (banner && bottom_Rect(banner->visBounds) < pos_SmoothScroll(&d->scrollY)) { | ||
807 | opacity = 1.0f; | 797 | opacity = 1.0f; |
808 | } | 798 | } |
809 | setValue_Anim(&d->sideOpacity, opacity, isAnimated ? (opacity < 0.5f ? 100 : 200) : 0); | 799 | setValue_Anim(&d->sideOpacity, opacity, isAnimated ? (opacity < 0.5f ? 100 : 200) : 0); |
810 | animate_DocumentWidget_(d); | 800 | animate_DocumentWidget_(d->owner); |
801 | } | ||
802 | |||
803 | static iRangecc currentHeading_DocumentView_(const iDocumentView *d) { | ||
804 | iRangecc heading = iNullRange; | ||
805 | if (d->visibleRuns.start) { | ||
806 | iConstForEach(Array, i, headings_GmDocument(d->doc)) { | ||
807 | const iGmHeading *head = i.value; | ||
808 | if (head->level == 0) { | ||
809 | if (head->text.start <= d->visibleRuns.start->text.start) { | ||
810 | heading = head->text; | ||
811 | } | ||
812 | if (d->visibleRuns.end && head->text.start > d->visibleRuns.end->text.start) { | ||
813 | break; | ||
814 | } | ||
815 | } | ||
816 | } | ||
817 | } | ||
818 | return heading; | ||
819 | } | ||
820 | |||
821 | static int updateScrollMax_DocumentView_(iDocumentView *d) { | ||
822 | arrange_Widget(d->owner->footerButtons); /* scrollMax depends on footer height */ | ||
823 | const int scrollMax = scrollMax_DocumentView_(d); | ||
824 | setMax_SmoothScroll(&d->scrollY, scrollMax); | ||
825 | return scrollMax; | ||
826 | } | ||
827 | |||
828 | static void updateVisible_DocumentView_(iDocumentView *d) { | ||
829 | /* TODO: The concerns of Widget and View are too tangled together here. */ | ||
830 | iChangeFlags(d->owner->flags, | ||
831 | centerVertically_DocumentWidgetFlag, | ||
832 | prefs_App()->centerShortDocs || startsWithCase_String(d->owner->mod.url, "about:") || | ||
833 | !isSuccess_GmStatusCode(d->owner->sourceStatus)); | ||
834 | iScrollWidget *scrollBar = d->owner->scroll; | ||
835 | const iRangei visRange = visibleRange_DocumentView_(d); | ||
836 | // printf("visRange: %d...%d\n", visRange.start, visRange.end); | ||
837 | const iRect bounds = bounds_Widget(as_Widget(d->owner)); | ||
838 | const int scrollMax = updateScrollMax_DocumentView_(d); | ||
839 | /* Reposition the footer buttons as appropriate. */ | ||
840 | setRange_ScrollWidget(scrollBar, (iRangei){ 0, scrollMax }); | ||
841 | const int docSize = pageHeight_DocumentView_(d) + footerHeight_DocumentWidget_(d->owner); | ||
842 | const float scrollPos = pos_SmoothScroll(&d->scrollY); | ||
843 | setThumb_ScrollWidget(scrollBar, | ||
844 | pos_SmoothScroll(&d->scrollY), | ||
845 | docSize > 0 ? height_Rect(bounds) * size_Range(&visRange) / docSize : 0); | ||
846 | if (d->owner->footerButtons) { | ||
847 | const iRect bounds = bounds_Widget(as_Widget(d->owner)); | ||
848 | const iRect docBounds = documentBounds_DocumentView_(d); | ||
849 | const int hPad = (width_Rect(bounds) - iMin(120 * gap_UI, width_Rect(docBounds))) / 2; | ||
850 | const int vPad = 3 * gap_UI; | ||
851 | setPadding_Widget(d->owner->footerButtons, hPad, 0, hPad, vPad); | ||
852 | d->owner->footerButtons->rect.pos.y = height_Rect(bounds) - | ||
853 | footerHeight_DocumentWidget_(d->owner) + | ||
854 | (scrollMax > 0 ? scrollMax - scrollPos : 0); | ||
855 | } | ||
856 | clear_PtrArray(&d->visibleLinks); | ||
857 | clear_PtrArray(&d->visibleWideRuns); | ||
858 | clear_PtrArray(&d->visiblePre); | ||
859 | clear_PtrArray(&d->visibleMedia); | ||
860 | const iRangecc oldHeading = currentHeading_DocumentView_(d); | ||
861 | /* Scan for visible runs. */ { | ||
862 | iZap(d->visibleRuns); | ||
863 | render_GmDocument(d->doc, visRange, addVisible_DocumentView_, d); | ||
864 | } | ||
865 | const iRangecc newHeading = currentHeading_DocumentView_(d); | ||
866 | if (memcmp(&oldHeading, &newHeading, sizeof(oldHeading))) { | ||
867 | d->drawBufs->flags |= updateSideBuf_DrawBufsFlag; | ||
868 | } | ||
869 | updateHover_DocumentView_(d, mouseCoord_Window(get_Window(), 0)); | ||
870 | updateSideOpacity_DocumentView_(d, iTrue); | ||
871 | animateMedia_DocumentWidget_(d->owner); | ||
872 | /* Remember scroll positions of recently visited pages. */ { | ||
873 | iRecentUrl *recent = mostRecentUrl_History(d->owner->mod.history); | ||
874 | if (recent && docSize && d->owner->state == ready_RequestState && | ||
875 | equal_String(&recent->url, d->owner->mod.url)) { | ||
876 | recent->normScrollY = normScrollPos_DocumentView_(d); | ||
877 | } | ||
878 | } | ||
879 | /* After scrolling/resizing stops, begin pre-rendering the visbuf contents. */ { | ||
880 | removeTicker_App(prerender_DocumentWidget_, d->owner); | ||
881 | remove_Periodic(periodic_App(), d); | ||
882 | add_Periodic(periodic_App(), d->owner, "document.render"); | ||
883 | } | ||
884 | } | ||
885 | |||
886 | static void swap_DocumentView_(iDocumentView *d, iDocumentView *swapBuffersWith) { | ||
887 | d->scrollY = swapBuffersWith->scrollY; | ||
888 | d->scrollY.widget = as_Widget(d->owner); | ||
889 | iSwap(iVisBuf *, d->visBuf, swapBuffersWith->visBuf); | ||
890 | iSwap(iVisBufMeta *, d->visBufMeta, swapBuffersWith->visBufMeta); | ||
891 | iSwap(iDrawBufs *, d->drawBufs, swapBuffersWith->drawBufs); | ||
892 | updateVisible_DocumentView_(d); | ||
893 | updateVisible_DocumentView_(swapBuffersWith); | ||
894 | } | ||
895 | |||
896 | static void updateTimestampBuf_DocumentView_(const iDocumentView *d) { | ||
897 | if (!isExposed_Window(get_Window())) { | ||
898 | return; | ||
899 | } | ||
900 | if (d->drawBufs->timestampBuf) { | ||
901 | delete_TextBuf(d->drawBufs->timestampBuf); | ||
902 | d->drawBufs->timestampBuf = NULL; | ||
903 | } | ||
904 | if (isValid_Time(&d->owner->sourceTime)) { | ||
905 | iString *fmt = timeFormatHourPreference_Lang("page.timestamp"); | ||
906 | d->drawBufs->timestampBuf = newRange_TextBuf( | ||
907 | uiLabel_FontId, | ||
908 | white_ColorId, | ||
909 | range_String(collect_String(format_Time(&d->owner->sourceTime, cstr_String(fmt))))); | ||
910 | delete_String(fmt); | ||
911 | } | ||
912 | d->drawBufs->flags &= ~updateTimestampBuf_DrawBufsFlag; | ||
913 | } | ||
914 | |||
915 | static void invalidate_DocumentView_(iDocumentView *d) { | ||
916 | invalidate_VisBuf(d->visBuf); | ||
917 | clear_PtrSet(d->invalidRuns); | ||
918 | } | ||
919 | |||
920 | static void documentRunsInvalidated_DocumentView_(iDocumentView *d) { | ||
921 | d->hoverPre = NULL; | ||
922 | d->hoverAltPre = NULL; | ||
923 | d->hoverLink = NULL; | ||
924 | iZap(d->visibleRuns); | ||
925 | iZap(d->renderRuns); | ||
926 | } | ||
927 | |||
928 | static void resetScroll_DocumentView_(iDocumentView *d) { | ||
929 | reset_SmoothScroll(&d->scrollY); | ||
930 | init_Anim(&d->sideOpacity, 0); | ||
931 | init_Anim(&d->altTextOpacity, 0); | ||
932 | resetWideRuns_DocumentView_(d); | ||
933 | } | ||
934 | |||
935 | static void updateWidth_DocumentView_(iDocumentView *d) { | ||
936 | updateWidth_GmDocument(d->doc, documentWidth_DocumentView_(d), width_Widget(d->owner)); | ||
937 | } | ||
938 | |||
939 | static void updateWidthAndRedoLayout_DocumentView_(iDocumentView *d) { | ||
940 | setWidth_GmDocument(d->doc, documentWidth_DocumentView_(d), width_Widget(d->owner)); | ||
941 | } | ||
942 | |||
943 | static void clampScroll_DocumentView_(iDocumentView *d) { | ||
944 | move_SmoothScroll(&d->scrollY, 0); | ||
945 | } | ||
946 | |||
947 | static void immediateScroll_DocumentView_(iDocumentView *d, int offset) { | ||
948 | move_SmoothScroll(&d->scrollY, offset); | ||
949 | } | ||
950 | |||
951 | static void smoothScroll_DocumentView_(iDocumentView *d, int offset, int duration) { | ||
952 | moveSpan_SmoothScroll(&d->scrollY, offset, duration); | ||
953 | } | ||
954 | |||
955 | static void scrollTo_DocumentView_(iDocumentView *d, int documentY, iBool centered) { | ||
956 | if (!isEmpty_Banner(d->owner->banner)) { | ||
957 | documentY += height_Banner(d->owner->banner) + documentTopPad_DocumentView_(d); | ||
958 | } | ||
959 | else { | ||
960 | documentY += documentTopPad_DocumentView_(d) + d->pageMargin * gap_UI; | ||
961 | } | ||
962 | init_Anim(&d->scrollY.pos, | ||
963 | documentY - (centered ? documentBounds_DocumentView_(d).size.y / 2 | ||
964 | : lineHeight_Text(paragraph_FontId))); | ||
965 | clampScroll_DocumentView_(d); | ||
966 | } | ||
967 | |||
968 | static void scrollToHeading_DocumentView_(iDocumentView *d, const char *heading) { | ||
969 | iConstForEach(Array, h, headings_GmDocument(d->doc)) { | ||
970 | const iGmHeading *head = h.value; | ||
971 | if (startsWithCase_Rangecc(head->text, heading)) { | ||
972 | postCommandf_Root(as_Widget(d->owner)->root, "document.goto loc:%p", head->text.start); | ||
973 | break; | ||
974 | } | ||
975 | } | ||
976 | } | ||
977 | |||
978 | static iBool scrollWideBlock_DocumentView_(iDocumentView *d, iInt2 mousePos, int delta, | ||
979 | int duration) { | ||
980 | if (delta == 0 || d->owner->flags & eitherWheelSwipe_DocumentWidgetFlag) { | ||
981 | return iFalse; | ||
982 | } | ||
983 | const iInt2 docPos = documentPos_DocumentView_(d, mousePos); | ||
984 | iConstForEach(PtrArray, i, &d->visibleWideRuns) { | ||
985 | const iGmRun *run = i.ptr; | ||
986 | if (docPos.y >= top_Rect(run->bounds) && docPos.y <= bottom_Rect(run->bounds)) { | ||
987 | /* We can scroll this run. First find out how much is allowed. */ | ||
988 | const iGmRunRange range = findPreformattedRange_GmDocument(d->doc, run); | ||
989 | int maxWidth = 0; | ||
990 | for (const iGmRun *r = range.start; r != range.end; r++) { | ||
991 | maxWidth = iMax(maxWidth, width_Rect(r->visBounds)); | ||
992 | } | ||
993 | const int maxOffset = maxWidth - documentWidth_DocumentView_(d) + d->pageMargin * gap_UI; | ||
994 | if (size_Array(&d->wideRunOffsets) <= preId_GmRun(run)) { | ||
995 | resize_Array(&d->wideRunOffsets, preId_GmRun(run) + 1); | ||
996 | } | ||
997 | int *offset = at_Array(&d->wideRunOffsets, preId_GmRun(run) - 1); | ||
998 | const int oldOffset = *offset; | ||
999 | *offset = iClamp(*offset + delta, 0, maxOffset); | ||
1000 | /* Make sure the whole block gets redraw. */ | ||
1001 | if (oldOffset != *offset) { | ||
1002 | for (const iGmRun *r = range.start; r != range.end; r++) { | ||
1003 | insert_PtrSet(d->invalidRuns, r); | ||
1004 | } | ||
1005 | refresh_Widget(d->owner); | ||
1006 | d->owner->selectMark = iNullRange; | ||
1007 | d->owner->foundMark = iNullRange; | ||
1008 | } | ||
1009 | if (duration) { | ||
1010 | if (d->animWideRunId != preId_GmRun(run) || isFinished_Anim(&d->animWideRunOffset)) { | ||
1011 | d->animWideRunId = preId_GmRun(run); | ||
1012 | init_Anim(&d->animWideRunOffset, oldOffset); | ||
1013 | } | ||
1014 | setValueEased_Anim(&d->animWideRunOffset, *offset, duration); | ||
1015 | d->animWideRunRange = range; | ||
1016 | addTicker_App(refreshWhileScrolling_DocumentWidget_, d->owner); | ||
1017 | } | ||
1018 | else { | ||
1019 | d->animWideRunId = 0; | ||
1020 | init_Anim(&d->animWideRunOffset, 0); | ||
1021 | } | ||
1022 | return iTrue; | ||
1023 | } | ||
1024 | } | ||
1025 | return iFalse; | ||
1026 | } | ||
1027 | |||
1028 | static iRangecc sourceLoc_DocumentView_(const iDocumentView *d, iInt2 pos) { | ||
1029 | return findLoc_GmDocument(d->doc, documentPos_DocumentView_(d, pos)); | ||
1030 | } | ||
1031 | |||
1032 | iDeclareType(MiddleRunParams) | ||
1033 | |||
1034 | struct Impl_MiddleRunParams { | ||
1035 | int midY; | ||
1036 | const iGmRun *closest; | ||
1037 | int distance; | ||
1038 | }; | ||
1039 | |||
1040 | static void find_MiddleRunParams_(void *params, const iGmRun *run) { | ||
1041 | iMiddleRunParams *d = params; | ||
1042 | if (isEmpty_Rect(run->bounds)) { | ||
1043 | return; | ||
1044 | } | ||
1045 | const int distance = iAbs(mid_Rect(run->bounds).y - d->midY); | ||
1046 | if (!d->closest || distance < d->distance) { | ||
1047 | d->closest = run; | ||
1048 | d->distance = distance; | ||
1049 | } | ||
1050 | } | ||
1051 | |||
1052 | static const iGmRun *middleRun_DocumentView_(const iDocumentView *d) { | ||
1053 | iRangei visRange = visibleRange_DocumentView_(d); | ||
1054 | iMiddleRunParams params = { (visRange.start + visRange.end) / 2, NULL, 0 }; | ||
1055 | render_GmDocument(d->doc, visRange, find_MiddleRunParams_, ¶ms); | ||
1056 | return params.closest; | ||
1057 | } | ||
1058 | |||
1059 | static void allocVisBuffer_DocumentView_(const iDocumentView *d) { | ||
1060 | const iWidget *w = constAs_Widget(d->owner); | ||
1061 | const iBool isVisible = isVisible_Widget(w); | ||
1062 | const iInt2 size = bounds_Widget(w).size; | ||
1063 | if (isVisible) { | ||
1064 | alloc_VisBuf(d->visBuf, size, 1); | ||
1065 | } | ||
1066 | else { | ||
1067 | dealloc_VisBuf(d->visBuf); | ||
1068 | } | ||
1069 | } | ||
1070 | |||
1071 | static size_t visibleLinkOrdinal_DocumentView_(const iDocumentView *d, iGmLinkId linkId) { | ||
1072 | size_t ord = 0; | ||
1073 | const iRangei visRange = visibleRange_DocumentView_(d); | ||
1074 | iConstForEach(PtrArray, i, &d->visibleLinks) { | ||
1075 | const iGmRun *run = i.ptr; | ||
1076 | if (top_Rect(run->visBounds) >= visRange.start + gap_UI * d->pageMargin * 4 / 5) { | ||
1077 | if (run->flags & decoration_GmRunFlag && run->linkId) { | ||
1078 | if (run->linkId == linkId) return ord; | ||
1079 | ord++; | ||
1080 | } | ||
1081 | } | ||
1082 | } | ||
1083 | return iInvalidPos; | ||
1084 | } | ||
1085 | |||
1086 | static void documentRunsInvalidated_DocumentWidget_(iDocumentWidget *d) { | ||
1087 | d->foundMark = iNullRange; | ||
1088 | d->selectMark = iNullRange; | ||
1089 | d->contextLink = NULL; | ||
1090 | documentRunsInvalidated_DocumentView_(&d->view); | ||
1091 | } | ||
1092 | |||
1093 | static iBool updateDocumentWidthRetainingScrollPosition_DocumentView_(iDocumentView *d, | ||
1094 | iBool keepCenter) { | ||
1095 | const int newWidth = documentWidth_DocumentView_(d); | ||
1096 | if (newWidth == size_GmDocument(d->doc).x && !keepCenter /* not a font change */) { | ||
1097 | return iFalse; | ||
1098 | } | ||
1099 | /* Font changes (i.e., zooming) will keep the view centered, otherwise keep the top | ||
1100 | of the visible area fixed. */ | ||
1101 | const iGmRun *run = keepCenter ? middleRun_DocumentView_(d) : d->visibleRuns.start; | ||
1102 | const char * runLoc = (run ? run->text.start : NULL); | ||
1103 | int voffset = 0; | ||
1104 | if (!keepCenter && run) { | ||
1105 | /* Keep the first visible run visible at the same position. */ | ||
1106 | /* TODO: First *fully* visible run? */ | ||
1107 | voffset = visibleRange_DocumentView_(d).start - top_Rect(run->visBounds); | ||
1108 | } | ||
1109 | setWidth_GmDocument(d->doc, newWidth, width_Widget(d->owner)); | ||
1110 | setWidth_Banner(d->owner->banner, newWidth); | ||
1111 | documentRunsInvalidated_DocumentWidget_(d->owner); | ||
1112 | if (runLoc && !keepCenter) { | ||
1113 | run = findRunAtLoc_GmDocument(d->doc, runLoc); | ||
1114 | if (run) { | ||
1115 | scrollTo_DocumentView_( | ||
1116 | d, top_Rect(run->visBounds) + lineHeight_Text(paragraph_FontId) + voffset, iFalse); | ||
1117 | } | ||
1118 | } | ||
1119 | else if (runLoc && keepCenter) { | ||
1120 | run = findRunAtLoc_GmDocument(d->doc, runLoc); | ||
1121 | if (run) { | ||
1122 | scrollTo_DocumentView_(d, mid_Rect(run->bounds).y, iTrue); | ||
1123 | } | ||
1124 | } | ||
1125 | return iTrue; | ||
1126 | } | ||
1127 | |||
1128 | static iRect runRect_DocumentView_(const iDocumentView *d, const iGmRun *run) { | ||
1129 | const iRect docBounds = documentBounds_DocumentView_(d); | ||
1130 | return moved_Rect(run->bounds, addY_I2(topLeft_Rect(docBounds), viewPos_DocumentView_(d))); | ||
1131 | } | ||
1132 | |||
1133 | iDeclareType(DrawContext) | ||
1134 | |||
1135 | struct Impl_DrawContext { | ||
1136 | const iDocumentView *view; | ||
1137 | iRect widgetBounds; | ||
1138 | iRect docBounds; | ||
1139 | iRangei vis; | ||
1140 | iInt2 viewPos; /* document area origin */ | ||
1141 | iPaint paint; | ||
1142 | iBool inSelectMark; | ||
1143 | iBool inFoundMark; | ||
1144 | iBool showLinkNumbers; | ||
1145 | iRect firstMarkRect; | ||
1146 | iRect lastMarkRect; | ||
1147 | iGmRunRange runsDrawn; | ||
1148 | }; | ||
1149 | |||
1150 | static void fillRange_DrawContext_(iDrawContext *d, const iGmRun *run, enum iColorId color, | ||
1151 | iRangecc mark, iBool *isInside) { | ||
1152 | if (mark.start > mark.end) { | ||
1153 | /* Selection may be done in either direction. */ | ||
1154 | iSwap(const char *, mark.start, mark.end); | ||
1155 | } | ||
1156 | if (*isInside || (contains_Range(&run->text, mark.start) || | ||
1157 | contains_Range(&mark, run->text.start))) { | ||
1158 | int x = 0; | ||
1159 | if (!*isInside) { | ||
1160 | x = measureRange_Text(run->font, | ||
1161 | (iRangecc){ run->text.start, iMax(run->text.start, mark.start) }) | ||
1162 | .advance.x; | ||
1163 | } | ||
1164 | int w = width_Rect(run->visBounds) - x; | ||
1165 | if (contains_Range(&run->text, mark.end) || mark.end < run->text.start) { | ||
1166 | iRangecc mk = !*isInside ? mark | ||
1167 | : (iRangecc){ run->text.start, iMax(run->text.start, mark.end) }; | ||
1168 | mk.start = iMax(mk.start, run->text.start); | ||
1169 | w = measureRange_Text(run->font, mk).advance.x; | ||
1170 | *isInside = iFalse; | ||
1171 | } | ||
1172 | else { | ||
1173 | *isInside = iTrue; /* at least until the next run */ | ||
1174 | } | ||
1175 | if (w > width_Rect(run->visBounds) - x) { | ||
1176 | w = width_Rect(run->visBounds) - x; | ||
1177 | } | ||
1178 | if (~run->flags & decoration_GmRunFlag) { | ||
1179 | const iInt2 visPos = | ||
1180 | add_I2(run->bounds.pos, addY_I2(d->viewPos, viewPos_DocumentView_(d->view))); | ||
1181 | const iRect rangeRect = { addX_I2(visPos, x), init_I2(w, height_Rect(run->bounds)) }; | ||
1182 | if (rangeRect.size.x) { | ||
1183 | fillRect_Paint(&d->paint, rangeRect, color); | ||
1184 | /* Keep track of the first and last marked rects. */ | ||
1185 | if (d->firstMarkRect.size.x == 0) { | ||
1186 | d->firstMarkRect = rangeRect; | ||
1187 | } | ||
1188 | d->lastMarkRect = rangeRect; | ||
1189 | } | ||
1190 | } | ||
1191 | } | ||
1192 | /* Link URLs are not part of the visible document, so they are ignored above. Handle | ||
1193 | these ranges as a special case. */ | ||
1194 | if (run->linkId && run->flags & decoration_GmRunFlag) { | ||
1195 | const iRangecc url = linkUrlRange_GmDocument(d->view->doc, run->linkId); | ||
1196 | if (contains_Range(&url, mark.start) && | ||
1197 | (contains_Range(&url, mark.end) || url.end == mark.end)) { | ||
1198 | fillRect_Paint( | ||
1199 | &d->paint, | ||
1200 | moved_Rect(run->visBounds, addY_I2(d->viewPos, viewPos_DocumentView_(d->view))), | ||
1201 | color); | ||
1202 | } | ||
1203 | } | ||
1204 | } | ||
1205 | |||
1206 | static void drawMark_DrawContext_(void *context, const iGmRun *run) { | ||
1207 | iDrawContext *d = context; | ||
1208 | if (!isMedia_GmRun(run)) { | ||
1209 | fillRange_DrawContext_(d, run, uiMatching_ColorId, d->view->owner->foundMark, &d->inFoundMark); | ||
1210 | fillRange_DrawContext_(d, run, uiMarked_ColorId, d->view->owner->selectMark, &d->inSelectMark); | ||
1211 | } | ||
1212 | } | ||
1213 | |||
1214 | static void drawRun_DrawContext_(void *context, const iGmRun *run) { | ||
1215 | iDrawContext *d = context; | ||
1216 | const iInt2 origin = d->viewPos; | ||
1217 | /* Keep track of the drawn visible runs. */ { | ||
1218 | if (!d->runsDrawn.start || run < d->runsDrawn.start) { | ||
1219 | d->runsDrawn.start = run; | ||
1220 | } | ||
1221 | if (!d->runsDrawn.end || run > d->runsDrawn.end) { | ||
1222 | d->runsDrawn.end = run; | ||
1223 | } | ||
1224 | } | ||
1225 | if (run->mediaType == image_MediaType) { | ||
1226 | SDL_Texture *tex = imageTexture_Media(media_GmDocument(d->view->doc), mediaId_GmRun(run)); | ||
1227 | const iRect dst = moved_Rect(run->visBounds, origin); | ||
1228 | if (tex) { | ||
1229 | fillRect_Paint(&d->paint, dst, tmBackground_ColorId); /* in case the image has alpha */ | ||
1230 | SDL_RenderCopy(d->paint.dst->render, tex, NULL, | ||
1231 | &(SDL_Rect){ dst.pos.x, dst.pos.y, dst.size.x, dst.size.y }); | ||
1232 | } | ||
1233 | else { | ||
1234 | drawRect_Paint(&d->paint, dst, tmQuoteIcon_ColorId); | ||
1235 | drawCentered_Text(uiLabel_FontId, | ||
1236 | dst, | ||
1237 | iFalse, | ||
1238 | tmQuote_ColorId, | ||
1239 | explosion_Icon " Error Loading Image"); | ||
1240 | } | ||
1241 | return; | ||
1242 | } | ||
1243 | else if (isMedia_GmRun(run)) { | ||
1244 | /* Media UIs are drawn afterwards as a dynamic overlay. */ | ||
1245 | return; | ||
1246 | } | ||
1247 | enum iColorId fg = run->color; | ||
1248 | const iGmDocument *doc = d->view->doc; | ||
1249 | const int linkFlags = linkFlags_GmDocument(doc, run->linkId); | ||
1250 | /* Hover state of a link. */ | ||
1251 | const iBool isPartOfHover = (run->linkId && d->view->hoverLink && | ||
1252 | run->linkId == d->view->hoverLink->linkId); | ||
1253 | iBool isHover = (isPartOfHover && ~run->flags & decoration_GmRunFlag); | ||
1254 | /* Visible (scrolled) position of the run. */ | ||
1255 | const iInt2 visPos = addX_I2(add_I2(run->visBounds.pos, origin), | ||
1256 | /* Preformatted runs can be scrolled. */ | ||
1257 | runOffset_DocumentView_(d->view, run)); | ||
1258 | const iRect visRect = { visPos, run->visBounds.size }; | ||
1259 | /* Fill the background. */ { | ||
1260 | #if 0 | ||
1261 | iBool isInlineImageCaption = run->linkId && linkFlags & content_GmLinkFlag && | ||
1262 | ~linkFlags & permanent_GmLinkFlag; | ||
1263 | if (run->flags & decoration_GmRunFlag && ~run->flags & startOfLine_GmRunFlag) { | ||
1264 | /* This is the metadata. */ | ||
1265 | isInlineImageCaption = iFalse; | ||
1266 | } | ||
1267 | #endif | ||
1268 | iBool isMobileHover = deviceType_App() != desktop_AppDeviceType && | ||
1269 | (isPartOfHover || contains_PtrSet(d->view->invalidRuns, run)) && | ||
1270 | (~run->flags & decoration_GmRunFlag || run->flags & startOfLine_GmRunFlag | ||
1271 | /* highlight link icon but not image captions */); | ||
1272 | /* While this is consistent, it's a bit excessive to indicate that an inlined image | ||
1273 | is open: the image itself is the indication. */ | ||
1274 | const iBool isInlineImageCaption = iFalse; | ||
1275 | if (run->linkId && (linkFlags & isOpen_GmLinkFlag || isInlineImageCaption || isMobileHover)) { | ||
1276 | /* Open links get a highlighted background. */ | ||
1277 | int bg = tmBackgroundOpenLink_ColorId; | ||
1278 | if (isMobileHover && !isPartOfHover) { | ||
1279 | bg = tmBackground_ColorId; /* hover ended and was invalidated */ | ||
1280 | } | ||
1281 | // const int frame = tmFrameOpenLink_ColorId; | ||
1282 | const int pad = gap_Text; | ||
1283 | iRect wideRect = { init_I2(origin.x - pad, visPos.y), | ||
1284 | init_I2(d->docBounds.size.x + 2 * pad, | ||
1285 | height_Rect(run->visBounds)) }; | ||
1286 | adjustEdges_Rect(&wideRect, | ||
1287 | run->flags & startOfLine_GmRunFlag ? -pad * 3 / 4 : 0, 0, | ||
1288 | run->flags & endOfLine_GmRunFlag ? pad * 3 / 4 : 0, 0); | ||
1289 | /* The first line is composed of two runs that may be drawn in either order, so | ||
1290 | only draw half of the background. */ | ||
1291 | if (run->flags & decoration_GmRunFlag) { | ||
1292 | wideRect.size.x = right_Rect(visRect) - left_Rect(wideRect); | ||
1293 | } | ||
1294 | else if (run->flags & startOfLine_GmRunFlag) { | ||
1295 | wideRect.size.x = right_Rect(wideRect) - left_Rect(visRect); | ||
1296 | wideRect.pos.x = left_Rect(visRect); | ||
1297 | } | ||
1298 | fillRect_Paint(&d->paint, wideRect, bg); | ||
1299 | } | ||
1300 | else { | ||
1301 | /* Normal background for other runs. There are cases when runs get drawn multiple times, | ||
1302 | e.g., at the buffer boundary, and there are slightly overlapping characters in | ||
1303 | monospace blocks. Clearing the background here ensures a cleaner visual appearance | ||
1304 | since only one glyph is visible at any given point. */ | ||
1305 | fillRect_Paint(&d->paint, visRect, tmBackground_ColorId); | ||
1306 | } | ||
1307 | } | ||
1308 | if (run->linkId) { | ||
1309 | if (run->flags & decoration_GmRunFlag && run->flags & startOfLine_GmRunFlag) { | ||
1310 | /* Link icon. */ | ||
1311 | if (linkFlags & content_GmLinkFlag) { | ||
1312 | fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart); | ||
1313 | } | ||
1314 | } | ||
1315 | else if (~run->flags & decoration_GmRunFlag) { | ||
1316 | fg = linkColor_GmDocument(doc, run->linkId, isHover ? textHover_GmLinkPart : text_GmLinkPart); | ||
1317 | if (linkFlags & content_GmLinkFlag) { | ||
1318 | fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart); /* link is inactive */ | ||
1319 | } | ||
1320 | } | ||
1321 | } | ||
1322 | if (run->flags & altText_GmRunFlag) { | ||
1323 | const iInt2 margin = preRunMargin_GmDocument(doc, preId_GmRun(run)); | ||
1324 | fillRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmBackgroundAltText_ColorId); | ||
1325 | drawRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmFrameAltText_ColorId); | ||
1326 | drawWrapRange_Text(run->font, | ||
1327 | add_I2(visPos, margin), | ||
1328 | run->visBounds.size.x - 2 * margin.x, | ||
1329 | run->color, | ||
1330 | run->text); | ||
1331 | } | ||
1332 | else { | ||
1333 | if (d->showLinkNumbers && run->linkId && run->flags & decoration_GmRunFlag) { | ||
1334 | const size_t ord = visibleLinkOrdinal_DocumentView_(d->view, run->linkId); | ||
1335 | if (ord >= d->view->owner->ordinalBase) { | ||
1336 | const iChar ordChar = | ||
1337 | linkOrdinalChar_DocumentWidget_(d->view->owner, ord - d->view->owner->ordinalBase); | ||
1338 | if (ordChar) { | ||
1339 | const char *circle = "\u25ef"; /* Large Circle */ | ||
1340 | const int circleFont = FONT_ID(default_FontId, regular_FontStyle, contentRegular_FontSize); | ||
1341 | iRect nbArea = { init_I2(d->viewPos.x - gap_UI / 3, visPos.y), | ||
1342 | init_I2(3.95f * gap_Text, 1.0f * lineHeight_Text(circleFont)) }; | ||
1343 | drawRange_Text( | ||
1344 | circleFont, topLeft_Rect(nbArea), tmQuote_ColorId, range_CStr(circle)); | ||
1345 | iRect circleArea = visualBounds_Text(circleFont, range_CStr(circle)); | ||
1346 | addv_I2(&circleArea.pos, topLeft_Rect(nbArea)); | ||
1347 | drawCentered_Text(FONT_ID(default_FontId, regular_FontStyle, contentSmall_FontSize), | ||
1348 | circleArea, | ||
1349 | iTrue, | ||
1350 | tmQuote_ColorId, | ||
1351 | "%lc", | ||
1352 | (int) ordChar); | ||
1353 | goto runDrawn; | ||
1354 | } | ||
1355 | } | ||
1356 | } | ||
1357 | if (run->flags & quoteBorder_GmRunFlag) { | ||
1358 | drawVLine_Paint(&d->paint, | ||
1359 | addX_I2(visPos, | ||
1360 | !run->isRTL | ||
1361 | ? -gap_Text * 5 / 2 | ||
1362 | : (width_Rect(run->visBounds) + gap_Text * 5 / 2)), | ||
1363 | height_Rect(run->visBounds), | ||
1364 | tmQuoteIcon_ColorId); | ||
1365 | } | ||
1366 | /* Base attributes. */ { | ||
1367 | int f, c; | ||
1368 | runBaseAttributes_GmDocument(doc, run, &f, &c); | ||
1369 | setBaseAttributes_Text(f, c); | ||
1370 | } | ||
1371 | /* Fancy date in Gemini feed links. */ { | ||
1372 | if (run->linkId && run->flags & startOfLine_GmRunFlag && ~run->flags & decoration_GmRunFlag) { | ||
1373 | static iRegExp *datePattern_; | ||
1374 | if (!datePattern_) { | ||
1375 | datePattern_ = new_RegExp("^[12][0-9][0-9][0-9]-[01][0-9]-[0-3][0-9]\\s", 0); | ||
1376 | } | ||
1377 | iRegExpMatch m; | ||
1378 | init_RegExpMatch(&m); | ||
1379 | if (matchRange_RegExp(datePattern_, run->text, &m)) { | ||
1380 | /* The date uses regular weight and a dimmed color. */ | ||
1381 | iString styled; | ||
1382 | initRange_String(&styled, run->text); | ||
1383 | insertData_Block(&styled.chars, 10, "\x1b[0m", 4); /* restore */ | ||
1384 | iBlock buf; | ||
1385 | init_Block(&buf, 0); | ||
1386 | appendCStr_Block(&buf, "\x1b[10m"); /* regular font weight */ | ||
1387 | appendCStr_Block(&buf, escape_Color(isHover ? fg : tmLinkFeedEntryDate_ColorId)); | ||
1388 | insertData_Block(&styled.chars, 0, constData_Block(&buf), size_Block(&buf)); | ||
1389 | deinit_Block(&buf); | ||
1390 | const int oldAnsi = ansiFlags_Text(); | ||
1391 | setAnsiFlags_Text(oldAnsi | allowFontStyle_AnsiFlag); | ||
1392 | setBaseAttributes_Text(run->font, fg); | ||
1393 | drawBoundRange_Text(run->font, | ||
1394 | visPos, | ||
1395 | (run->isRTL ? -1 : 1) * width_Rect(run->visBounds), | ||
1396 | fg, | ||
1397 | range_String(&styled)); | ||
1398 | setAnsiFlags_Text(oldAnsi); | ||
1399 | deinit_String(&styled); | ||
1400 | goto runDrawn; | ||
1401 | } | ||
1402 | } | ||
1403 | } | ||
1404 | drawBoundRange_Text(run->font, | ||
1405 | visPos, | ||
1406 | (run->isRTL ? -1 : 1) * width_Rect(run->visBounds), | ||
1407 | fg, | ||
1408 | run->text); | ||
1409 | runDrawn:; | ||
1410 | setBaseAttributes_Text(-1, -1); | ||
1411 | } | ||
1412 | /* Presentation of links. */ | ||
1413 | if (run->linkId && ~run->flags & decoration_GmRunFlag) { | ||
1414 | const int metaFont = paragraph_FontId; | ||
1415 | /* TODO: Show status of an ongoing media request. */ | ||
1416 | const int flags = linkFlags; | ||
1417 | const iRect linkRect = moved_Rect(run->visBounds, origin); | ||
1418 | iMediaRequest *mr = NULL; | ||
1419 | /* Show metadata about inline content. */ | ||
1420 | if (flags & content_GmLinkFlag && run->flags & endOfLine_GmRunFlag) { | ||
1421 | fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart); | ||
1422 | iString text; | ||
1423 | init_String(&text); | ||
1424 | const iMediaId linkMedia = findMediaForLink_Media(constMedia_GmDocument(doc), | ||
1425 | run->linkId, none_MediaType); | ||
1426 | iAssert(linkMedia.type != none_MediaType); | ||
1427 | iGmMediaInfo info; | ||
1428 | info_Media(constMedia_GmDocument(doc), linkMedia, &info); | ||
1429 | switch (linkMedia.type) { | ||
1430 | case image_MediaType: { | ||
1431 | /* There's a separate decorative GmRun for the metadata. */ | ||
1432 | break; | ||
1433 | } | ||
1434 | case audio_MediaType: | ||
1435 | format_String(&text, "%s", info.type); | ||
1436 | break; | ||
1437 | case download_MediaType: | ||
1438 | format_String(&text, "%s", info.type); | ||
1439 | break; | ||
1440 | default: | ||
1441 | break; | ||
1442 | } | ||
1443 | if (linkMedia.type != download_MediaType && /* can't cancel downloads currently */ | ||
1444 | linkMedia.type != image_MediaType && | ||
1445 | findMediaRequest_DocumentWidget_(d->view->owner, run->linkId)) { | ||
1446 | appendFormat_String( | ||
1447 | &text, " %s" close_Icon, isHover ? escape_Color(tmLinkText_ColorId) : ""); | ||
1448 | } | ||
1449 | const iInt2 size = measureRange_Text(metaFont, range_String(&text)).bounds.size; | ||
1450 | if (size.x) { | ||
1451 | fillRect_Paint( | ||
1452 | &d->paint, | ||
1453 | (iRect){ add_I2(origin, addX_I2(topRight_Rect(run->bounds), -size.x - gap_UI)), | ||
1454 | addX_I2(size, 2 * gap_UI) }, | ||
1455 | tmBackground_ColorId); | ||
1456 | drawAlign_Text(metaFont, | ||
1457 | add_I2(topRight_Rect(run->bounds), origin), | ||
1458 | fg, | ||
1459 | right_Alignment, | ||
1460 | "%s", cstr_String(&text)); | ||
1461 | } | ||
1462 | deinit_String(&text); | ||
1463 | } | ||
1464 | else if (run->flags & endOfLine_GmRunFlag && | ||
1465 | (mr = findMediaRequest_DocumentWidget_(d->view->owner, run->linkId)) != NULL) { | ||
1466 | if (!isFinished_GmRequest(mr->req)) { | ||
1467 | draw_Text(metaFont, | ||
1468 | topRight_Rect(linkRect), | ||
1469 | tmInlineContentMetadata_ColorId, | ||
1470 | translateCStr_Lang(" \u2014 ${doc.fetching}\u2026 (%.1f ${mb})"), | ||
1471 | (float) bodySize_GmRequest(mr->req) / 1.0e6f); | ||
1472 | } | ||
1473 | } | ||
1474 | } | ||
1475 | if (0) { | ||
1476 | drawRect_Paint(&d->paint, (iRect){ visPos, run->bounds.size }, green_ColorId); | ||
1477 | drawRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, red_ColorId); | ||
1478 | } | ||
1479 | } | ||
1480 | |||
1481 | static int drawSideRect_(iPaint *p, iRect rect) { | ||
1482 | int bg = tmBannerBackground_ColorId; | ||
1483 | int fg = tmBannerIcon_ColorId; | ||
1484 | if (equal_Color(get_Color(bg), get_Color(tmBackground_ColorId))) { | ||
1485 | bg = tmBannerIcon_ColorId; | ||
1486 | fg = tmBannerBackground_ColorId; | ||
1487 | } | ||
1488 | fillRect_Paint(p, rect, bg); | ||
1489 | return fg; | ||
1490 | } | ||
1491 | |||
1492 | static int sideElementAvailWidth_DocumentView_(const iDocumentView *d) { | ||
1493 | return left_Rect(documentBounds_DocumentView_(d)) - | ||
1494 | left_Rect(bounds_Widget(constAs_Widget(d->owner))) - 2 * d->pageMargin * gap_UI; | ||
1495 | } | ||
1496 | |||
1497 | static iBool isSideHeadingVisible_DocumentView_(const iDocumentView *d) { | ||
1498 | return sideElementAvailWidth_DocumentView_(d) >= lineHeight_Text(banner_FontId) * 4.5f; | ||
1499 | } | ||
1500 | |||
1501 | static void updateSideIconBuf_DocumentView_(const iDocumentView *d) { | ||
1502 | if (!isExposed_Window(get_Window())) { | ||
1503 | return; | ||
1504 | } | ||
1505 | iDrawBufs *dbuf = d->drawBufs; | ||
1506 | dbuf->flags &= ~updateSideBuf_DrawBufsFlag; | ||
1507 | if (dbuf->sideIconBuf) { | ||
1508 | SDL_DestroyTexture(dbuf->sideIconBuf); | ||
1509 | dbuf->sideIconBuf = NULL; | ||
1510 | } | ||
1511 | // const iGmRun *banner = siteBanner_GmDocument(d->doc); | ||
1512 | if (isEmpty_Banner(d->owner->banner)) { | ||
1513 | return; | ||
1514 | } | ||
1515 | const int margin = gap_UI * d->pageMargin; | ||
1516 | const int minBannerSize = lineHeight_Text(banner_FontId) * 2; | ||
1517 | const iChar icon = siteIcon_GmDocument(d->doc); | ||
1518 | const int avail = sideElementAvailWidth_DocumentView_(d) - margin; | ||
1519 | iBool isHeadingVisible = isSideHeadingVisible_DocumentView_(d); | ||
1520 | /* Determine the required size. */ | ||
1521 | iInt2 bufSize = init1_I2(minBannerSize); | ||
1522 | const int sideHeadingFont = FONT_ID(documentHeading_FontId, regular_FontStyle, contentBig_FontSize); | ||
1523 | if (isHeadingVisible) { | ||
1524 | const iInt2 headingSize = measureWrapRange_Text(sideHeadingFont, avail, | ||
1525 | currentHeading_DocumentView_(d)).bounds.size; | ||
1526 | if (headingSize.x > 0) { | ||
1527 | bufSize.y += gap_Text + headingSize.y; | ||
1528 | bufSize.x = iMax(bufSize.x, headingSize.x); | ||
1529 | } | ||
1530 | else { | ||
1531 | isHeadingVisible = iFalse; | ||
1532 | } | ||
1533 | } | ||
1534 | SDL_Renderer *render = renderer_Window(get_Window()); | ||
1535 | dbuf->sideIconBuf = SDL_CreateTexture(render, | ||
1536 | SDL_PIXELFORMAT_RGBA4444, | ||
1537 | SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET, | ||
1538 | bufSize.x, bufSize.y); | ||
1539 | iPaint p; | ||
1540 | init_Paint(&p); | ||
1541 | beginTarget_Paint(&p, dbuf->sideIconBuf); | ||
1542 | const iColor back = get_Color(tmBannerSideTitle_ColorId); | ||
1543 | SDL_SetRenderDrawColor(render, back.r, back.g, back.b, 0); /* better blending of the edge */ | ||
1544 | SDL_RenderClear(render); | ||
1545 | const iRect iconRect = { zero_I2(), init1_I2(minBannerSize) }; | ||
1546 | int fg = drawSideRect_(&p, iconRect); | ||
1547 | iString str; | ||
1548 | initUnicodeN_String(&str, &icon, 1); | ||
1549 | drawCentered_Text(banner_FontId, iconRect, iTrue, fg, "%s", cstr_String(&str)); | ||
1550 | deinit_String(&str); | ||
1551 | if (isHeadingVisible) { | ||
1552 | iRangecc text = currentHeading_DocumentView_(d); | ||
1553 | iInt2 pos = addY_I2(bottomLeft_Rect(iconRect), gap_Text); | ||
1554 | const int font = sideHeadingFont; | ||
1555 | drawWrapRange_Text(font, pos, avail, tmBannerSideTitle_ColorId, text); | ||
1556 | } | ||
1557 | endTarget_Paint(&p); | ||
1558 | SDL_SetTextureBlendMode(dbuf->sideIconBuf, SDL_BLENDMODE_BLEND); | ||
1559 | } | ||
1560 | |||
1561 | static void drawSideElements_DocumentView_(const iDocumentView *d) { | ||
1562 | const iWidget *w = constAs_Widget(d->owner); | ||
1563 | const iRect bounds = bounds_Widget(w); | ||
1564 | const iRect docBounds = documentBounds_DocumentView_(d); | ||
1565 | const int margin = gap_UI * d->pageMargin; | ||
1566 | float opacity = value_Anim(&d->sideOpacity); | ||
1567 | const int avail = left_Rect(docBounds) - left_Rect(bounds) - 2 * margin; | ||
1568 | iDrawBufs * dbuf = d->drawBufs; | ||
1569 | iPaint p; | ||
1570 | init_Paint(&p); | ||
1571 | setClip_Paint(&p, boundsWithoutVisualOffset_Widget(w)); | ||
1572 | /* Side icon and current heading. */ | ||
1573 | if (prefs_App()->sideIcon && opacity > 0 && dbuf->sideIconBuf) { | ||
1574 | const iInt2 texSize = size_SDLTexture(dbuf->sideIconBuf); | ||
1575 | if (avail > texSize.x) { | ||
1576 | const int minBannerSize = lineHeight_Text(banner_FontId) * 2; | ||
1577 | iInt2 pos = addY_I2(add_I2(topLeft_Rect(bounds), init_I2(margin, 0)), | ||
1578 | height_Rect(bounds) / 2 - minBannerSize / 2 - | ||
1579 | (texSize.y > minBannerSize | ||
1580 | ? (gap_Text + lineHeight_Text(heading3_FontId)) / 2 | ||
1581 | : 0)); | ||
1582 | SDL_SetTextureAlphaMod(dbuf->sideIconBuf, 255 * opacity); | ||
1583 | SDL_RenderCopy(renderer_Window(get_Window()), | ||
1584 | dbuf->sideIconBuf, NULL, | ||
1585 | &(SDL_Rect){ pos.x, pos.y, texSize.x, texSize.y }); | ||
1586 | } | ||
1587 | } | ||
1588 | /* Reception timestamp. */ | ||
1589 | if (dbuf->timestampBuf && dbuf->timestampBuf->size.x <= avail) { | ||
1590 | draw_TextBuf( | ||
1591 | dbuf->timestampBuf, | ||
1592 | add_I2( | ||
1593 | bottomLeft_Rect(bounds), | ||
1594 | init_I2(margin, | ||
1595 | -margin + -dbuf->timestampBuf->size.y + | ||
1596 | iMax(0, d->scrollY.max - pos_SmoothScroll(&d->scrollY)))), | ||
1597 | tmQuoteIcon_ColorId); | ||
1598 | } | ||
1599 | unsetClip_Paint(&p); | ||
1600 | } | ||
1601 | |||
1602 | static void drawMedia_DocumentView_(const iDocumentView *d, iPaint *p) { | ||
1603 | iConstForEach(PtrArray, i, &d->visibleMedia) { | ||
1604 | const iGmRun * run = i.ptr; | ||
1605 | if (run->mediaType == audio_MediaType) { | ||
1606 | iPlayerUI ui; | ||
1607 | init_PlayerUI(&ui, | ||
1608 | audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run)), | ||
1609 | runRect_DocumentView_(d, run)); | ||
1610 | draw_PlayerUI(&ui, p); | ||
1611 | } | ||
1612 | else if (run->mediaType == download_MediaType) { | ||
1613 | iDownloadUI ui; | ||
1614 | init_DownloadUI(&ui, constMedia_GmDocument(d->doc), run->mediaId, | ||
1615 | runRect_DocumentView_(d, run)); | ||
1616 | draw_DownloadUI(&ui, p); | ||
1617 | } | ||
1618 | } | ||
1619 | } | ||
1620 | |||
1621 | static void extend_GmRunRange_(iGmRunRange *runs) { | ||
1622 | if (runs->start) { | ||
1623 | runs->start--; | ||
1624 | runs->end++; | ||
1625 | } | ||
1626 | } | ||
1627 | |||
1628 | static iBool render_DocumentView_(const iDocumentView *d, iDrawContext *ctx, iBool prerenderExtra) { | ||
1629 | iBool didDraw = iFalse; | ||
1630 | const iRect bounds = bounds_Widget(constAs_Widget(d->owner)); | ||
1631 | const iRect ctxWidgetBounds = | ||
1632 | init_Rect(0, | ||
1633 | 0, | ||
1634 | width_Rect(bounds) - constAs_Widget(d->owner->scroll)->rect.size.x, | ||
1635 | height_Rect(bounds)); | ||
1636 | const iRangei full = { 0, size_GmDocument(d->doc).y }; | ||
1637 | const iRangei vis = ctx->vis; | ||
1638 | iVisBuf *visBuf = d->visBuf; /* will be updated now */ | ||
1639 | d->drawBufs->lastRenderTime = SDL_GetTicks(); | ||
1640 | /* Swap buffers around to have room available both before and after the visible region. */ | ||
1641 | allocVisBuffer_DocumentView_(d); | ||
1642 | reposition_VisBuf(visBuf, vis); | ||
1643 | /* Redraw the invalid ranges. */ | ||
1644 | if (~flags_Widget(constAs_Widget(d->owner)) & destroyPending_WidgetFlag) { | ||
1645 | iPaint *p = &ctx->paint; | ||
1646 | init_Paint(p); | ||
1647 | iForIndices(i, visBuf->buffers) { | ||
1648 | iVisBufTexture *buf = &visBuf->buffers[i]; | ||
1649 | iVisBufMeta *meta = buf->user; | ||
1650 | const iRangei bufRange = intersect_Rangei(bufferRange_VisBuf(visBuf, i), full); | ||
1651 | const iRangei bufVisRange = intersect_Rangei(bufRange, vis); | ||
1652 | ctx->widgetBounds = moved_Rect(ctxWidgetBounds, init_I2(0, -buf->origin)); | ||
1653 | ctx->viewPos = init_I2(left_Rect(ctx->docBounds) - left_Rect(bounds), -buf->origin); | ||
1654 | // printf(" buffer %zu: buf vis range %d...%d\n", i, bufVisRange.start, bufVisRange.end); | ||
1655 | if (!prerenderExtra && !isEmpty_Range(&bufVisRange)) { | ||
1656 | didDraw = iTrue; | ||
1657 | if (isEmpty_Rangei(buf->validRange)) { | ||
1658 | /* Fill the required currently visible range (vis). */ | ||
1659 | const iRangei bufVisRange = intersect_Rangei(bufRange, vis); | ||
1660 | if (!isEmpty_Range(&bufVisRange)) { | ||
1661 | beginTarget_Paint(p, buf->texture); | ||
1662 | fillRect_Paint(p, (iRect){ zero_I2(), visBuf->texSize }, tmBackground_ColorId); | ||
1663 | iZap(ctx->runsDrawn); | ||
1664 | render_GmDocument(d->doc, bufVisRange, drawRun_DrawContext_, ctx); | ||
1665 | meta->runsDrawn = ctx->runsDrawn; | ||
1666 | extend_GmRunRange_(&meta->runsDrawn); | ||
1667 | buf->validRange = bufVisRange; | ||
1668 | // printf(" buffer %zu valid %d...%d\n", i, bufRange.start, bufRange.end); | ||
1669 | } | ||
1670 | } | ||
1671 | else { | ||
1672 | /* Progressively fill the required runs. */ | ||
1673 | if (meta->runsDrawn.start) { | ||
1674 | beginTarget_Paint(p, buf->texture); | ||
1675 | meta->runsDrawn.start = renderProgressive_GmDocument(d->doc, meta->runsDrawn.start, | ||
1676 | -1, iInvalidSize, | ||
1677 | bufVisRange, | ||
1678 | drawRun_DrawContext_, | ||
1679 | ctx); | ||
1680 | buf->validRange.start = bufVisRange.start; | ||
1681 | } | ||
1682 | if (meta->runsDrawn.end) { | ||
1683 | beginTarget_Paint(p, buf->texture); | ||
1684 | meta->runsDrawn.end = renderProgressive_GmDocument(d->doc, meta->runsDrawn.end, | ||
1685 | +1, iInvalidSize, | ||
1686 | bufVisRange, | ||
1687 | drawRun_DrawContext_, | ||
1688 | ctx); | ||
1689 | buf->validRange.end = bufVisRange.end; | ||
1690 | } | ||
1691 | } | ||
1692 | } | ||
1693 | /* Progressively draw the rest of the buffer if it isn't fully valid. */ | ||
1694 | if (prerenderExtra && !equal_Rangei(bufRange, buf->validRange)) { | ||
1695 | const iGmRun *next; | ||
1696 | // printf("%zu: prerenderExtra (start:%p end:%p)\n", i, meta->runsDrawn.start, meta->runsDrawn.end); | ||
1697 | if (meta->runsDrawn.start == NULL) { | ||
1698 | /* Haven't drawn anything yet in this buffer, so let's try seeding it. */ | ||
1699 | const int rh = lineHeight_Text(paragraph_FontId); | ||
1700 | const int y = i >= iElemCount(visBuf->buffers) / 2 ? bufRange.start : (bufRange.end - rh); | ||
1701 | beginTarget_Paint(p, buf->texture); | ||
1702 | fillRect_Paint(p, (iRect){ zero_I2(), visBuf->texSize }, tmBackground_ColorId); | ||
1703 | buf->validRange = (iRangei){ y, y + rh }; | ||
1704 | iZap(ctx->runsDrawn); | ||
1705 | render_GmDocument(d->doc, buf->validRange, drawRun_DrawContext_, ctx); | ||
1706 | meta->runsDrawn = ctx->runsDrawn; | ||
1707 | extend_GmRunRange_(&meta->runsDrawn); | ||
1708 | // printf("%zu: seeded, next %p:%p\n", i, meta->runsDrawn.start, meta->runsDrawn.end); | ||
1709 | didDraw = iTrue; | ||
1710 | } | ||
1711 | else { | ||
1712 | if (meta->runsDrawn.start) { | ||
1713 | const iRangei upper = intersect_Rangei(bufRange, (iRangei){ full.start, buf->validRange.start }); | ||
1714 | if (upper.end > upper.start) { | ||
1715 | beginTarget_Paint(p, buf->texture); | ||
1716 | next = renderProgressive_GmDocument(d->doc, meta->runsDrawn.start, | ||
1717 | -1, 1, upper, | ||
1718 | drawRun_DrawContext_, | ||
1719 | ctx); | ||
1720 | if (next && meta->runsDrawn.start != next) { | ||
1721 | meta->runsDrawn.start = next; | ||
1722 | buf->validRange.start = bottom_Rect(next->visBounds); | ||
1723 | didDraw = iTrue; | ||
1724 | } | ||
1725 | else { | ||
1726 | buf->validRange.start = bufRange.start; | ||
1727 | } | ||
1728 | } | ||
1729 | } | ||
1730 | if (!didDraw && meta->runsDrawn.end) { | ||
1731 | const iRangei lower = intersect_Rangei(bufRange, (iRangei){ buf->validRange.end, full.end }); | ||
1732 | if (lower.end > lower.start) { | ||
1733 | beginTarget_Paint(p, buf->texture); | ||
1734 | next = renderProgressive_GmDocument(d->doc, meta->runsDrawn.end, | ||
1735 | +1, 1, lower, | ||
1736 | drawRun_DrawContext_, | ||
1737 | ctx); | ||
1738 | if (next && meta->runsDrawn.end != next) { | ||
1739 | meta->runsDrawn.end = next; | ||
1740 | buf->validRange.end = top_Rect(next->visBounds); | ||
1741 | didDraw = iTrue; | ||
1742 | } | ||
1743 | else { | ||
1744 | buf->validRange.end = bufRange.end; | ||
1745 | } | ||
1746 | } | ||
1747 | } | ||
1748 | } | ||
1749 | } | ||
1750 | /* Draw any invalidated runs that fall within this buffer. */ | ||
1751 | if (!prerenderExtra) { | ||
1752 | const iRangei bufRange = { buf->origin, buf->origin + visBuf->texSize.y }; | ||
1753 | /* Clear full-width backgrounds first in case there are any dynamic elements. */ { | ||
1754 | iConstForEach(PtrSet, r, d->invalidRuns) { | ||
1755 | const iGmRun *run = *r.value; | ||
1756 | if (isOverlapping_Rangei(bufRange, ySpan_Rect(run->visBounds))) { | ||
1757 | beginTarget_Paint(p, buf->texture); | ||
1758 | fillRect_Paint(p, | ||
1759 | init_Rect(0, | ||
1760 | run->visBounds.pos.y - buf->origin, | ||
1761 | visBuf->texSize.x, | ||
1762 | run->visBounds.size.y), | ||
1763 | tmBackground_ColorId); | ||
1764 | } | ||
1765 | } | ||
1766 | } | ||
1767 | setAnsiFlags_Text(ansiEscapes_GmDocument(d->doc)); | ||
1768 | iConstForEach(PtrSet, r, d->invalidRuns) { | ||
1769 | const iGmRun *run = *r.value; | ||
1770 | if (isOverlapping_Rangei(bufRange, ySpan_Rect(run->visBounds))) { | ||
1771 | beginTarget_Paint(p, buf->texture); | ||
1772 | drawRun_DrawContext_(ctx, run); | ||
1773 | } | ||
1774 | } | ||
1775 | setAnsiFlags_Text(allowAll_AnsiFlag); | ||
1776 | } | ||
1777 | endTarget_Paint(p); | ||
1778 | if (prerenderExtra && didDraw) { | ||
1779 | /* Just a run at a time. */ | ||
1780 | break; | ||
1781 | } | ||
1782 | } | ||
1783 | if (!prerenderExtra) { | ||
1784 | clear_PtrSet(d->invalidRuns); | ||
1785 | } | ||
1786 | } | ||
1787 | return didDraw; | ||
1788 | } | ||
1789 | |||
1790 | static void draw_DocumentView_(const iDocumentView *d) { | ||
1791 | const iWidget *w = constAs_Widget(d->owner); | ||
1792 | const iRect bounds = bounds_Widget(w); | ||
1793 | const iRect boundsWithoutVisOff = boundsWithoutVisualOffset_Widget(w); | ||
1794 | const iRect clipBounds = intersect_Rect(bounds, boundsWithoutVisOff); | ||
1795 | /* Each document has its own palette, but the drawing routines rely on a global one. | ||
1796 | As we're now drawing a document, ensure that the right palette is in effect. | ||
1797 | Document theme colors can be used elsewhere, too, but first a document's palette | ||
1798 | must be made global. */ | ||
1799 | makePaletteGlobal_GmDocument(d->doc); | ||
1800 | if (d->drawBufs->flags & updateTimestampBuf_DrawBufsFlag) { | ||
1801 | updateTimestampBuf_DocumentView_(d); | ||
1802 | } | ||
1803 | if (d->drawBufs->flags & updateSideBuf_DrawBufsFlag) { | ||
1804 | updateSideIconBuf_DocumentView_(d); | ||
1805 | } | ||
1806 | const iRect docBounds = documentBounds_DocumentView_(d); | ||
1807 | const iRangei vis = visibleRange_DocumentView_(d); | ||
1808 | iDrawContext ctx = { | ||
1809 | .view = d, | ||
1810 | .docBounds = docBounds, | ||
1811 | .vis = vis, | ||
1812 | .showLinkNumbers = (d->owner->flags & showLinkNumbers_DocumentWidgetFlag) != 0, | ||
1813 | }; | ||
1814 | init_Paint(&ctx.paint); | ||
1815 | render_DocumentView_(d, &ctx, iFalse /* just the mandatory parts */); | ||
1816 | iBanner *banner = d->owner->banner; | ||
1817 | int yTop = docBounds.pos.y + viewPos_DocumentView_(d); | ||
1818 | const iBool isDocEmpty = size_GmDocument(d->doc).y == 0; | ||
1819 | const iBool isTouchSelecting = (flags_Widget(w) & touchDrag_WidgetFlag) != 0; | ||
1820 | if (!isDocEmpty || !isEmpty_Banner(banner)) { | ||
1821 | const int docBgColor = isDocEmpty ? tmBannerBackground_ColorId : tmBackground_ColorId; | ||
1822 | setClip_Paint(&ctx.paint, clipBounds); | ||
1823 | if (!isDocEmpty) { | ||
1824 | draw_VisBuf(d->visBuf, init_I2(bounds.pos.x, yTop), ySpan_Rect(bounds)); | ||
1825 | } | ||
1826 | /* Text markers. */ | ||
1827 | if (!isEmpty_Range(&d->owner->foundMark) || !isEmpty_Range(&d->owner->selectMark)) { | ||
1828 | SDL_Renderer *render = renderer_Window(get_Window()); | ||
1829 | ctx.firstMarkRect = zero_Rect(); | ||
1830 | ctx.lastMarkRect = zero_Rect(); | ||
1831 | SDL_SetRenderDrawBlendMode(render, | ||
1832 | isDark_ColorTheme(colorTheme_App()) ? SDL_BLENDMODE_ADD | ||
1833 | : SDL_BLENDMODE_BLEND); | ||
1834 | ctx.viewPos = topLeft_Rect(docBounds); | ||
1835 | /* Marker starting outside the visible range? */ | ||
1836 | if (d->visibleRuns.start) { | ||
1837 | if (!isEmpty_Range(&d->owner->selectMark) && | ||
1838 | d->owner->selectMark.start < d->visibleRuns.start->text.start && | ||
1839 | d->owner->selectMark.end > d->visibleRuns.start->text.start) { | ||
1840 | ctx.inSelectMark = iTrue; | ||
1841 | } | ||
1842 | if (isEmpty_Range(&d->owner->foundMark) && | ||
1843 | d->owner->foundMark.start < d->visibleRuns.start->text.start && | ||
1844 | d->owner->foundMark.end > d->visibleRuns.start->text.start) { | ||
1845 | ctx.inFoundMark = iTrue; | ||
1846 | } | ||
1847 | } | ||
1848 | render_GmDocument(d->doc, vis, drawMark_DrawContext_, &ctx); | ||
1849 | SDL_SetRenderDrawBlendMode(render, SDL_BLENDMODE_NONE); | ||
1850 | /* Selection range pins. */ | ||
1851 | if (isTouchSelecting) { | ||
1852 | drawPin_Paint(&ctx.paint, ctx.firstMarkRect, 0, tmQuote_ColorId); | ||
1853 | drawPin_Paint(&ctx.paint, ctx.lastMarkRect, 1, tmQuote_ColorId); | ||
1854 | } | ||
1855 | } | ||
1856 | drawMedia_DocumentView_(d, &ctx.paint); | ||
1857 | /* Fill the top and bottom, in case the document is short. */ | ||
1858 | if (yTop > top_Rect(bounds)) { | ||
1859 | fillRect_Paint(&ctx.paint, | ||
1860 | (iRect){ bounds.pos, init_I2(bounds.size.x, yTop - top_Rect(bounds)) }, | ||
1861 | !isEmpty_Banner(banner) ? tmBannerBackground_ColorId | ||
1862 | : docBgColor); | ||
1863 | } | ||
1864 | /* Banner. */ | ||
1865 | if (!isDocEmpty || numItems_Banner(banner) > 0) { | ||
1866 | /* Fill the part between the banner and the top of the document. */ | ||
1867 | fillRect_Paint(&ctx.paint, | ||
1868 | (iRect){ init_I2(left_Rect(bounds), | ||
1869 | top_Rect(docBounds) + viewPos_DocumentView_(d) - | ||
1870 | documentTopPad_DocumentView_(d)), | ||
1871 | init_I2(bounds.size.x, documentTopPad_DocumentView_(d)) }, | ||
1872 | docBgColor); | ||
1873 | setPos_Banner(banner, addY_I2(topLeft_Rect(docBounds), | ||
1874 | -pos_SmoothScroll(&d->scrollY))); | ||
1875 | draw_Banner(banner); | ||
1876 | } | ||
1877 | const int yBottom = yTop + size_GmDocument(d->doc).y; | ||
1878 | if (yBottom < bottom_Rect(bounds)) { | ||
1879 | fillRect_Paint(&ctx.paint, | ||
1880 | init_Rect(bounds.pos.x, yBottom, bounds.size.x, bottom_Rect(bounds) - yBottom), | ||
1881 | !isDocEmpty ? docBgColor : tmBannerBackground_ColorId); | ||
1882 | } | ||
1883 | unsetClip_Paint(&ctx.paint); | ||
1884 | drawSideElements_DocumentView_(d); | ||
1885 | /* Alt text. */ | ||
1886 | const float altTextOpacity = value_Anim(&d->altTextOpacity) * 6 - 5; | ||
1887 | if (d->hoverAltPre && altTextOpacity > 0) { | ||
1888 | const iGmPreMeta *meta = preMeta_GmDocument(d->doc, preId_GmRun(d->hoverAltPre)); | ||
1889 | if (meta->flags & topLeft_GmPreMetaFlag && ~meta->flags & decoration_GmRunFlag && | ||
1890 | !isEmpty_Range(&meta->altText)) { | ||
1891 | const int margin = 3 * gap_UI / 2; | ||
1892 | const int altFont = uiLabel_FontId; | ||
1893 | const int wrap = docBounds.size.x - 2 * margin; | ||
1894 | iInt2 pos = addY_I2(add_I2(docBounds.pos, meta->pixelRect.pos), | ||
1895 | viewPos_DocumentView_(d)); | ||
1896 | const iInt2 textSize = measureWrapRange_Text(altFont, wrap, meta->altText).bounds.size; | ||
1897 | pos.y -= textSize.y + gap_UI; | ||
1898 | pos.y = iMax(pos.y, top_Rect(bounds)); | ||
1899 | const iRect altRect = { pos, init_I2(docBounds.size.x, textSize.y) }; | ||
1900 | ctx.paint.alpha = altTextOpacity * 255; | ||
1901 | if (altTextOpacity < 1) { | ||
1902 | SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND); | ||
1903 | } | ||
1904 | fillRect_Paint(&ctx.paint, altRect, tmBackgroundAltText_ColorId); | ||
1905 | drawRect_Paint(&ctx.paint, altRect, tmFrameAltText_ColorId); | ||
1906 | setOpacity_Text(altTextOpacity); | ||
1907 | drawWrapRange_Text(altFont, addX_I2(pos, margin), wrap, | ||
1908 | tmQuote_ColorId, meta->altText); | ||
1909 | SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE); | ||
1910 | setOpacity_Text(1.0f); | ||
1911 | } | ||
1912 | } | ||
1913 | /* Touch selection indicator. */ | ||
1914 | if (isTouchSelecting) { | ||
1915 | iRect rect = { topLeft_Rect(bounds), | ||
1916 | init_I2(width_Rect(bounds), lineHeight_Text(uiLabelBold_FontId)) }; | ||
1917 | fillRect_Paint(&ctx.paint, rect, uiTextAction_ColorId); | ||
1918 | const iRangecc mark = selectMark_DocumentWidget_(d->owner); | ||
1919 | drawCentered_Text(uiLabelBold_FontId, | ||
1920 | rect, | ||
1921 | iFalse, | ||
1922 | uiBackground_ColorId, | ||
1923 | "%zu bytes selected", /* TODO: i18n */ | ||
1924 | size_Range(&mark)); | ||
1925 | } | ||
1926 | } | ||
1927 | } | ||
1928 | |||
1929 | /*----------------------------------------------------------------------------------------------*/ | ||
1930 | |||
1931 | static void enableActions_DocumentWidget_(iDocumentWidget *d, iBool enable) { | ||
1932 | /* Actions are invisible child widgets of the DocumentWidget. */ | ||
1933 | iForEach(ObjectList, i, children_Widget(d)) { | ||
1934 | if (isAction_Widget(i.object)) { | ||
1935 | setFlags_Widget(i.object, disabled_WidgetFlag, !enable); | ||
1936 | } | ||
1937 | } | ||
1938 | } | ||
1939 | |||
1940 | static void setLinkNumberMode_DocumentWidget_(iDocumentWidget *d, iBool set) { | ||
1941 | if (((d->flags & showLinkNumbers_DocumentWidgetFlag) != 0) != set) { | ||
1942 | iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, set); | ||
1943 | /* Children have priority when handling events. */ | ||
1944 | enableActions_DocumentWidget_(d, !set); | ||
1945 | #if defined (iPlatformAppleDesktop) | ||
1946 | enableMenuItemsOnHomeRow_MacOS(!set); | ||
1947 | #endif | ||
1948 | /* Ensure all keyboard events come here first. */ | ||
1949 | setKeyboardGrab_Widget(set ? as_Widget(d) : NULL); | ||
1950 | if (d->menu) { | ||
1951 | setFlags_Widget(d->menu, disabled_WidgetFlag, set); | ||
1952 | } | ||
1953 | } | ||
1954 | } | ||
1955 | |||
1956 | static void requestUpdated_DocumentWidget_(iAnyObject *obj) { | ||
1957 | iDocumentWidget *d = obj; | ||
1958 | const int wasUpdated = exchange_Atomic(&d->isRequestUpdated, iTrue); | ||
1959 | if (!wasUpdated) { | ||
1960 | postCommand_Widget(obj, | ||
1961 | "document.request.updated doc:%p reqid:%u request:%p", | ||
1962 | d, | ||
1963 | id_GmRequest(d->request), | ||
1964 | d->request); | ||
1965 | } | ||
1966 | } | ||
1967 | |||
1968 | static void requestFinished_DocumentWidget_(iAnyObject *obj) { | ||
1969 | iDocumentWidget *d = obj; | ||
1970 | postCommand_Widget(obj, | ||
1971 | "document.request.finished doc:%p reqid:%u request:%p", | ||
1972 | d, | ||
1973 | id_GmRequest(d->request), | ||
1974 | d->request); | ||
1975 | } | ||
1976 | |||
1977 | static void animate_DocumentWidget_(void *ticker) { | ||
1978 | iDocumentWidget *d = ticker; | ||
1979 | iAssert(isInstance_Object(d, &Class_DocumentWidget)); | ||
1980 | refresh_Widget(d); | ||
1981 | if (!isFinished_Anim(&d->view.sideOpacity) || !isFinished_Anim(&d->view.altTextOpacity) || | ||
1982 | (d->linkInfo && !isFinished_Anim(&d->linkInfo->opacity))) { | ||
1983 | addTicker_App(animate_DocumentWidget_, d); | ||
1984 | } | ||
811 | } | 1985 | } |
812 | 1986 | ||
813 | static uint32_t mediaUpdateInterval_DocumentWidget_(const iDocumentWidget *d) { | 1987 | static uint32_t mediaUpdateInterval_DocumentWidget_(const iDocumentWidget *d) { |
@@ -819,10 +1993,10 @@ static uint32_t mediaUpdateInterval_DocumentWidget_(const iDocumentWidget *d) { | |||
819 | } | 1993 | } |
820 | static const uint32_t invalidInterval_ = ~0u; | 1994 | static const uint32_t invalidInterval_ = ~0u; |
821 | uint32_t interval = invalidInterval_; | 1995 | uint32_t interval = invalidInterval_; |
822 | iConstForEach(PtrArray, i, &d->visibleMedia) { | 1996 | iConstForEach(PtrArray, i, &d->view.visibleMedia) { |
823 | const iGmRun *run = i.ptr; | 1997 | const iGmRun *run = i.ptr; |
824 | if (run->mediaType == audio_MediaType) { | 1998 | if (run->mediaType == audio_MediaType) { |
825 | iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run)); | 1999 | iPlayer *plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run)); |
826 | if (flags_Player(plr) & adjustingVolume_PlayerFlag || | 2000 | if (flags_Player(plr) & adjustingVolume_PlayerFlag || |
827 | (isStarted_Player(plr) && !isPaused_Player(plr))) { | 2001 | (isStarted_Player(plr) && !isPaused_Player(plr))) { |
828 | interval = iMin(interval, 1000 / 15); | 2002 | interval = iMin(interval, 1000 / 15); |
@@ -845,10 +2019,10 @@ static uint32_t postMediaUpdate_DocumentWidget_(uint32_t interval, void *context | |||
845 | static void updateMedia_DocumentWidget_(iDocumentWidget *d) { | 2019 | static void updateMedia_DocumentWidget_(iDocumentWidget *d) { |
846 | if (document_App() == d) { | 2020 | if (document_App() == d) { |
847 | refresh_Widget(d); | 2021 | refresh_Widget(d); |
848 | iConstForEach(PtrArray, i, &d->visibleMedia) { | 2022 | iConstForEach(PtrArray, i, &d->view.visibleMedia) { |
849 | const iGmRun *run = i.ptr; | 2023 | const iGmRun *run = i.ptr; |
850 | if (run->mediaType == audio_MediaType) { | 2024 | if (run->mediaType == audio_MediaType) { |
851 | iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run)); | 2025 | iPlayer *plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run)); |
852 | if (idleTimeMs_Player(plr) > 3000 && ~flags_Player(plr) & volumeGrabbed_PlayerFlag && | 2026 | if (idleTimeMs_Player(plr) > 3000 && ~flags_Player(plr) & volumeGrabbed_PlayerFlag && |
853 | flags_Player(plr) & adjustingVolume_PlayerFlag) { | 2027 | flags_Player(plr) & adjustingVolume_PlayerFlag) { |
854 | setFlags_Player(plr, adjustingVolume_PlayerFlag, iFalse); | 2028 | setFlags_Player(plr, adjustingVolume_PlayerFlag, iFalse); |
@@ -876,88 +2050,6 @@ static void animateMedia_DocumentWidget_(iDocumentWidget *d) { | |||
876 | } | 2050 | } |
877 | } | 2051 | } |
878 | 2052 | ||
879 | static iRangecc currentHeading_DocumentWidget_(const iDocumentWidget *d) { | ||
880 | iRangecc heading = iNullRange; | ||
881 | if (d->visibleRuns.start) { | ||
882 | iConstForEach(Array, i, headings_GmDocument(d->doc)) { | ||
883 | const iGmHeading *head = i.value; | ||
884 | if (head->level == 0) { | ||
885 | if (head->text.start <= d->visibleRuns.start->text.start) { | ||
886 | heading = head->text; | ||
887 | } | ||
888 | if (d->visibleRuns.end && head->text.start > d->visibleRuns.end->text.start) { | ||
889 | break; | ||
890 | } | ||
891 | } | ||
892 | } | ||
893 | } | ||
894 | return heading; | ||
895 | } | ||
896 | |||
897 | static int updateScrollMax_DocumentWidget_(iDocumentWidget *d) { | ||
898 | arrange_Widget(d->footerButtons); /* scrollMax depends on footer height */ | ||
899 | const int scrollMax = scrollMax_DocumentWidget_(d); | ||
900 | setMax_SmoothScroll(&d->scrollY, scrollMax); | ||
901 | return scrollMax; | ||
902 | } | ||
903 | |||
904 | static void updateVisible_DocumentWidget_(iDocumentWidget *d) { | ||
905 | iChangeFlags(d->flags, | ||
906 | centerVertically_DocumentWidgetFlag, | ||
907 | prefs_App()->centerShortDocs || startsWithCase_String(d->mod.url, "about:") || | ||
908 | !isSuccess_GmStatusCode(d->sourceStatus)); | ||
909 | const iRangei visRange = visibleRange_DocumentWidget_(d); | ||
910 | // printf("visRange: %d...%d\n", visRange.start, visRange.end); | ||
911 | const iRect bounds = bounds_Widget(as_Widget(d)); | ||
912 | const int scrollMax = updateScrollMax_DocumentWidget_(d); | ||
913 | /* Reposition the footer buttons as appropriate. */ | ||
914 | /* TODO: You can just position `footerButtons` here completely without having to get | ||
915 | `Widget` involved with the offset in any way. */ | ||
916 | setRange_ScrollWidget(d->scroll, (iRangei){ 0, scrollMax }); | ||
917 | const int docSize = pageHeight_DocumentWidget_(d) + iMax(height_Widget(d->phoneToolbar), | ||
918 | height_Widget(d->footerButtons)); | ||
919 | const float scrollPos = pos_SmoothScroll(&d->scrollY); | ||
920 | setThumb_ScrollWidget(d->scroll, | ||
921 | pos_SmoothScroll(&d->scrollY), | ||
922 | docSize > 0 ? height_Rect(bounds) * size_Range(&visRange) / docSize : 0); | ||
923 | if (d->footerButtons) { | ||
924 | const iRect bounds = bounds_Widget(as_Widget(d)); | ||
925 | const iRect docBounds = documentBounds_DocumentWidget_(d); | ||
926 | const int hPad = (width_Rect(bounds) - iMin(120 * gap_UI, width_Rect(docBounds))) / 2; | ||
927 | const int vPad = 3 * gap_UI; | ||
928 | setPadding_Widget(d->footerButtons, hPad, 0, hPad, vPad); | ||
929 | d->footerButtons->rect.pos.y = height_Rect(bounds) - height_Widget(d->footerButtons) + | ||
930 | (scrollMax > 0 ? scrollMax - scrollPos : 0); | ||
931 | } | ||
932 | clear_PtrArray(&d->visibleLinks); | ||
933 | clear_PtrArray(&d->visibleWideRuns); | ||
934 | clear_PtrArray(&d->visiblePre); | ||
935 | clear_PtrArray(&d->visibleMedia); | ||
936 | const iRangecc oldHeading = currentHeading_DocumentWidget_(d); | ||
937 | /* Scan for visible runs. */ { | ||
938 | iZap(d->visibleRuns); | ||
939 | render_GmDocument(d->doc, visRange, addVisible_DocumentWidget_, d); | ||
940 | } | ||
941 | const iRangecc newHeading = currentHeading_DocumentWidget_(d); | ||
942 | if (memcmp(&oldHeading, &newHeading, sizeof(oldHeading))) { | ||
943 | d->drawBufs->flags |= updateSideBuf_DrawBufsFlag; | ||
944 | } | ||
945 | updateHover_DocumentWidget_(d, mouseCoord_Window(get_Window(), 0)); | ||
946 | updateSideOpacity_DocumentWidget_(d, iTrue); | ||
947 | animateMedia_DocumentWidget_(d); | ||
948 | /* Remember scroll positions of recently visited pages. */ { | ||
949 | iRecentUrl *recent = mostRecentUrl_History(d->mod.history); | ||
950 | if (recent && docSize && d->state == ready_RequestState) { | ||
951 | recent->normScrollY = normScrollPos_DocumentWidget_(d); | ||
952 | } | ||
953 | } | ||
954 | /* After scrolling/resizing stops, begin pre-rendering the visbuf contents. */ { | ||
955 | removeTicker_App(prerender_DocumentWidget_, d); | ||
956 | remove_Periodic(periodic_App(), d); | ||
957 | add_Periodic(periodic_App(), d, "document.render"); | ||
958 | } | ||
959 | } | ||
960 | |||
961 | static void updateWindowTitle_DocumentWidget_(const iDocumentWidget *d) { | 2053 | static void updateWindowTitle_DocumentWidget_(const iDocumentWidget *d) { |
962 | iLabelWidget *tabButton = tabPageButton_Widget(findChild_Widget(root_Widget(constAs_Widget(d)), | 2054 | iLabelWidget *tabButton = tabPageButton_Widget(findChild_Widget(root_Widget(constAs_Widget(d)), |
963 | "doctabs"), d); | 2055 | "doctabs"), d); |
@@ -966,8 +2058,8 @@ static void updateWindowTitle_DocumentWidget_(const iDocumentWidget *d) { | |||
966 | return; | 2058 | return; |
967 | } | 2059 | } |
968 | iStringArray *title = iClob(new_StringArray()); | 2060 | iStringArray *title = iClob(new_StringArray()); |
969 | if (!isEmpty_String(title_GmDocument(d->doc))) { | 2061 | if (!isEmpty_String(title_GmDocument(d->view.doc))) { |
970 | pushBack_StringArray(title, title_GmDocument(d->doc)); | 2062 | pushBack_StringArray(title, title_GmDocument(d->view.doc)); |
971 | } | 2063 | } |
972 | if (!isEmpty_String(d->titleUser)) { | 2064 | if (!isEmpty_String(d->titleUser)) { |
973 | pushBack_StringArray(title, d->titleUser); | 2065 | pushBack_StringArray(title, d->titleUser); |
@@ -998,7 +2090,7 @@ static void updateWindowTitle_DocumentWidget_(const iDocumentWidget *d) { | |||
998 | setTitle_MainWindow(get_MainWindow(), text); | 2090 | setTitle_MainWindow(get_MainWindow(), text); |
999 | setWindow = iFalse; | 2091 | setWindow = iFalse; |
1000 | } | 2092 | } |
1001 | const iChar siteIcon = siteIcon_GmDocument(d->doc); | 2093 | const iChar siteIcon = siteIcon_GmDocument(d->view.doc); |
1002 | if (siteIcon) { | 2094 | if (siteIcon) { |
1003 | if (!isEmpty_String(text)) { | 2095 | if (!isEmpty_String(text)) { |
1004 | prependCStr_String(text, " " restore_ColorEscape); | 2096 | prependCStr_String(text, " " restore_ColorEscape); |
@@ -1038,50 +2130,28 @@ static void updateWindowTitle_DocumentWidget_(const iDocumentWidget *d) { | |||
1038 | } | 2130 | } |
1039 | } | 2131 | } |
1040 | 2132 | ||
1041 | static void updateTimestampBuf_DocumentWidget_(const iDocumentWidget *d) { | 2133 | static void invalidate_DocumentWidget_(iDocumentWidget *d) { |
1042 | if (!isExposed_Window(get_Window())) { | 2134 | if (flags_Widget(as_Widget(d)) & destroyPending_WidgetFlag) { |
1043 | return; | 2135 | return; |
1044 | } | 2136 | } |
1045 | if (d->drawBufs->timestampBuf) { | 2137 | if (d->flags & invalidationPending_DocumentWidgetFlag) { |
1046 | delete_TextBuf(d->drawBufs->timestampBuf); | 2138 | return; |
1047 | d->drawBufs->timestampBuf = NULL; | ||
1048 | } | ||
1049 | if (isValid_Time(&d->sourceTime)) { | ||
1050 | iString *fmt = timeFormatHourPreference_Lang("page.timestamp"); | ||
1051 | d->drawBufs->timestampBuf = newRange_TextBuf( | ||
1052 | uiLabel_FontId, | ||
1053 | white_ColorId, | ||
1054 | range_String(collect_String(format_Time(&d->sourceTime, cstr_String(fmt))))); | ||
1055 | delete_String(fmt); | ||
1056 | } | 2139 | } |
1057 | d->drawBufs->flags &= ~updateTimestampBuf_DrawBufsFlag; | 2140 | if (isAffectedByVisualOffset_Widget(as_Widget(d))) { |
1058 | } | 2141 | d->flags |= invalidationPending_DocumentWidgetFlag; |
1059 | |||
1060 | static void invalidate_DocumentWidget_(iDocumentWidget *d) { | ||
1061 | if (flags_Widget(as_Widget(d)) & destroyPending_WidgetFlag) { | ||
1062 | return; | 2142 | return; |
1063 | } | 2143 | } |
1064 | invalidate_VisBuf(d->visBuf); | 2144 | d->flags &= ~invalidationPending_DocumentWidgetFlag; |
1065 | clear_PtrSet(d->invalidRuns); | 2145 | invalidate_DocumentView_(&d->view); |
2146 | // printf("[%p] '%s' invalidated\n", d, cstr_String(id_Widget(as_Widget(d)))); | ||
1066 | } | 2147 | } |
1067 | 2148 | ||
1068 | static iRangecc siteText_DocumentWidget_(const iDocumentWidget *d) { | 2149 | static iRangecc siteText_DocumentWidget_(const iDocumentWidget *d) { |
1069 | return isEmpty_String(d->titleUser) ? urlHost_String(d->mod.url) | 2150 | return isEmpty_String(d->titleUser) ? urlHost_String(d->mod.url) |
1070 | : range_String(d->titleUser); | 2151 | : range_String(d->titleUser); |
1071 | } | 2152 | } |
1072 | 2153 | ||
1073 | static void documentRunsInvalidated_DocumentWidget_(iDocumentWidget *d) { | 2154 | static iBool isPinned_DocumentWidget_(const iDocumentWidget *d) { |
1074 | d->foundMark = iNullRange; | ||
1075 | d->selectMark = iNullRange; | ||
1076 | d->hoverPre = NULL; | ||
1077 | d->hoverAltPre = NULL; | ||
1078 | d->hoverLink = NULL; | ||
1079 | d->contextLink = NULL; | ||
1080 | iZap(d->visibleRuns); | ||
1081 | iZap(d->renderRuns); | ||
1082 | } | ||
1083 | |||
1084 | iBool isPinned_DocumentWidget_(const iDocumentWidget *d) { | ||
1085 | if (deviceType_App() == phone_AppDeviceType) { | 2155 | if (deviceType_App() == phone_AppDeviceType) { |
1086 | return iFalse; | 2156 | return iFalse; |
1087 | } | 2157 | } |
@@ -1104,12 +2174,20 @@ static void showOrHidePinningIndicator_DocumentWidget_(iDocumentWidget *d) { | |||
1104 | isPinned_DocumentWidget_(d)); | 2174 | isPinned_DocumentWidget_(d)); |
1105 | } | 2175 | } |
1106 | 2176 | ||
2177 | static void updateBanner_DocumentWidget_(iDocumentWidget *d) { | ||
2178 | setSite_Banner(d->banner, siteText_DocumentWidget_(d), siteIcon_GmDocument(d->view.doc)); | ||
2179 | } | ||
2180 | |||
1107 | static void documentWasChanged_DocumentWidget_(iDocumentWidget *d) { | 2181 | static void documentWasChanged_DocumentWidget_(iDocumentWidget *d) { |
1108 | updateVisitedLinks_GmDocument(d->doc); | 2182 | iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iFalse); |
2183 | setFlags_Widget(as_Widget(d), touchDrag_WidgetFlag, iFalse); | ||
2184 | d->requestLinkId = 0; | ||
2185 | updateVisitedLinks_GmDocument(d->view.doc); | ||
1109 | documentRunsInvalidated_DocumentWidget_(d); | 2186 | documentRunsInvalidated_DocumentWidget_(d); |
1110 | updateWindowTitle_DocumentWidget_(d); | 2187 | updateWindowTitle_DocumentWidget_(d); |
1111 | updateVisible_DocumentWidget_(d); | 2188 | updateBanner_DocumentWidget_(d); |
1112 | d->drawBufs->flags |= updateSideBuf_DrawBufsFlag; | 2189 | updateVisible_DocumentView_(&d->view); |
2190 | d->view.drawBufs->flags |= updateSideBuf_DrawBufsFlag; | ||
1113 | invalidate_DocumentWidget_(d); | 2191 | invalidate_DocumentWidget_(d); |
1114 | refresh_Widget(as_Widget(d)); | 2192 | refresh_Widget(as_Widget(d)); |
1115 | /* Check for special bookmark tags. */ | 2193 | /* Check for special bookmark tags. */ |
@@ -1117,59 +2195,28 @@ static void documentWasChanged_DocumentWidget_(iDocumentWidget *d) { | |||
1117 | const uint16_t bmid = findUrl_Bookmarks(bookmarks_App(), d->mod.url); | 2195 | const uint16_t bmid = findUrl_Bookmarks(bookmarks_App(), d->mod.url); |
1118 | if (bmid) { | 2196 | if (bmid) { |
1119 | const iBookmark *bm = get_Bookmarks(bookmarks_App(), bmid); | 2197 | const iBookmark *bm = get_Bookmarks(bookmarks_App(), bmid); |
1120 | if (hasTag_Bookmark(bm, linkSplit_BookmarkTag)) { | 2198 | if (bm->flags & linkSplit_BookmarkFlag) { |
1121 | d->flags |= otherRootByDefault_DocumentWidgetFlag; | 2199 | d->flags |= otherRootByDefault_DocumentWidgetFlag; |
1122 | } | 2200 | } |
1123 | } | 2201 | } |
1124 | showOrHidePinningIndicator_DocumentWidget_(d); | 2202 | showOrHidePinningIndicator_DocumentWidget_(d); |
1125 | setCachedDocument_History(d->mod.history, | 2203 | if (~d->flags & fromCache_DocumentWidgetFlag) { |
1126 | d->doc, /* keeps a ref */ | 2204 | setCachedDocument_History(d->mod.history, d->view.doc /* keeps a ref */); |
1127 | (d->flags & openedFromSidebar_DocumentWidgetFlag) != 0); | 2205 | } |
1128 | } | ||
1129 | |||
1130 | void setSource_DocumentWidget(iDocumentWidget *d, const iString *source) { | ||
1131 | setUrl_GmDocument(d->doc, d->mod.url); | ||
1132 | const int docWidth = documentWidth_DocumentWidget_(d); | ||
1133 | setSource_GmDocument(d->doc, | ||
1134 | source, | ||
1135 | docWidth, | ||
1136 | width_Widget(d), | ||
1137 | isFinished_GmRequest(d->request) ? final_GmDocumentUpdate | ||
1138 | : partial_GmDocumentUpdate); | ||
1139 | setWidth_Banner(d->banner, docWidth); | ||
1140 | documentWasChanged_DocumentWidget_(d); | ||
1141 | } | 2206 | } |
1142 | 2207 | ||
1143 | static void replaceDocument_DocumentWidget_(iDocumentWidget *d, iGmDocument *newDoc) { | 2208 | static void replaceDocument_DocumentWidget_(iDocumentWidget *d, iGmDocument *newDoc) { |
1144 | pauseAllPlayers_Media(media_GmDocument(d->doc), iTrue); | 2209 | pauseAllPlayers_Media(media_GmDocument(d->view.doc), iTrue); |
1145 | iRelease(d->doc); | 2210 | iRelease(d->view.doc); |
1146 | d->doc = ref_Object(newDoc); | 2211 | d->view.doc = ref_Object(newDoc); |
1147 | documentWasChanged_DocumentWidget_(d); | 2212 | documentWasChanged_DocumentWidget_(d); |
1148 | } | 2213 | } |
1149 | 2214 | ||
1150 | static void updateBanner_DocumentWidget_(iDocumentWidget *d) { | ||
1151 | /* TODO: Set width. */ | ||
1152 | setSite_Banner(d->banner, siteText_DocumentWidget_(d), siteIcon_GmDocument(d->doc)); | ||
1153 | } | ||
1154 | |||
1155 | static void updateTheme_DocumentWidget_(iDocumentWidget *d) { | 2215 | static void updateTheme_DocumentWidget_(iDocumentWidget *d) { |
1156 | if (document_App() != d || category_GmStatusCode(d->sourceStatus) == categoryInput_GmStatusCode) { | 2216 | if (document_App() != d || category_GmStatusCode(d->sourceStatus) == categoryInput_GmStatusCode) { |
1157 | return; | 2217 | return; |
1158 | } | 2218 | } |
1159 | if (equalCase_Rangecc(urlScheme_String(d->mod.url), "file")) { | 2219 | d->view.drawBufs->flags |= updateTimestampBuf_DrawBufsFlag; |
1160 | iBlock empty; | ||
1161 | init_Block(&empty, 0); | ||
1162 | setThemeSeed_GmDocument(d->doc, &empty); | ||
1163 | deinit_Block(&empty); | ||
1164 | } | ||
1165 | else if (isEmpty_String(d->titleUser)) { | ||
1166 | setThemeSeed_GmDocument(d->doc, | ||
1167 | collect_Block(newRange_Block(urlHost_String(d->mod.url)))); | ||
1168 | } | ||
1169 | else { | ||
1170 | setThemeSeed_GmDocument(d->doc, &d->titleUser->chars); | ||
1171 | } | ||
1172 | d->drawBufs->flags |= updateTimestampBuf_DrawBufsFlag; | ||
1173 | updateBanner_DocumentWidget_(d); | 2220 | updateBanner_DocumentWidget_(d); |
1174 | } | 2221 | } |
1175 | 2222 | ||
@@ -1186,7 +2233,6 @@ static void makeFooterButtons_DocumentWidget_(iDocumentWidget *d, const iMenuIte | |||
1186 | resizeWidthOfChildren_WidgetFlag | arrangeHeight_WidgetFlag | | 2233 | resizeWidthOfChildren_WidgetFlag | arrangeHeight_WidgetFlag | |
1187 | fixedPosition_WidgetFlag | resizeToParentWidth_WidgetFlag, | 2234 | fixedPosition_WidgetFlag | resizeToParentWidth_WidgetFlag, |
1188 | iTrue); | 2235 | iTrue); |
1189 | //setBackgroundColor_Widget(d->footerButtons, tmBackground_ColorId); | ||
1190 | for (size_t i = 0; i < count; ++i) { | 2236 | for (size_t i = 0; i < count; ++i) { |
1191 | iLabelWidget *button = addChildFlags_Widget( | 2237 | iLabelWidget *button = addChildFlags_Widget( |
1192 | d->footerButtons, | 2238 | d->footerButtons, |
@@ -1196,25 +2242,18 @@ static void makeFooterButtons_DocumentWidget_(iDocumentWidget *d, const iMenuIte | |||
1196 | setPadding1_Widget(as_Widget(button), gap_UI / 2); | 2242 | setPadding1_Widget(as_Widget(button), gap_UI / 2); |
1197 | checkIcon_LabelWidget(button); | 2243 | checkIcon_LabelWidget(button); |
1198 | setFont_LabelWidget(button, uiContent_FontId); | 2244 | setFont_LabelWidget(button, uiContent_FontId); |
1199 | } | 2245 | setBackgroundColor_Widget(as_Widget(button), uiBackgroundSidebar_ColorId); |
1200 | if (deviceType_App() == phone_AppDeviceType) { | ||
1201 | /* Footer buttons shouldn't be under the toolbar. */ | ||
1202 | addChild_Widget(d->footerButtons, iClob(makePadding_Widget(height_Widget(d->phoneToolbar)))); | ||
1203 | } | 2246 | } |
1204 | addChild_Widget(as_Widget(d), iClob(d->footerButtons)); | 2247 | addChild_Widget(as_Widget(d), iClob(d->footerButtons)); |
1205 | arrange_Widget(d->footerButtons); | 2248 | arrange_Widget(d->footerButtons); |
1206 | arrange_Widget(w); | 2249 | arrange_Widget(w); |
1207 | updateVisible_DocumentWidget_(d); /* final placement for the buttons */ | 2250 | updateVisible_DocumentView_(&d->view); /* final placement for the buttons */ |
1208 | } | 2251 | } |
1209 | 2252 | ||
1210 | static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode code, | 2253 | static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode code, |
1211 | const iString *meta) { | 2254 | const iString *meta) { |
1212 | /* TODO: No such thing as an "error page". It should be an empty page with an error banner. */ | 2255 | iString *src = collectNew_String(); |
1213 | iString *src = collectNew_String(); | ||
1214 | const iGmError *msg = get_GmError(code); | 2256 | const iGmError *msg = get_GmError(code); |
1215 | // appendChar_String(src, msg->icon ? msg->icon : 0x2327); /* X in a box */ | ||
1216 | //appendFormat_String(src, " %s\n%s", msg->title, msg->info); | ||
1217 | // iBool useBanner = iTrue; | ||
1218 | destroy_Widget(d->footerButtons); | 2257 | destroy_Widget(d->footerButtons); |
1219 | d->footerButtons = NULL; | 2258 | d->footerButtons = NULL; |
1220 | const iString *serverErrorMsg = NULL; | 2259 | const iString *serverErrorMsg = NULL; |
@@ -1264,6 +2303,11 @@ static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode | |||
1264 | 0, | 2303 | 0, |
1265 | format_CStr("document.setmediatype mime:%s", mtype) }); | 2304 | format_CStr("document.setmediatype mime:%s", mtype) }); |
1266 | } | 2305 | } |
2306 | pushBack_Array(&items, | ||
2307 | &(iMenuItem){ export_Icon " ${menu.open.external}", | ||
2308 | SDLK_RETURN, | ||
2309 | KMOD_PRIMARY, | ||
2310 | "document.save extview:1" }); | ||
1267 | pushBack_Array( | 2311 | pushBack_Array( |
1268 | &items, | 2312 | &items, |
1269 | &(iMenuItem){ translateCStr_Lang(download_Icon " " saveToDownloads_Label), | 2313 | &(iMenuItem){ translateCStr_Lang(download_Icon " " saveToDownloads_Label), |
@@ -1285,12 +2329,18 @@ static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode | |||
1285 | if (category_GmStatusCode(code) == categoryClientCertificate_GmStatus) { | 2329 | if (category_GmStatusCode(code) == categoryClientCertificate_GmStatus) { |
1286 | makeFooterButtons_DocumentWidget_( | 2330 | makeFooterButtons_DocumentWidget_( |
1287 | d, | 2331 | d, |
1288 | (iMenuItem[]){ { leftHalf_Icon " ${menu.show.identities}", '4', KMOD_PRIMARY, "sidebar.mode arg:3 show:1" }, | 2332 | (iMenuItem[]){ |
1289 | { person_Icon " ${menu.identity.new}", newIdentity_KeyShortcut, "ident.new" } }, | 2333 | { leftHalf_Icon " ${menu.show.identities}", |
2334 | '4', | ||
2335 | KMOD_PRIMARY, | ||
2336 | deviceType_App() == desktop_AppDeviceType ? "sidebar.mode arg:3 show:1" | ||
2337 | : "preferences idents:1" }, | ||
2338 | { person_Icon " ${menu.identity.new}", newIdentity_KeyShortcut, "ident.new" } }, | ||
1290 | 2); | 2339 | 2); |
1291 | } | 2340 | } |
1292 | /* Make a new document for the error page.*/ | 2341 | /* Make a new document for the error page.*/ |
1293 | iGmDocument *errorDoc = new_GmDocument(); | 2342 | iGmDocument *errorDoc = new_GmDocument(); |
2343 | setWidth_GmDocument(errorDoc, documentWidth_DocumentView_(&d->view), width_Widget(d)); | ||
1294 | setUrl_GmDocument(errorDoc, d->mod.url); | 2344 | setUrl_GmDocument(errorDoc, d->mod.url); |
1295 | setFormat_GmDocument(errorDoc, gemini_SourceFormat); | 2345 | setFormat_GmDocument(errorDoc, gemini_SourceFormat); |
1296 | replaceDocument_DocumentWidget_(d, errorDoc); | 2346 | replaceDocument_DocumentWidget_(d, errorDoc); |
@@ -1300,10 +2350,7 @@ static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode | |||
1300 | d->state = ready_RequestState; | 2350 | d->state = ready_RequestState; |
1301 | setSource_DocumentWidget(d, src); | 2351 | setSource_DocumentWidget(d, src); |
1302 | updateTheme_DocumentWidget_(d); | 2352 | updateTheme_DocumentWidget_(d); |
1303 | reset_SmoothScroll(&d->scrollY); | 2353 | resetScroll_DocumentView_(&d->view); |
1304 | init_Anim(&d->sideOpacity, 0); | ||
1305 | init_Anim(&d->altTextOpacity, 0); | ||
1306 | resetWideRuns_DocumentWidget_(d); | ||
1307 | } | 2354 | } |
1308 | 2355 | ||
1309 | static void updateFetchProgress_DocumentWidget_(iDocumentWidget *d) { | 2356 | static void updateFetchProgress_DocumentWidget_(iDocumentWidget *d) { |
@@ -1419,9 +2466,9 @@ static void postProcessRequestContent_DocumentWidget_(iDocumentWidget *d, iBool | |||
1419 | "document.save" } }, | 2466 | "document.save" } }, |
1420 | 2); | 2467 | 2); |
1421 | } | 2468 | } |
1422 | if (preloadCoverImage_Gempub(d->sourceGempub, d->doc)) { | 2469 | if (preloadCoverImage_Gempub(d->sourceGempub, d->view.doc)) { |
1423 | redoLayout_GmDocument(d->doc); | 2470 | redoLayout_GmDocument(d->view.doc); |
1424 | updateVisible_DocumentWidget_(d); | 2471 | updateVisible_DocumentView_(&d->view); |
1425 | invalidate_DocumentWidget_(d); | 2472 | invalidate_DocumentWidget_(d); |
1426 | } | 2473 | } |
1427 | } | 2474 | } |
@@ -1497,7 +2544,7 @@ static void postProcessRequestContent_DocumentWidget_(iDocumentWidget *d, iBool | |||
1497 | } | 2544 | } |
1498 | else { | 2545 | else { |
1499 | postCommandf_App( | 2546 | postCommandf_App( |
1500 | "open newtab:%d url:%s", otherRoot_OpenTabFlag, cstr_String(navStart)); | 2547 | "open splitmode:1 newtab:%d url:%s", otherRoot_OpenTabFlag, cstr_String(navStart)); |
1501 | } | 2548 | } |
1502 | } | 2549 | } |
1503 | } | 2550 | } |
@@ -1525,7 +2572,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, | |||
1525 | } | 2572 | } |
1526 | clear_String(&d->sourceMime); | 2573 | clear_String(&d->sourceMime); |
1527 | d->sourceTime = response->when; | 2574 | d->sourceTime = response->when; |
1528 | d->drawBufs->flags |= updateTimestampBuf_DrawBufsFlag; | 2575 | d->view.drawBufs->flags |= updateTimestampBuf_DrawBufsFlag; |
1529 | initBlock_String(&str, &response->body); /* Note: Body may be megabytes in size. */ | 2576 | initBlock_String(&str, &response->body); /* Note: Body may be megabytes in size. */ |
1530 | if (isSuccess_GmStatusCode(statusCode)) { | 2577 | if (isSuccess_GmStatusCode(statusCode)) { |
1531 | /* Check the MIME type. */ | 2578 | /* Check the MIME type. */ |
@@ -1565,7 +2612,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, | |||
1565 | setRange_String(&d->sourceMime, param); | 2612 | setRange_String(&d->sourceMime, param); |
1566 | format_String(&str, "# TrueType Font\n"); | 2613 | format_String(&str, "# TrueType Font\n"); |
1567 | iString *decUrl = collect_String(urlDecode_String(d->mod.url)); | 2614 | iString *decUrl = collect_String(urlDecode_String(d->mod.url)); |
1568 | iRangecc name = baseName_Path(decUrl); | 2615 | iRangecc name = baseNameSep_Path(decUrl, "/"); |
1569 | iBool isInstalled = iFalse; | 2616 | iBool isInstalled = iFalse; |
1570 | if (startsWith_String(collect_String(localFilePathFromUrl_String(d->mod.url)), | 2617 | if (startsWith_String(collect_String(localFilePathFromUrl_String(d->mod.url)), |
1571 | cstr_String(dataDir_App()))) { | 2618 | cstr_String(dataDir_App()))) { |
@@ -1628,7 +2675,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, | |||
1628 | appendFormat_String(&str, | 2675 | appendFormat_String(&str, |
1629 | cstr_Lang("doc.archive"), | 2676 | cstr_Lang("doc.archive"), |
1630 | cstr_Rangecc(baseName_Path(d->mod.url))); | 2677 | cstr_Rangecc(baseName_Path(d->mod.url))); |
1631 | appendCStr_String(&str, "\n"); | 2678 | appendCStr_String(&str, "\n"); |
1632 | } | 2679 | } |
1633 | appendCStr_String(&str, "\n"); | 2680 | appendCStr_String(&str, "\n"); |
1634 | iString *localPath = localFilePathFromUrl_String(d->mod.url); | 2681 | iString *localPath = localFilePathFromUrl_String(d->mod.url); |
@@ -1669,16 +2716,16 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, | |||
1669 | format_String(&str, "=> %s %s\n", | 2716 | format_String(&str, "=> %s %s\n", |
1670 | cstr_String(canonicalUrl_String(d->mod.url)), | 2717 | cstr_String(canonicalUrl_String(d->mod.url)), |
1671 | linkTitle); | 2718 | linkTitle); |
1672 | setData_Media(media_GmDocument(d->doc), | 2719 | setData_Media(media_GmDocument(d->view.doc), |
1673 | imgLinkId, | 2720 | imgLinkId, |
1674 | mimeStr, | 2721 | mimeStr, |
1675 | &response->body, | 2722 | &response->body, |
1676 | !isRequestFinished ? partialData_MediaFlag : 0); | 2723 | !isRequestFinished ? partialData_MediaFlag : 0); |
1677 | redoLayout_GmDocument(d->doc); | 2724 | redoLayout_GmDocument(d->view.doc); |
1678 | } | 2725 | } |
1679 | else if (isAudio && !isInitialUpdate) { | 2726 | else if (isAudio && !isInitialUpdate) { |
1680 | /* Update the audio content. */ | 2727 | /* Update the audio content. */ |
1681 | setData_Media(media_GmDocument(d->doc), | 2728 | setData_Media(media_GmDocument(d->view.doc), |
1682 | imgLinkId, | 2729 | imgLinkId, |
1683 | mimeStr, | 2730 | mimeStr, |
1684 | &response->body, | 2731 | &response->body, |
@@ -1708,11 +2755,12 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, | |||
1708 | return; | 2755 | return; |
1709 | } | 2756 | } |
1710 | d->flags |= drawDownloadCounter_DocumentWidgetFlag; | 2757 | d->flags |= drawDownloadCounter_DocumentWidgetFlag; |
1711 | clear_PtrSet(d->invalidRuns); | 2758 | clear_PtrSet(d->view.invalidRuns); |
2759 | documentRunsInvalidated_DocumentWidget_(d); | ||
1712 | deinit_String(&str); | 2760 | deinit_String(&str); |
1713 | return; | 2761 | return; |
1714 | } | 2762 | } |
1715 | setFormat_GmDocument(d->doc, docFormat); | 2763 | setFormat_GmDocument(d->view.doc, docFormat); |
1716 | /* Convert the source to UTF-8 if needed. */ | 2764 | /* Convert the source to UTF-8 if needed. */ |
1717 | if (!equalCase_Rangecc(charset, "utf-8")) { | 2765 | if (!equalCase_Rangecc(charset, "utf-8")) { |
1718 | set_String(&str, | 2766 | set_String(&str, |
@@ -1721,7 +2769,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, | |||
1721 | } | 2769 | } |
1722 | if (cachedDoc) { | 2770 | if (cachedDoc) { |
1723 | replaceDocument_DocumentWidget_(d, cachedDoc); | 2771 | replaceDocument_DocumentWidget_(d, cachedDoc); |
1724 | updateWidth_GmDocument(d->doc, documentWidth_DocumentWidget_(d), width_Widget(d)); | 2772 | updateWidth_DocumentView_(&d->view); |
1725 | } | 2773 | } |
1726 | else if (setSource) { | 2774 | else if (setSource) { |
1727 | setSource_DocumentWidget(d, &str); | 2775 | setSource_DocumentWidget(d, &str); |
@@ -1731,6 +2779,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, | |||
1731 | } | 2779 | } |
1732 | 2780 | ||
1733 | static void fetch_DocumentWidget_(iDocumentWidget *d) { | 2781 | static void fetch_DocumentWidget_(iDocumentWidget *d) { |
2782 | iAssert(~d->flags & animationPlaceholder_DocumentWidgetFlag); | ||
1734 | /* Forget the previous request. */ | 2783 | /* Forget the previous request. */ |
1735 | if (d->request) { | 2784 | if (d->request) { |
1736 | iRelease(d->request); | 2785 | iRelease(d->request); |
@@ -1740,8 +2789,6 @@ static void fetch_DocumentWidget_(iDocumentWidget *d) { | |||
1740 | "document.request.started doc:%p url:%s", | 2789 | "document.request.started doc:%p url:%s", |
1741 | d, | 2790 | d, |
1742 | cstr_String(d->mod.url)); | 2791 | cstr_String(d->mod.url)); |
1743 | clear_ObjectList(d->media); | ||
1744 | d->certFlags = 0; | ||
1745 | setLinkNumberMode_DocumentWidget_(d, iFalse); | 2792 | setLinkNumberMode_DocumentWidget_(d, iFalse); |
1746 | d->flags &= ~drawDownloadCounter_DocumentWidgetFlag; | 2793 | d->flags &= ~drawDownloadCounter_DocumentWidgetFlag; |
1747 | d->state = fetching_RequestState; | 2794 | d->state = fetching_RequestState; |
@@ -1774,7 +2821,7 @@ static void updateTrust_DocumentWidget_(iDocumentWidget *d, const iGmResponse *r | |||
1774 | } | 2821 | } |
1775 | else if (~d->certFlags & timeVerified_GmCertFlag) { | 2822 | else if (~d->certFlags & timeVerified_GmCertFlag) { |
1776 | updateTextCStr_LabelWidget(lock, isDarkMode ? orange_ColorEscape warning_Icon | 2823 | updateTextCStr_LabelWidget(lock, isDarkMode ? orange_ColorEscape warning_Icon |
1777 | : black_ColorEscape warning_Icon); | 2824 | : black_ColorEscape warning_Icon); |
1778 | } | 2825 | } |
1779 | else { | 2826 | else { |
1780 | updateTextCStr_LabelWidget(lock, green_ColorEscape closedLock_Icon); | 2827 | updateTextCStr_LabelWidget(lock, green_ColorEscape closedLock_Icon); |
@@ -1793,17 +2840,24 @@ static void cacheRunGlyphs_(void *data, const iGmRun *run) { | |||
1793 | } | 2840 | } |
1794 | 2841 | ||
1795 | static void cacheDocumentGlyphs_DocumentWidget_(const iDocumentWidget *d) { | 2842 | static void cacheDocumentGlyphs_DocumentWidget_(const iDocumentWidget *d) { |
1796 | if (isFinishedLaunching_App() && isExposed_Window(get_Window())) { | 2843 | if (isFinishedLaunching_App() && isExposed_Window(get_Window()) && |
2844 | ~d->flags & animationPlaceholder_DocumentWidgetFlag) { | ||
1797 | /* Just cache the top of the document, since this is what we usually need. */ | 2845 | /* Just cache the top of the document, since this is what we usually need. */ |
1798 | int maxY = height_Widget(&d->widget) * 2; | 2846 | int maxY = height_Widget(&d->widget) * 2; |
1799 | if (maxY == 0) { | 2847 | if (maxY == 0) { |
1800 | maxY = size_GmDocument(d->doc).y; | 2848 | maxY = size_GmDocument(d->view.doc).y; |
1801 | } | 2849 | } |
1802 | render_GmDocument(d->doc, (iRangei){ 0, maxY }, cacheRunGlyphs_, NULL); | 2850 | render_GmDocument(d->view.doc, (iRangei){ 0, maxY }, cacheRunGlyphs_, NULL); |
1803 | } | 2851 | } |
1804 | } | 2852 | } |
1805 | 2853 | ||
1806 | static void addBannerWarnings_DocumentWidget_(iDocumentWidget *d) { | 2854 | static void addBannerWarnings_DocumentWidget_(iDocumentWidget *d) { |
2855 | updateBanner_DocumentWidget_(d); | ||
2856 | /* Warnings are not shown on internal pages. */ | ||
2857 | if (equalCase_Rangecc(urlScheme_String(d->mod.url), "about")) { | ||
2858 | clear_Banner(d->banner); | ||
2859 | return; | ||
2860 | } | ||
1807 | /* Warnings related to certificates and trust. */ | 2861 | /* Warnings related to certificates and trust. */ |
1808 | const int certFlags = d->certFlags; | 2862 | const int certFlags = d->certFlags; |
1809 | const int req = timeVerified_GmCertFlag | domainVerified_GmCertFlag | trusted_GmCertFlag; | 2863 | const int req = timeVerified_GmCertFlag | domainVerified_GmCertFlag | trusted_GmCertFlag; |
@@ -1850,7 +2904,7 @@ static void addBannerWarnings_DocumentWidget_(iDocumentWidget *d) { | |||
1850 | value_SiteSpec(collectNewRange_String(urlRoot_String(d->mod.url)), | 2904 | value_SiteSpec(collectNewRange_String(urlRoot_String(d->mod.url)), |
1851 | dismissWarnings_SiteSpecKey) | | 2905 | dismissWarnings_SiteSpecKey) | |
1852 | (!prefs_App()->warnAboutMissingGlyphs ? missingGlyphs_GmDocumentWarning : 0); | 2906 | (!prefs_App()->warnAboutMissingGlyphs ? missingGlyphs_GmDocumentWarning : 0); |
1853 | const int warnings = warnings_GmDocument(d->doc) & ~dismissed; | 2907 | const int warnings = warnings_GmDocument(d->view.doc) & ~dismissed; |
1854 | if (warnings & missingGlyphs_GmDocumentWarning) { | 2908 | if (warnings & missingGlyphs_GmDocumentWarning) { |
1855 | add_Banner(d->banner, warning_BannerType, missingGlyphs_GmStatusCode, NULL, NULL); | 2909 | add_Banner(d->banner, warning_BannerType, missingGlyphs_GmStatusCode, NULL, NULL); |
1856 | /* TODO: List one or more of the missing characters and/or their Unicode blocks? */ | 2910 | /* TODO: List one or more of the missing characters and/or their Unicode blocks? */ |
@@ -1862,17 +2916,18 @@ static void addBannerWarnings_DocumentWidget_(iDocumentWidget *d) { | |||
1862 | 2916 | ||
1863 | static void updateFromCachedResponse_DocumentWidget_(iDocumentWidget *d, float normScrollY, | 2917 | static void updateFromCachedResponse_DocumentWidget_(iDocumentWidget *d, float normScrollY, |
1864 | const iGmResponse *resp, iGmDocument *cachedDoc) { | 2918 | const iGmResponse *resp, iGmDocument *cachedDoc) { |
2919 | // iAssert(width_Widget(d) > 0); /* must be laid out by now */ | ||
1865 | setLinkNumberMode_DocumentWidget_(d, iFalse); | 2920 | setLinkNumberMode_DocumentWidget_(d, iFalse); |
1866 | clear_ObjectList(d->media); | 2921 | clear_ObjectList(d->media); |
1867 | delete_Gempub(d->sourceGempub); | 2922 | delete_Gempub(d->sourceGempub); |
1868 | d->sourceGempub = NULL; | 2923 | d->sourceGempub = NULL; |
1869 | pauseAllPlayers_Media(media_GmDocument(d->doc), iTrue); | 2924 | pauseAllPlayers_Media(media_GmDocument(d->view.doc), iTrue); |
1870 | iRelease(d->doc); | ||
1871 | destroy_Widget(d->footerButtons); | 2925 | destroy_Widget(d->footerButtons); |
1872 | d->footerButtons = NULL; | 2926 | d->footerButtons = NULL; |
1873 | d->doc = new_GmDocument(); | 2927 | iRelease(d->view.doc); |
1874 | resetWideRuns_DocumentWidget_(d); | 2928 | d->view.doc = new_GmDocument(); |
1875 | d->state = fetching_RequestState; | 2929 | d->state = fetching_RequestState; |
2930 | d->flags |= fromCache_DocumentWidgetFlag; | ||
1876 | /* Do the fetch. */ { | 2931 | /* Do the fetch. */ { |
1877 | d->initNormScrollY = normScrollY; | 2932 | d->initNormScrollY = normScrollY; |
1878 | /* Use the cached response data. */ | 2933 | /* Use the cached response data. */ |
@@ -1881,36 +2936,37 @@ static void updateFromCachedResponse_DocumentWidget_(iDocumentWidget *d, float n | |||
1881 | d->sourceStatus = success_GmStatusCode; | 2936 | d->sourceStatus = success_GmStatusCode; |
1882 | format_String(&d->sourceHeader, cstr_Lang("pageinfo.header.cached")); | 2937 | format_String(&d->sourceHeader, cstr_Lang("pageinfo.header.cached")); |
1883 | set_Block(&d->sourceContent, &resp->body); | 2938 | set_Block(&d->sourceContent, &resp->body); |
2939 | if (!cachedDoc) { | ||
2940 | updateWidthAndRedoLayout_DocumentView_(&d->view); | ||
2941 | } | ||
1884 | updateDocument_DocumentWidget_(d, resp, cachedDoc, iTrue); | 2942 | updateDocument_DocumentWidget_(d, resp, cachedDoc, iTrue); |
1885 | // setCachedDocument_History(d->mod.history, d->doc, | ||
1886 | // (d->flags & openedFromSidebar_DocumentWidgetFlag) != 0); | ||
1887 | clear_Banner(d->banner); | 2943 | clear_Banner(d->banner); |
1888 | updateBanner_DocumentWidget_(d); | 2944 | updateBanner_DocumentWidget_(d); |
1889 | addBannerWarnings_DocumentWidget_(d); | 2945 | addBannerWarnings_DocumentWidget_(d); |
1890 | } | 2946 | } |
1891 | d->state = ready_RequestState; | 2947 | d->state = ready_RequestState; |
1892 | postProcessRequestContent_DocumentWidget_(d, iTrue); | 2948 | postProcessRequestContent_DocumentWidget_(d, iTrue); |
1893 | init_Anim(&d->altTextOpacity, 0); | 2949 | resetScroll_DocumentView_(&d->view); |
1894 | reset_SmoothScroll(&d->scrollY); | 2950 | init_Anim(&d->view.scrollY.pos, d->initNormScrollY * pageHeight_DocumentView_(&d->view)); |
1895 | init_Anim(&d->scrollY.pos, d->initNormScrollY * pageHeight_DocumentWidget_(d)); | 2951 | updateVisible_DocumentView_(&d->view); |
1896 | updateSideOpacity_DocumentWidget_(d, iFalse); | 2952 | moveSpan_SmoothScroll(&d->view.scrollY, 0, 0); /* clamp position to new max */ |
1897 | updateVisible_DocumentWidget_(d); | 2953 | updateSideOpacity_DocumentView_(&d->view, iFalse); |
1898 | moveSpan_SmoothScroll(&d->scrollY, 0, 0); /* clamp position to new max */ | ||
1899 | cacheDocumentGlyphs_DocumentWidget_(d); | 2954 | cacheDocumentGlyphs_DocumentWidget_(d); |
1900 | d->drawBufs->flags |= updateTimestampBuf_DrawBufsFlag | updateSideBuf_DrawBufsFlag; | 2955 | d->view.drawBufs->flags |= updateTimestampBuf_DrawBufsFlag | updateSideBuf_DrawBufsFlag; |
1901 | d->flags &= ~(urlChanged_DocumentWidgetFlag | drawDownloadCounter_DocumentWidgetFlag); | 2956 | d->flags &= ~(urlChanged_DocumentWidgetFlag | drawDownloadCounter_DocumentWidgetFlag); |
1902 | postCommandf_Root( | 2957 | postCommandf_Root( |
1903 | as_Widget(d)->root, "document.changed doc:%p url:%s", d, cstr_String(d->mod.url)); | 2958 | as_Widget(d)->root, "document.changed doc:%p url:%s", d, cstr_String(d->mod.url)); |
1904 | } | 2959 | } |
1905 | 2960 | ||
1906 | static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) { | 2961 | static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) { |
1907 | const iRecentUrl *recent = findUrl_History(d->mod.history, d->mod.url); | 2962 | const iRecentUrl *recent = constMostRecentUrl_History(d->mod.history); |
1908 | if (recent && recent->cachedResponse) { | 2963 | if (recent && recent->cachedResponse && equalCase_String(&recent->url, d->mod.url)) { |
1909 | iChangeFlags(d->flags, | ||
1910 | openedFromSidebar_DocumentWidgetFlag, | ||
1911 | recent->flags.openedFromSidebar); | ||
1912 | updateFromCachedResponse_DocumentWidget_( | 2964 | updateFromCachedResponse_DocumentWidget_( |
1913 | d, recent->normScrollY, recent->cachedResponse, recent->cachedDoc); | 2965 | d, recent->normScrollY, recent->cachedResponse, recent->cachedDoc); |
2966 | if (!recent->cachedDoc) { | ||
2967 | /* We have a cached copy now. */ | ||
2968 | setCachedDocument_History(d->mod.history, d->view.doc); | ||
2969 | } | ||
1914 | return iTrue; | 2970 | return iTrue; |
1915 | } | 2971 | } |
1916 | else if (!isEmpty_String(d->mod.url)) { | 2972 | else if (!isEmpty_String(d->mod.url)) { |
@@ -1924,23 +2980,25 @@ static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) { | |||
1924 | } | 2980 | } |
1925 | 2981 | ||
1926 | static void refreshWhileScrolling_DocumentWidget_(iAny *ptr) { | 2982 | static void refreshWhileScrolling_DocumentWidget_(iAny *ptr) { |
2983 | iAssert(isInstance_Object(ptr, &Class_DocumentWidget)); | ||
1927 | iDocumentWidget *d = ptr; | 2984 | iDocumentWidget *d = ptr; |
1928 | updateVisible_DocumentWidget_(d); | 2985 | iDocumentView *view = &d->view; |
2986 | updateVisible_DocumentView_(view); | ||
1929 | refresh_Widget(d); | 2987 | refresh_Widget(d); |
1930 | if (d->animWideRunId) { | 2988 | if (view->animWideRunId) { |
1931 | for (const iGmRun *r = d->animWideRunRange.start; r != d->animWideRunRange.end; r++) { | 2989 | for (const iGmRun *r = view->animWideRunRange.start; r != view->animWideRunRange.end; r++) { |
1932 | insert_PtrSet(d->invalidRuns, r); | 2990 | insert_PtrSet(view->invalidRuns, r); |
1933 | } | 2991 | } |
1934 | } | 2992 | } |
1935 | if (isFinished_Anim(&d->animWideRunOffset)) { | 2993 | if (isFinished_Anim(&view->animWideRunOffset)) { |
1936 | d->animWideRunId = 0; | 2994 | view->animWideRunId = 0; |
1937 | } | 2995 | } |
1938 | if (!isFinished_SmoothScroll(&d->scrollY) || !isFinished_Anim(&d->animWideRunOffset)) { | 2996 | if (!isFinished_SmoothScroll(&view->scrollY) || !isFinished_Anim(&view->animWideRunOffset)) { |
1939 | addTicker_App(refreshWhileScrolling_DocumentWidget_, d); | 2997 | addTicker_App(refreshWhileScrolling_DocumentWidget_, d); |
1940 | } | 2998 | } |
1941 | if (isFinished_SmoothScroll(&d->scrollY)) { | 2999 | if (isFinished_SmoothScroll(&view->scrollY)) { |
1942 | iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iFalse); | 3000 | iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iFalse); |
1943 | updateHover_DocumentWidget_(d, mouseCoord_Window(get_Window(), 0)); | 3001 | updateHover_DocumentView_(view, mouseCoord_Window(get_Window(), 0)); |
1944 | } | 3002 | } |
1945 | } | 3003 | } |
1946 | 3004 | ||
@@ -1949,16 +3007,16 @@ static void scrollBegan_DocumentWidget_(iAnyObject *any, int offset, uint32_t du | |||
1949 | /* Get rid of link numbers when scrolling. */ | 3007 | /* Get rid of link numbers when scrolling. */ |
1950 | if (offset && d->flags & showLinkNumbers_DocumentWidgetFlag) { | 3008 | if (offset && d->flags & showLinkNumbers_DocumentWidgetFlag) { |
1951 | setLinkNumberMode_DocumentWidget_(d, iFalse); | 3009 | setLinkNumberMode_DocumentWidget_(d, iFalse); |
1952 | invalidateVisibleLinks_DocumentWidget_(d); | 3010 | invalidateVisibleLinks_DocumentView_(&d->view); |
1953 | } | 3011 | } |
1954 | /* Show and hide toolbar on scroll. */ | 3012 | /* Show and hide toolbar on scroll. */ |
1955 | if (deviceType_App() == phone_AppDeviceType) { | 3013 | if (deviceType_App() == phone_AppDeviceType) { |
1956 | const float normPos = normScrollPos_DocumentWidget_(d); | 3014 | const float normPos = normScrollPos_DocumentView_(&d->view); |
1957 | if (prefs_App()->hideToolbarOnScroll && iAbs(offset) > 5 && normPos >= 0) { | 3015 | if (prefs_App()->hideToolbarOnScroll && iAbs(offset) > 5 && normPos >= 0) { |
1958 | showToolbar_Root(as_Widget(d)->root, offset < 0); | 3016 | showToolbar_Root(as_Widget(d)->root, offset < 0); |
1959 | } | 3017 | } |
1960 | } | 3018 | } |
1961 | updateVisible_DocumentWidget_(d); | 3019 | updateVisible_DocumentView_(&d->view); |
1962 | refresh_Widget(as_Widget(d)); | 3020 | refresh_Widget(as_Widget(d)); |
1963 | if (duration > 0) { | 3021 | if (duration > 0) { |
1964 | iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iTrue); | 3022 | iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iTrue); |
@@ -1966,98 +3024,14 @@ static void scrollBegan_DocumentWidget_(iAnyObject *any, int offset, uint32_t du | |||
1966 | } | 3024 | } |
1967 | } | 3025 | } |
1968 | 3026 | ||
1969 | static void clampScroll_DocumentWidget_(iDocumentWidget *d) { | ||
1970 | move_SmoothScroll(&d->scrollY, 0); | ||
1971 | } | ||
1972 | |||
1973 | static void immediateScroll_DocumentWidget_(iDocumentWidget *d, int offset) { | ||
1974 | move_SmoothScroll(&d->scrollY, offset); | ||
1975 | } | ||
1976 | |||
1977 | static void smoothScroll_DocumentWidget_(iDocumentWidget *d, int offset, int duration) { | ||
1978 | moveSpan_SmoothScroll(&d->scrollY, offset, duration); | ||
1979 | } | ||
1980 | |||
1981 | static void scrollTo_DocumentWidget_(iDocumentWidget *d, int documentY, iBool centered) { | ||
1982 | if (!isEmpty_Banner(d->banner)) { | ||
1983 | documentY += height_Banner(d->banner) + documentTopPad_DocumentWidget_(d); | ||
1984 | } | ||
1985 | else { | ||
1986 | documentY += documentTopPad_DocumentWidget_(d) + d->pageMargin * gap_UI; | ||
1987 | } | ||
1988 | init_Anim(&d->scrollY.pos, | ||
1989 | documentY - (centered ? documentBounds_DocumentWidget_(d).size.y / 2 | ||
1990 | : lineHeight_Text(paragraph_FontId))); | ||
1991 | clampScroll_DocumentWidget_(d); | ||
1992 | } | ||
1993 | |||
1994 | static void scrollToHeading_DocumentWidget_(iDocumentWidget *d, const char *heading) { | ||
1995 | iConstForEach(Array, h, headings_GmDocument(d->doc)) { | ||
1996 | const iGmHeading *head = h.value; | ||
1997 | if (startsWithCase_Rangecc(head->text, heading)) { | ||
1998 | postCommandf_Root(as_Widget(d)->root, "document.goto loc:%p", head->text.start); | ||
1999 | break; | ||
2000 | } | ||
2001 | } | ||
2002 | } | ||
2003 | |||
2004 | static void scrollWideBlock_DocumentWidget_(iDocumentWidget *d, iInt2 mousePos, int delta, | ||
2005 | int duration) { | ||
2006 | if (delta == 0) { | ||
2007 | return; | ||
2008 | } | ||
2009 | const iInt2 docPos = documentPos_DocumentWidget_(d, mousePos); | ||
2010 | iConstForEach(PtrArray, i, &d->visibleWideRuns) { | ||
2011 | const iGmRun *run = i.ptr; | ||
2012 | if (docPos.y >= top_Rect(run->bounds) && docPos.y <= bottom_Rect(run->bounds)) { | ||
2013 | /* We can scroll this run. First find out how much is allowed. */ | ||
2014 | const iGmRunRange range = findPreformattedRange_GmDocument(d->doc, run); | ||
2015 | int maxWidth = 0; | ||
2016 | for (const iGmRun *r = range.start; r != range.end; r++) { | ||
2017 | maxWidth = iMax(maxWidth, width_Rect(r->visBounds)); | ||
2018 | } | ||
2019 | const int maxOffset = maxWidth - documentWidth_DocumentWidget_(d) + d->pageMargin * gap_UI; | ||
2020 | if (size_Array(&d->wideRunOffsets) <= preId_GmRun(run)) { | ||
2021 | resize_Array(&d->wideRunOffsets, preId_GmRun(run) + 1); | ||
2022 | } | ||
2023 | int *offset = at_Array(&d->wideRunOffsets, preId_GmRun(run) - 1); | ||
2024 | const int oldOffset = *offset; | ||
2025 | *offset = iClamp(*offset + delta, 0, maxOffset); | ||
2026 | /* Make sure the whole block gets redraw. */ | ||
2027 | if (oldOffset != *offset) { | ||
2028 | for (const iGmRun *r = range.start; r != range.end; r++) { | ||
2029 | insert_PtrSet(d->invalidRuns, r); | ||
2030 | } | ||
2031 | refresh_Widget(d); | ||
2032 | d->selectMark = iNullRange; | ||
2033 | d->foundMark = iNullRange; | ||
2034 | } | ||
2035 | if (duration) { | ||
2036 | if (d->animWideRunId != preId_GmRun(run) || isFinished_Anim(&d->animWideRunOffset)) { | ||
2037 | d->animWideRunId = preId_GmRun(run); | ||
2038 | init_Anim(&d->animWideRunOffset, oldOffset); | ||
2039 | } | ||
2040 | setValueEased_Anim(&d->animWideRunOffset, *offset, duration); | ||
2041 | d->animWideRunRange = range; | ||
2042 | addTicker_App(refreshWhileScrolling_DocumentWidget_, d); | ||
2043 | } | ||
2044 | else { | ||
2045 | d->animWideRunId = 0; | ||
2046 | init_Anim(&d->animWideRunOffset, 0); | ||
2047 | } | ||
2048 | break; | ||
2049 | } | ||
2050 | } | ||
2051 | } | ||
2052 | |||
2053 | static void togglePreFold_DocumentWidget_(iDocumentWidget *d, uint16_t preId) { | 3027 | static void togglePreFold_DocumentWidget_(iDocumentWidget *d, uint16_t preId) { |
2054 | d->hoverPre = NULL; | 3028 | d->view.hoverPre = NULL; |
2055 | d->hoverAltPre = NULL; | 3029 | d->view.hoverAltPre = NULL; |
2056 | d->selectMark = iNullRange; | 3030 | d->selectMark = iNullRange; |
2057 | foldPre_GmDocument(d->doc, preId); | 3031 | foldPre_GmDocument(d->view.doc, preId); |
2058 | redoLayout_GmDocument(d->doc); | 3032 | redoLayout_GmDocument(d->view.doc); |
2059 | clampScroll_DocumentWidget_(d); | 3033 | clampScroll_DocumentView_(&d->view); |
2060 | updateHover_DocumentWidget_(d, mouseCoord_Window(get_Window(), 0)); | 3034 | updateHover_DocumentView_(&d->view, mouseCoord_Window(get_Window(), 0)); |
2061 | invalidate_DocumentWidget_(d); | 3035 | invalidate_DocumentWidget_(d); |
2062 | refresh_Widget(as_Widget(d)); | 3036 | refresh_Widget(as_Widget(d)); |
2063 | } | 3037 | } |
@@ -2071,7 +3045,15 @@ static iString *makeQueryUrl_DocumentWidget_(const iDocumentWidget *d, | |||
2071 | remove_Block(&url->chars, qPos, iInvalidSize); | 3045 | remove_Block(&url->chars, qPos, iInvalidSize); |
2072 | } | 3046 | } |
2073 | appendCStr_String(url, "?"); | 3047 | appendCStr_String(url, "?"); |
2074 | append_String(url, collect_String(urlEncode_String(userEnteredText))); | 3048 | iString *cleaned = copy_String(userEnteredText); |
3049 | if (deviceType_App() != desktop_AppDeviceType) { | ||
3050 | trimEnd_String(cleaned); /* autocorrect may insert an extra space */ | ||
3051 | if (isEmpty_String(cleaned)) { | ||
3052 | set_String(cleaned, userEnteredText); /* user wanted just spaces? */ | ||
3053 | } | ||
3054 | } | ||
3055 | append_String(url, collect_String(urlEncode_String(cleaned))); | ||
3056 | delete_String(cleaned); | ||
2075 | return url; | 3057 | return url; |
2076 | } | 3058 | } |
2077 | 3059 | ||
@@ -2107,6 +3089,14 @@ static const char *humanReadableStatusCode_(enum iGmStatusCode code) { | |||
2107 | return format_CStr("%d ", code); | 3089 | return format_CStr("%d ", code); |
2108 | } | 3090 | } |
2109 | 3091 | ||
3092 | static void setUrl_DocumentWidget_(iDocumentWidget *d, const iString *url) { | ||
3093 | url = canonicalUrl_String(url); | ||
3094 | if (!equal_String(d->mod.url, url)) { | ||
3095 | d->flags |= urlChanged_DocumentWidgetFlag; | ||
3096 | set_String(d->mod.url, url); | ||
3097 | } | ||
3098 | } | ||
3099 | |||
2110 | static void checkResponse_DocumentWidget_(iDocumentWidget *d) { | 3100 | static void checkResponse_DocumentWidget_(iDocumentWidget *d) { |
2111 | if (!d->request) { | 3101 | if (!d->request) { |
2112 | return; | 3102 | return; |
@@ -2117,7 +3107,35 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) { | |||
2117 | } | 3107 | } |
2118 | iGmResponse *resp = lockResponse_GmRequest(d->request); | 3108 | iGmResponse *resp = lockResponse_GmRequest(d->request); |
2119 | if (d->state == fetching_RequestState) { | 3109 | if (d->state == fetching_RequestState) { |
3110 | /* Under certain conditions, inline any image response into the current document. */ | ||
3111 | if (d->requestLinkId && | ||
3112 | isSuccess_GmStatusCode(d->sourceStatus) && | ||
3113 | startsWithCase_String(&d->sourceMime, "text/gemini") && | ||
3114 | isSuccess_GmStatusCode(statusCode) && | ||
3115 | startsWithCase_String(&resp->meta, "image/")) { | ||
3116 | /* This request is turned into a new media request in the current document. */ | ||
3117 | iDisconnect(GmRequest, d->request, updated, d, requestUpdated_DocumentWidget_); | ||
3118 | iDisconnect(GmRequest, d->request, finished, d, requestFinished_DocumentWidget_); | ||
3119 | iMediaRequest *mr = newReused_MediaRequest(d, d->requestLinkId, d->request); | ||
3120 | unlockResponse_GmRequest(d->request); | ||
3121 | d->request = NULL; /* ownership moved */ | ||
3122 | postCommand_Widget(d, "document.request.cancelled doc:%p", d); | ||
3123 | pushBack_ObjectList(d->media, mr); | ||
3124 | iRelease(mr); | ||
3125 | /* Reset the fetch state, returning to the originating page. */ | ||
3126 | d->state = ready_RequestState; | ||
3127 | if (equal_String(&mostRecentUrl_History(d->mod.history)->url, url_GmRequest(mr->req))) { | ||
3128 | undo_History(d->mod.history); | ||
3129 | } | ||
3130 | setUrl_DocumentWidget_(d, url_GmDocument(d->view.doc)); | ||
3131 | updateFetchProgress_DocumentWidget_(d); | ||
3132 | postCommand_Widget(d, "media.updated link:%u request:%p", d->requestLinkId, mr); | ||
3133 | return; | ||
3134 | } | ||
3135 | /* Get ready for the incoming new document. */ | ||
2120 | d->state = receivedPartialResponse_RequestState; | 3136 | d->state = receivedPartialResponse_RequestState; |
3137 | d->flags &= ~fromCache_DocumentWidgetFlag; | ||
3138 | clear_ObjectList(d->media); | ||
2121 | updateTrust_DocumentWidget_(d, resp); | 3139 | updateTrust_DocumentWidget_(d, resp); |
2122 | if (isSuccess_GmStatusCode(statusCode)) { | 3140 | if (isSuccess_GmStatusCode(statusCode)) { |
2123 | clear_Banner(d->banner); | 3141 | clear_Banner(d->banner); |
@@ -2128,8 +3146,8 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) { | |||
2128 | equalCase_Rangecc(urlScheme_String(d->mod.url), "gemini")) { | 3146 | equalCase_Rangecc(urlScheme_String(d->mod.url), "gemini")) { |
2129 | statusCode = tlsServerCertificateNotVerified_GmStatusCode; | 3147 | statusCode = tlsServerCertificateNotVerified_GmStatusCode; |
2130 | } | 3148 | } |
2131 | init_Anim(&d->sideOpacity, 0); | 3149 | init_Anim(&d->view.sideOpacity, 0); |
2132 | init_Anim(&d->altTextOpacity, 0); | 3150 | init_Anim(&d->view.altTextOpacity, 0); |
2133 | format_String(&d->sourceHeader, | 3151 | format_String(&d->sourceHeader, |
2134 | "%s%s", | 3152 | "%s%s", |
2135 | humanReadableStatusCode_(statusCode), | 3153 | humanReadableStatusCode_(statusCode), |
@@ -2143,7 +3161,7 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) { | |||
2143 | it is only displayed as an input dialog. */ | 3161 | it is only displayed as an input dialog. */ |
2144 | visitUrl_Visited(visited_App(), d->mod.url, transient_VisitedUrlFlag); | 3162 | visitUrl_Visited(visited_App(), d->mod.url, transient_VisitedUrlFlag); |
2145 | iUrl parts; | 3163 | iUrl parts; |
2146 | init_Url(&parts, d->mod.url); | 3164 | init_Url(&parts, d->mod.url); |
2147 | iWidget *dlg = makeValueInput_Widget( | 3165 | iWidget *dlg = makeValueInput_Widget( |
2148 | as_Widget(d), | 3166 | as_Widget(d), |
2149 | NULL, | 3167 | NULL, |
@@ -2169,19 +3187,41 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) { | |||
2169 | NULL); | 3187 | NULL); |
2170 | insertChildAfter_Widget(buttons, iClob(lineBreak), 0); | 3188 | insertChildAfter_Widget(buttons, iClob(lineBreak), 0); |
2171 | } | 3189 | } |
2172 | else { | 3190 | if (lineBreak) { |
2173 | lineBreak = new_LabelWidget("${dlg.input.linebreak}", "text.insert arg:10"); | 3191 | setFlags_Widget(as_Widget(lineBreak), frameless_WidgetFlag, iTrue); |
3192 | setTextColor_LabelWidget(lineBreak, uiTextDim_ColorId); | ||
2174 | } | 3193 | } |
2175 | setFlags_Widget(as_Widget(lineBreak), frameless_WidgetFlag, iTrue); | ||
2176 | setTextColor_LabelWidget(lineBreak, uiTextDim_ColorId); | ||
2177 | } | 3194 | } |
2178 | setId_Widget(addChildPosFlags_Widget(buttons, | 3195 | iWidget *counter = (iWidget *) new_LabelWidget("", NULL); |
2179 | iClob(new_LabelWidget("", NULL)), | 3196 | setId_Widget(counter, "valueinput.counter"); |
2180 | front_WidgetAddPos, frameless_WidgetFlag), | 3197 | setFlags_Widget(counter, frameless_WidgetFlag | resizeToParentHeight_WidgetFlag, iTrue); |
2181 | "valueinput.counter"); | 3198 | if (deviceType_App() == desktop_AppDeviceType) { |
3199 | addChildPos_Widget(buttons, iClob(counter), front_WidgetAddPos); | ||
3200 | } | ||
3201 | else { | ||
3202 | insertChildAfter_Widget(buttons, iClob(counter), 1); | ||
3203 | } | ||
2182 | if (lineBreak && deviceType_App() != desktop_AppDeviceType) { | 3204 | if (lineBreak && deviceType_App() != desktop_AppDeviceType) { |
2183 | addChildPos_Widget(buttons, iClob(lineBreak), front_WidgetAddPos); | 3205 | addChildPos_Widget(buttons, iClob(lineBreak), front_WidgetAddPos); |
2184 | } | 3206 | } |
3207 | /* Menu for additional actions, past entries. */ { | ||
3208 | iMenuItem items[] = { { "${menu.input.precedingline}", | ||
3209 | SDLK_v, | ||
3210 | KMOD_PRIMARY | KMOD_SHIFT, | ||
3211 | format_CStr("!valueinput.set ptr:%p text:%s", | ||
3212 | buttons, | ||
3213 | cstr_String(&d->linePrecedingLink)) } }; | ||
3214 | iLabelWidget *menu = makeMenuButton_LabelWidget(midEllipsis_Icon, items, 1); | ||
3215 | if (deviceType_App() == desktop_AppDeviceType) { | ||
3216 | addChildPos_Widget(buttons, iClob(menu), front_WidgetAddPos); | ||
3217 | } | ||
3218 | else { | ||
3219 | insertChildAfterFlags_Widget(buttons, iClob(menu), 0, | ||
3220 | frameless_WidgetFlag | noBackground_WidgetFlag); | ||
3221 | setFont_LabelWidget(menu, font_LabelWidget((iLabelWidget *) lastChild_Widget(buttons))); | ||
3222 | setTextColor_LabelWidget(menu, uiTextAction_ColorId); | ||
3223 | } | ||
3224 | } | ||
2185 | setValidator_InputWidget(findChild_Widget(dlg, "input"), inputQueryValidator_, d); | 3225 | setValidator_InputWidget(findChild_Widget(dlg, "input"), inputQueryValidator_, d); |
2186 | setSensitiveContent_InputWidget(findChild_Widget(dlg, "input"), | 3226 | setSensitiveContent_InputWidget(findChild_Widget(dlg, "input"), |
2187 | statusCode == sensitiveInput_GmStatusCode); | 3227 | statusCode == sensitiveInput_GmStatusCode); |
@@ -2196,16 +3236,17 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) { | |||
2196 | case categorySuccess_GmStatusCode: | 3236 | case categorySuccess_GmStatusCode: |
2197 | if (d->flags & urlChanged_DocumentWidgetFlag) { | 3237 | if (d->flags & urlChanged_DocumentWidgetFlag) { |
2198 | /* Keep scroll position when reloading the same page. */ | 3238 | /* Keep scroll position when reloading the same page. */ |
2199 | reset_SmoothScroll(&d->scrollY); | 3239 | resetScroll_DocumentView_(&d->view); |
2200 | } | 3240 | } |
2201 | pauseAllPlayers_Media(media_GmDocument(d->doc), iTrue); | 3241 | d->view.scrollY.pullActionTriggered = 0; |
2202 | iRelease(d->doc); /* new content incoming */ | 3242 | pauseAllPlayers_Media(media_GmDocument(d->view.doc), iTrue); |
2203 | d->doc = new_GmDocument(); | 3243 | iReleasePtr(&d->view.doc); /* new content incoming */ |
2204 | delete_Gempub(d->sourceGempub); | 3244 | delete_Gempub(d->sourceGempub); |
2205 | d->sourceGempub = NULL; | 3245 | d->sourceGempub = NULL; |
2206 | destroy_Widget(d->footerButtons); | 3246 | destroy_Widget(d->footerButtons); |
2207 | d->footerButtons = NULL; | 3247 | d->footerButtons = NULL; |
2208 | resetWideRuns_DocumentWidget_(d); | 3248 | d->view.doc = new_GmDocument(); |
3249 | resetWideRuns_DocumentView_(&d->view); | ||
2209 | updateDocument_DocumentWidget_(d, resp, NULL, iTrue); | 3250 | updateDocument_DocumentWidget_(d, resp, NULL, iTrue); |
2210 | break; | 3251 | break; |
2211 | case categoryRedirect_GmStatusCode: | 3252 | case categoryRedirect_GmStatusCode: |
@@ -2260,6 +3301,7 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) { | |||
2260 | } | 3301 | } |
2261 | } | 3302 | } |
2262 | else if (d->state == receivedPartialResponse_RequestState) { | 3303 | else if (d->state == receivedPartialResponse_RequestState) { |
3304 | d->flags &= ~fromCache_DocumentWidgetFlag; | ||
2263 | switch (category_GmStatusCode(statusCode)) { | 3305 | switch (category_GmStatusCode(statusCode)) { |
2264 | case categorySuccess_GmStatusCode: | 3306 | case categorySuccess_GmStatusCode: |
2265 | /* More content available. */ | 3307 | /* More content available. */ |
@@ -2272,37 +3314,6 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) { | |||
2272 | unlockResponse_GmRequest(d->request); | 3314 | unlockResponse_GmRequest(d->request); |
2273 | } | 3315 | } |
2274 | 3316 | ||
2275 | static iRangecc sourceLoc_DocumentWidget_(const iDocumentWidget *d, iInt2 pos) { | ||
2276 | return findLoc_GmDocument(d->doc, documentPos_DocumentWidget_(d, pos)); | ||
2277 | } | ||
2278 | |||
2279 | iDeclareType(MiddleRunParams) | ||
2280 | |||
2281 | struct Impl_MiddleRunParams { | ||
2282 | int midY; | ||
2283 | const iGmRun *closest; | ||
2284 | int distance; | ||
2285 | }; | ||
2286 | |||
2287 | static void find_MiddleRunParams_(void *params, const iGmRun *run) { | ||
2288 | iMiddleRunParams *d = params; | ||
2289 | if (isEmpty_Rect(run->bounds)) { | ||
2290 | return; | ||
2291 | } | ||
2292 | const int distance = iAbs(mid_Rect(run->bounds).y - d->midY); | ||
2293 | if (!d->closest || distance < d->distance) { | ||
2294 | d->closest = run; | ||
2295 | d->distance = distance; | ||
2296 | } | ||
2297 | } | ||
2298 | |||
2299 | static const iGmRun *middleRun_DocumentWidget_(const iDocumentWidget *d) { | ||
2300 | iRangei visRange = visibleRange_DocumentWidget_(d); | ||
2301 | iMiddleRunParams params = { (visRange.start + visRange.end) / 2, NULL, 0 }; | ||
2302 | render_GmDocument(d->doc, visRange, find_MiddleRunParams_, ¶ms); | ||
2303 | return params.closest; | ||
2304 | } | ||
2305 | |||
2306 | static void removeMediaRequest_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId) { | 3317 | static void removeMediaRequest_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId) { |
2307 | iForEach(ObjectList, i, d->media) { | 3318 | iForEach(ObjectList, i, d->media) { |
2308 | iMediaRequest *req = (iMediaRequest *) i.object; | 3319 | iMediaRequest *req = (iMediaRequest *) i.object; |
@@ -2313,19 +3324,9 @@ static void removeMediaRequest_DocumentWidget_(iDocumentWidget *d, iGmLinkId lin | |||
2313 | } | 3324 | } |
2314 | } | 3325 | } |
2315 | 3326 | ||
2316 | static iMediaRequest *findMediaRequest_DocumentWidget_(const iDocumentWidget *d, iGmLinkId linkId) { | ||
2317 | iConstForEach(ObjectList, i, d->media) { | ||
2318 | const iMediaRequest *req = (const iMediaRequest *) i.object; | ||
2319 | if (req->linkId == linkId) { | ||
2320 | return iConstCast(iMediaRequest *, req); | ||
2321 | } | ||
2322 | } | ||
2323 | return NULL; | ||
2324 | } | ||
2325 | |||
2326 | static iBool requestMedia_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId, iBool enableFilters) { | 3327 | static iBool requestMedia_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId, iBool enableFilters) { |
2327 | if (!findMediaRequest_DocumentWidget_(d, linkId)) { | 3328 | if (!findMediaRequest_DocumentWidget_(d, linkId)) { |
2328 | const iString *mediaUrl = absoluteUrl_String(d->mod.url, linkUrl_GmDocument(d->doc, linkId)); | 3329 | const iString *mediaUrl = absoluteUrl_String(d->mod.url, linkUrl_GmDocument(d->view.doc, linkId)); |
2329 | pushBack_ObjectList(d->media, iClob(new_MediaRequest(d, linkId, mediaUrl, enableFilters))); | 3330 | pushBack_ObjectList(d->media, iClob(new_MediaRequest(d, linkId, mediaUrl, enableFilters))); |
2330 | invalidate_DocumentWidget_(d); | 3331 | invalidate_DocumentWidget_(d); |
2331 | return iTrue; | 3332 | return iTrue; |
@@ -2334,7 +3335,7 @@ static iBool requestMedia_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId, | |||
2334 | } | 3335 | } |
2335 | 3336 | ||
2336 | static iBool isDownloadRequest_DocumentWidget(const iDocumentWidget *d, const iMediaRequest *req) { | 3337 | static iBool isDownloadRequest_DocumentWidget(const iDocumentWidget *d, const iMediaRequest *req) { |
2337 | return findMediaForLink_Media(constMedia_GmDocument(d->doc), req->linkId, download_MediaType).type != 0; | 3338 | return findMediaForLink_Media(constMedia_GmDocument(d->view.doc), req->linkId, download_MediaType).type != 0; |
2338 | } | 3339 | } |
2339 | 3340 | ||
2340 | static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) { | 3341 | static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) { |
@@ -2358,21 +3359,21 @@ static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char * | |||
2358 | if (isDownloadRequest_DocumentWidget(d, req) || | 3359 | if (isDownloadRequest_DocumentWidget(d, req) || |
2359 | startsWith_String(&resp->meta, "audio/")) { | 3360 | startsWith_String(&resp->meta, "audio/")) { |
2360 | /* TODO: Use a helper? This is same as below except for the partialData flag. */ | 3361 | /* TODO: Use a helper? This is same as below except for the partialData flag. */ |
2361 | if (setData_Media(media_GmDocument(d->doc), | 3362 | if (setData_Media(media_GmDocument(d->view.doc), |
2362 | req->linkId, | 3363 | req->linkId, |
2363 | &resp->meta, | 3364 | &resp->meta, |
2364 | &resp->body, | 3365 | &resp->body, |
2365 | partialData_MediaFlag | allowHide_MediaFlag)) { | 3366 | partialData_MediaFlag | allowHide_MediaFlag)) { |
2366 | redoLayout_GmDocument(d->doc); | 3367 | redoLayout_GmDocument(d->view.doc); |
2367 | } | 3368 | } |
2368 | updateVisible_DocumentWidget_(d); | 3369 | updateVisible_DocumentView_(&d->view); |
2369 | invalidate_DocumentWidget_(d); | 3370 | invalidate_DocumentWidget_(d); |
2370 | refresh_Widget(as_Widget(d)); | 3371 | refresh_Widget(as_Widget(d)); |
2371 | } | 3372 | } |
2372 | unlockResponse_GmRequest(req->req); | 3373 | unlockResponse_GmRequest(req->req); |
2373 | } | 3374 | } |
2374 | /* Update the link's progress. */ | 3375 | /* Update the link's progress. */ |
2375 | invalidateLink_DocumentWidget_(d, req->linkId); | 3376 | invalidateLink_DocumentView_(&d->view, req->linkId); |
2376 | refresh_Widget(d); | 3377 | refresh_Widget(d); |
2377 | return iTrue; | 3378 | return iTrue; |
2378 | } | 3379 | } |
@@ -2383,14 +3384,14 @@ static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char * | |||
2383 | if (isDownloadRequest_DocumentWidget(d, req) || | 3384 | if (isDownloadRequest_DocumentWidget(d, req) || |
2384 | startsWith_String(meta_GmRequest(req->req), "image/") || | 3385 | startsWith_String(meta_GmRequest(req->req), "image/") || |
2385 | startsWith_String(meta_GmRequest(req->req), "audio/")) { | 3386 | startsWith_String(meta_GmRequest(req->req), "audio/")) { |
2386 | setData_Media(media_GmDocument(d->doc), | 3387 | setData_Media(media_GmDocument(d->view.doc), |
2387 | req->linkId, | 3388 | req->linkId, |
2388 | meta_GmRequest(req->req), | 3389 | meta_GmRequest(req->req), |
2389 | body_GmRequest(req->req), | 3390 | body_GmRequest(req->req), |
2390 | allowHide_MediaFlag); | 3391 | allowHide_MediaFlag); |
2391 | redoLayout_GmDocument(d->doc); | 3392 | redoLayout_GmDocument(d->view.doc); |
2392 | iZap(d->visibleRuns); /* pointers invalidated */ | 3393 | iZap(d->view.visibleRuns); /* pointers invalidated */ |
2393 | updateVisible_DocumentWidget_(d); | 3394 | updateVisible_DocumentView_(&d->view); |
2394 | invalidate_DocumentWidget_(d); | 3395 | invalidate_DocumentWidget_(d); |
2395 | refresh_Widget(as_Widget(d)); | 3396 | refresh_Widget(as_Widget(d)); |
2396 | } | 3397 | } |
@@ -2405,25 +3406,13 @@ static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char * | |||
2405 | return iFalse; | 3406 | return iFalse; |
2406 | } | 3407 | } |
2407 | 3408 | ||
2408 | static void allocVisBuffer_DocumentWidget_(const iDocumentWidget *d) { | ||
2409 | const iWidget *w = constAs_Widget(d); | ||
2410 | const iBool isVisible = isVisible_Widget(w); | ||
2411 | const iInt2 size = bounds_Widget(w).size; | ||
2412 | if (isVisible) { | ||
2413 | alloc_VisBuf(d->visBuf, size, 1); | ||
2414 | } | ||
2415 | else { | ||
2416 | dealloc_VisBuf(d->visBuf); | ||
2417 | } | ||
2418 | } | ||
2419 | |||
2420 | static iBool fetchNextUnfetchedImage_DocumentWidget_(iDocumentWidget *d) { | 3409 | static iBool fetchNextUnfetchedImage_DocumentWidget_(iDocumentWidget *d) { |
2421 | iConstForEach(PtrArray, i, &d->visibleLinks) { | 3410 | iConstForEach(PtrArray, i, &d->view.visibleLinks) { |
2422 | const iGmRun *run = i.ptr; | 3411 | const iGmRun *run = i.ptr; |
2423 | if (run->linkId && run->mediaType == none_MediaType && | 3412 | if (run->linkId && run->mediaType == none_MediaType && |
2424 | ~run->flags & decoration_GmRunFlag) { | 3413 | ~run->flags & decoration_GmRunFlag) { |
2425 | const int linkFlags = linkFlags_GmDocument(d->doc, run->linkId); | 3414 | const int linkFlags = linkFlags_GmDocument(d->view.doc, run->linkId); |
2426 | if (isMediaLink_GmDocument(d->doc, run->linkId) && | 3415 | if (isMediaLink_GmDocument(d->view.doc, run->linkId) && |
2427 | linkFlags & imageFileExtension_GmLinkFlag && | 3416 | linkFlags & imageFileExtension_GmLinkFlag && |
2428 | ~linkFlags & content_GmLinkFlag && ~linkFlags & permanent_GmLinkFlag ) { | 3417 | ~linkFlags & content_GmLinkFlag && ~linkFlags & permanent_GmLinkFlag ) { |
2429 | if (requestMedia_DocumentWidget_(d, run->linkId, iTrue)) { | 3418 | if (requestMedia_DocumentWidget_(d, run->linkId, iTrue)) { |
@@ -2435,9 +3424,8 @@ static iBool fetchNextUnfetchedImage_DocumentWidget_(iDocumentWidget *d) { | |||
2435 | return iFalse; | 3424 | return iFalse; |
2436 | } | 3425 | } |
2437 | 3426 | ||
2438 | static const iString *saveToDownloads_(const iString *url, const iString *mime, const iBlock *content, | 3427 | static iBool saveToFile_(const iString *savePath, const iBlock *content, iBool showDialog) { |
2439 | iBool showDialog) { | 3428 | iBool ok = iFalse; |
2440 | const iString *savePath = downloadPathForUrl_App(url, mime); | ||
2441 | /* Write the file. */ { | 3429 | /* Write the file. */ { |
2442 | iFile *f = new_File(savePath); | 3430 | iFile *f = new_File(savePath); |
2443 | if (open_File(f, writeOnly_FileMode)) { | 3431 | if (open_File(f, writeOnly_FileMode)) { |
@@ -2449,21 +3437,21 @@ static const iString *saveToDownloads_(const iString *url, const iString *mime, | |||
2449 | exportDownloadedFile_iOS(savePath); | 3437 | exportDownloadedFile_iOS(savePath); |
2450 | #else | 3438 | #else |
2451 | if (showDialog) { | 3439 | if (showDialog) { |
2452 | const iMenuItem items[2] = { | 3440 | const iMenuItem items[2] = { |
2453 | { "${dlg.save.opendownload}", 0, 0, | 3441 | { "${dlg.save.opendownload}", 0, 0, |
2454 | format_CStr("!open url:%s", cstrCollect_String(makeFileUrl_String(savePath))) }, | 3442 | format_CStr("!open url:%s", cstrCollect_String(makeFileUrl_String(savePath))) }, |
2455 | { "${dlg.message.ok}", 0, 0, "message.ok" }, | 3443 | { "${dlg.message.ok}", 0, 0, "message.ok" }, |
2456 | }; | 3444 | }; |
2457 | makeMessage_Widget(uiHeading_ColorEscape "${heading.save}", | 3445 | makeMessage_Widget(uiHeading_ColorEscape "${heading.save}", |
2458 | format_CStr("%s\n${dlg.save.size} %.3f %s", | 3446 | format_CStr("%s\n${dlg.save.size} %.3f %s", |
2459 | cstr_String(path_File(f)), | 3447 | cstr_String(path_File(f)), |
2460 | isMega ? size / 1.0e6f : (size / 1.0e3f), | 3448 | isMega ? size / 1.0e6f : (size / 1.0e3f), |
2461 | isMega ? "${mb}" : "${kb}"), | 3449 | isMega ? "${mb}" : "${kb}"), |
2462 | items, | 3450 | items, |
2463 | iElemCount(items)); | 3451 | iElemCount(items)); |
2464 | } | 3452 | } |
2465 | #endif | 3453 | #endif |
2466 | return savePath; | 3454 | ok = iTrue; |
2467 | } | 3455 | } |
2468 | else { | 3456 | else { |
2469 | makeSimpleMessage_Widget(uiTextCaution_ColorEscape "${heading.save.error}", | 3457 | makeSimpleMessage_Widget(uiTextCaution_ColorEscape "${heading.save.error}", |
@@ -2471,7 +3459,16 @@ static const iString *saveToDownloads_(const iString *url, const iString *mime, | |||
2471 | } | 3459 | } |
2472 | iRelease(f); | 3460 | iRelease(f); |
2473 | } | 3461 | } |
2474 | return collectNew_String(); | 3462 | return ok; |
3463 | } | ||
3464 | |||
3465 | static const iString *saveToDownloads_(const iString *url, const iString *mime, const iBlock *content, | ||
3466 | iBool showDialog) { | ||
3467 | const iString *savePath = downloadPathForUrl_App(url, mime); | ||
3468 | if (!saveToFile_(savePath, content, showDialog)) { | ||
3469 | return collectNew_String(); | ||
3470 | } | ||
3471 | return savePath; | ||
2475 | } | 3472 | } |
2476 | 3473 | ||
2477 | static void addAllLinks_(void *context, const iGmRun *run) { | 3474 | static void addAllLinks_(void *context, const iGmRun *run) { |
@@ -2481,71 +3478,6 @@ static void addAllLinks_(void *context, const iGmRun *run) { | |||
2481 | } | 3478 | } |
2482 | } | 3479 | } |
2483 | 3480 | ||
2484 | static size_t visibleLinkOrdinal_DocumentWidget_(const iDocumentWidget *d, iGmLinkId linkId) { | ||
2485 | size_t ord = 0; | ||
2486 | const iRangei visRange = visibleRange_DocumentWidget_(d); | ||
2487 | iConstForEach(PtrArray, i, &d->visibleLinks) { | ||
2488 | const iGmRun *run = i.ptr; | ||
2489 | if (top_Rect(run->visBounds) >= visRange.start + gap_UI * d->pageMargin * 4 / 5) { | ||
2490 | if (run->flags & decoration_GmRunFlag && run->linkId) { | ||
2491 | if (run->linkId == linkId) return ord; | ||
2492 | ord++; | ||
2493 | } | ||
2494 | } | ||
2495 | } | ||
2496 | return iInvalidPos; | ||
2497 | } | ||
2498 | |||
2499 | /* Sorted by proximity to F and J. */ | ||
2500 | static const int homeRowKeys_[] = { | ||
2501 | 'f', 'd', 's', 'a', | ||
2502 | 'j', 'k', 'l', | ||
2503 | 'r', 'e', 'w', 'q', | ||
2504 | 'u', 'i', 'o', 'p', | ||
2505 | 'v', 'c', 'x', 'z', | ||
2506 | 'm', 'n', | ||
2507 | 'g', 'h', | ||
2508 | 'b', | ||
2509 | 't', 'y', | ||
2510 | }; | ||
2511 | |||
2512 | static iBool updateDocumentWidthRetainingScrollPosition_DocumentWidget_(iDocumentWidget *d, | ||
2513 | iBool keepCenter) { | ||
2514 | const int newWidth = documentWidth_DocumentWidget_(d); | ||
2515 | if (newWidth == size_GmDocument(d->doc).x && !keepCenter /* not a font change */) { | ||
2516 | return iFalse; | ||
2517 | } | ||
2518 | /* Font changes (i.e., zooming) will keep the view centered, otherwise keep the top | ||
2519 | of the visible area fixed. */ | ||
2520 | const iGmRun *run = keepCenter ? middleRun_DocumentWidget_(d) : d->visibleRuns.start; | ||
2521 | const char * runLoc = (run ? run->text.start : NULL); | ||
2522 | int voffset = 0; | ||
2523 | if (!keepCenter && run) { | ||
2524 | /* Keep the first visible run visible at the same position. */ | ||
2525 | /* TODO: First *fully* visible run? */ | ||
2526 | voffset = visibleRange_DocumentWidget_(d).start - top_Rect(run->visBounds); | ||
2527 | } | ||
2528 | setWidth_GmDocument(d->doc, newWidth, width_Widget(d)); | ||
2529 | setWidth_Banner(d->banner, newWidth); | ||
2530 | documentRunsInvalidated_DocumentWidget_(d); | ||
2531 | if (runLoc && !keepCenter) { | ||
2532 | run = findRunAtLoc_GmDocument(d->doc, runLoc); | ||
2533 | if (run) { | ||
2534 | scrollTo_DocumentWidget_(d, | ||
2535 | top_Rect(run->visBounds) + | ||
2536 | lineHeight_Text(paragraph_FontId) + voffset, | ||
2537 | iFalse); | ||
2538 | } | ||
2539 | } | ||
2540 | else if (runLoc && keepCenter) { | ||
2541 | run = findRunAtLoc_GmDocument(d->doc, runLoc); | ||
2542 | if (run) { | ||
2543 | scrollTo_DocumentWidget_(d, mid_Rect(run->bounds).y, iTrue); | ||
2544 | } | ||
2545 | } | ||
2546 | return iTrue; | ||
2547 | } | ||
2548 | |||
2549 | static iBool handlePinch_DocumentWidget_(iDocumentWidget *d, const char *cmd) { | 3481 | static iBool handlePinch_DocumentWidget_(iDocumentWidget *d, const char *cmd) { |
2550 | if (equal_Command(cmd, "pinch.began")) { | 3482 | if (equal_Command(cmd, "pinch.began")) { |
2551 | d->pinchZoomInitial = d->pinchZoomPosted = prefs_App()->zoomPercent; | 3483 | d->pinchZoomInitial = d->pinchZoomPosted = prefs_App()->zoomPercent; |
@@ -2577,15 +3509,12 @@ static void swap_DocumentWidget_(iDocumentWidget *d, iGmDocument *doc, | |||
2577 | iDocumentWidget *swapBuffersWith) { | 3509 | iDocumentWidget *swapBuffersWith) { |
2578 | if (doc) { | 3510 | if (doc) { |
2579 | iAssert(isInstance_Object(doc, &Class_GmDocument)); | 3511 | iAssert(isInstance_Object(doc, &Class_GmDocument)); |
2580 | iGmDocument *copy = ref_Object(doc); | 3512 | replaceDocument_DocumentWidget_(d, doc); |
2581 | iRelease(d->doc); | 3513 | iSwap(iBanner *, d->banner, swapBuffersWith->banner); |
2582 | d->doc = copy; | 3514 | setOwner_Banner(d->banner, d); |
2583 | d->scrollY = swapBuffersWith->scrollY; | 3515 | setOwner_Banner(swapBuffersWith->banner, swapBuffersWith); |
2584 | updateVisible_DocumentWidget_(d); | 3516 | swap_DocumentView_(&d->view, &swapBuffersWith->view); |
2585 | iSwap(iVisBuf *, d->visBuf, swapBuffersWith->visBuf); | 3517 | // invalidate_DocumentWidget_(swapBuffersWith); |
2586 | iSwap(iVisBufMeta *, d->visBufMeta, swapBuffersWith->visBufMeta); | ||
2587 | iSwap(iDrawBufs *, d->drawBufs, swapBuffersWith->drawBufs); | ||
2588 | invalidate_DocumentWidget_(swapBuffersWith); | ||
2589 | } | 3518 | } |
2590 | } | 3519 | } |
2591 | 3520 | ||
@@ -2593,11 +3522,49 @@ static iWidget *swipeParent_DocumentWidget_(iDocumentWidget *d) { | |||
2593 | return findChild_Widget(as_Widget(d)->root->widget, "doctabs"); | 3522 | return findChild_Widget(as_Widget(d)->root->widget, "doctabs"); |
2594 | } | 3523 | } |
2595 | 3524 | ||
3525 | static void setupSwipeOverlay_DocumentWidget_(iDocumentWidget *d, iWidget *overlay) { | ||
3526 | iWidget *w = as_Widget(d); | ||
3527 | iWidget *swipeParent = swipeParent_DocumentWidget_(d); | ||
3528 | /* The target takes the old document and jumps on top. */ | ||
3529 | overlay->rect.pos = windowToInner_Widget(swipeParent, innerToWindow_Widget(w, zero_I2())); | ||
3530 | /* Note: `innerToWindow_Widget` does not apply visual offset. */ | ||
3531 | overlay->rect.size = w->rect.size; | ||
3532 | setFlags_Widget(overlay, fixedPosition_WidgetFlag | fixedSize_WidgetFlag, iTrue); | ||
3533 | // swap_DocumentWidget_(target, d->doc, d); | ||
3534 | setFlags_Widget(as_Widget(d), refChildrenOffset_WidgetFlag, iTrue); | ||
3535 | as_Widget(d)->offsetRef = swipeParent; | ||
3536 | /* `overlay` animates off the screen to the right. */ | ||
3537 | const int fromPos = value_Anim(&w->visualOffset); | ||
3538 | const int toPos = width_Widget(overlay); | ||
3539 | setVisualOffset_Widget(overlay, fromPos, 0, 0); | ||
3540 | /* Bigger screen, faster swipes. */ | ||
3541 | if (deviceType_App() == desktop_AppDeviceType) { | ||
3542 | setVisualOffset_Widget(overlay, toPos, 250, easeOut_AnimFlag | softer_AnimFlag); | ||
3543 | } | ||
3544 | else { | ||
3545 | const float devFactor = (deviceType_App() == phone_AppDeviceType ? 1.0f : 2.0f); | ||
3546 | float swipe = iClamp(d->swipeSpeed, devFactor * 400, devFactor * 1000) * gap_UI; | ||
3547 | uint32_t span = ((toPos - fromPos) / swipe) * 1000; | ||
3548 | // printf("from:%d to:%d swipe:%f span:%u\n", fromPos, toPos, d->swipeSpeed, span); | ||
3549 | setVisualOffset_Widget(overlay, toPos, span, deviceType_App() == tablet_AppDeviceType ? | ||
3550 | easeOut_AnimFlag : 0); | ||
3551 | } | ||
3552 | setVisualOffset_Widget(w, 0, 0, 0); | ||
3553 | } | ||
3554 | |||
2596 | static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { | 3555 | static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { |
3556 | /* TODO: Cleanup | ||
3557 | If DocumentWidget is refactored to split the document presentation from state | ||
3558 | and request management (a new DocumentView class), plain views could be used for this | ||
3559 | animation without having to mess with the complete state of the DocumentWidget. That | ||
3560 | seems like a less error-prone approach -- the current implementation will likely break | ||
3561 | down (again) if anything is changed in the document internals. | ||
3562 | */ | ||
2597 | iWidget *w = as_Widget(d); | 3563 | iWidget *w = as_Widget(d); |
2598 | /* Swipe animations are rather complex and utilize both cached GmDocument content | 3564 | /* The swipe animation is implemented in a rather complex way. It utilizes both cached |
2599 | and temporary DocumentWidgets. Depending on the swipe direction, this DocumentWidget | 3565 | GmDocument content and temporary underlay/overlay DocumentWidgets. Depending on the |
2600 | may wait until the finger is released to actually perform the navigation action. */ | 3566 | swipe direction, the DocumentWidget `d` may wait until the finger is released to actually |
3567 | perform the navigation action. */ | ||
2601 | if (equal_Command(cmd, "edgeswipe.moved")) { | 3568 | if (equal_Command(cmd, "edgeswipe.moved")) { |
2602 | //printf("[%p] responds to edgeswipe.moved\n", d); | 3569 | //printf("[%p] responds to edgeswipe.moved\n", d); |
2603 | as_Widget(d)->offsetRef = NULL; | 3570 | as_Widget(d)->offsetRef = NULL; |
@@ -2608,32 +3575,40 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { | |||
2608 | return iTrue; | 3575 | return iTrue; |
2609 | } | 3576 | } |
2610 | iWidget *swipeParent = swipeParent_DocumentWidget_(d); | 3577 | iWidget *swipeParent = swipeParent_DocumentWidget_(d); |
2611 | /* The temporary "swipeIn" will display the previous page until the finger is lifted. */ | 3578 | if (findChild_Widget(swipeParent, "swipeout")) { |
3579 | return iTrue; /* too fast, previous animation hasn't finished */ | ||
3580 | } | ||
3581 | /* The temporary "swipein" will display the previous page until the finger is lifted. */ | ||
2612 | iDocumentWidget *swipeIn = findChild_Widget(swipeParent, "swipein"); | 3582 | iDocumentWidget *swipeIn = findChild_Widget(swipeParent, "swipein"); |
2613 | if (!swipeIn) { | 3583 | if (!swipeIn) { |
2614 | const iBool sidebarSwipe = (isPortraitPhone_App() && | ||
2615 | d->flags & openedFromSidebar_DocumentWidgetFlag && | ||
2616 | !isVisible_Widget(findWidget_App("sidebar"))); | ||
2617 | swipeIn = new_DocumentWidget(); | 3584 | swipeIn = new_DocumentWidget(); |
3585 | swipeIn->flags |= animationPlaceholder_DocumentWidgetFlag; | ||
2618 | setId_Widget(as_Widget(swipeIn), "swipein"); | 3586 | setId_Widget(as_Widget(swipeIn), "swipein"); |
2619 | setFlags_Widget(as_Widget(swipeIn), | 3587 | setFlags_Widget(as_Widget(swipeIn), |
2620 | disabled_WidgetFlag | refChildrenOffset_WidgetFlag | | 3588 | disabled_WidgetFlag | refChildrenOffset_WidgetFlag | |
2621 | fixedPosition_WidgetFlag | fixedSize_WidgetFlag, iTrue); | 3589 | fixedPosition_WidgetFlag | fixedSize_WidgetFlag, iTrue); |
3590 | setFlags_Widget(findChild_Widget(as_Widget(swipeIn), "scroll"), hidden_WidgetFlag, iTrue); | ||
2622 | swipeIn->widget.rect.pos = windowToInner_Widget(swipeParent, localToWindow_Widget(w, w->rect.pos)); | 3591 | swipeIn->widget.rect.pos = windowToInner_Widget(swipeParent, localToWindow_Widget(w, w->rect.pos)); |
2623 | swipeIn->widget.rect.size = d->widget.rect.size; | 3592 | swipeIn->widget.rect.size = d->widget.rect.size; |
2624 | swipeIn->widget.offsetRef = parent_Widget(w); | 3593 | swipeIn->widget.offsetRef = parent_Widget(w); |
2625 | if (!sidebarSwipe) { | 3594 | /* Use a cached document for the layer underneath. */ { |
2626 | iRecentUrl *recent = new_RecentUrl(); | 3595 | lock_History(d->mod.history); |
2627 | preceding_History(d->mod.history, recent); | 3596 | iRecentUrl *recent = precedingLocked_History(d->mod.history); |
2628 | if (recent->cachedDoc) { | 3597 | if (recent && recent->cachedResponse) { |
2629 | iChangeRef(swipeIn->doc, recent->cachedDoc); | 3598 | setUrl_DocumentWidget_(swipeIn, &recent->url); |
2630 | updateScrollMax_DocumentWidget_(d); | 3599 | updateFromCachedResponse_DocumentWidget_(swipeIn, |
2631 | setValue_Anim(&swipeIn->scrollY.pos, | 3600 | recent->normScrollY, |
2632 | pageHeight_DocumentWidget_(d) * recent->normScrollY, 0); | 3601 | recent->cachedResponse, |
2633 | updateVisible_DocumentWidget_(swipeIn); | 3602 | recent->cachedDoc); |
2634 | swipeIn->drawBufs->flags |= updateTimestampBuf_DrawBufsFlag | updateSideBuf_DrawBufsFlag; | 3603 | parseUser_DocumentWidget_(swipeIn); |
3604 | updateBanner_DocumentWidget_(swipeIn); | ||
2635 | } | 3605 | } |
2636 | delete_RecentUrl(recent); | 3606 | else { |
3607 | setUrlAndSource_DocumentWidget(swipeIn, &recent->url, | ||
3608 | collectNewCStr_String("text/gemini"), | ||
3609 | collect_Block(new_Block(0))); | ||
3610 | } | ||
3611 | unlock_History(d->mod.history); | ||
2637 | } | 3612 | } |
2638 | addChildPos_Widget(swipeParent, iClob(swipeIn), front_WidgetAddPos); | 3613 | addChildPos_Widget(swipeParent, iClob(swipeIn), front_WidgetAddPos); |
2639 | } | 3614 | } |
@@ -2641,24 +3616,36 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { | |||
2641 | if (side == 2) { /* right edge */ | 3616 | if (side == 2) { /* right edge */ |
2642 | if (offset < -get_Window()->pixelRatio * 10) { | 3617 | if (offset < -get_Window()->pixelRatio * 10) { |
2643 | int animSpan = 10; | 3618 | int animSpan = 10; |
2644 | if (!atLatest_History(d->mod.history) && | 3619 | if (!atNewest_History(d->mod.history) && ~flags_Widget(w) & dragged_WidgetFlag) { |
2645 | ~flags_Widget(w) & dragged_WidgetFlag) { | 3620 | iWidget *swipeParent = swipeParent_DocumentWidget_(d); |
3621 | if (findChild_Widget(swipeParent, "swipeout")) { | ||
3622 | return iTrue; /* too fast, previous animation hasn't finished */ | ||
3623 | } | ||
3624 | /* Setup the drag. `d` will be moving with the finger. */ | ||
2646 | animSpan = 0; | 3625 | animSpan = 0; |
2647 | postCommand_Widget(d, "navigate.forward"); | 3626 | postCommand_Widget(d, "navigate.forward"); |
2648 | setFlags_Widget(w, dragged_WidgetFlag, iTrue); | 3627 | setFlags_Widget(w, dragged_WidgetFlag, iTrue); |
2649 | /* Set up the swipe dummy. */ | 3628 | /* Set up the swipe dummy. */ |
2650 | iWidget *swipeParent = swipeParent_DocumentWidget_(d); | ||
2651 | iDocumentWidget *target = new_DocumentWidget(); | 3629 | iDocumentWidget *target = new_DocumentWidget(); |
3630 | target->flags |= animationPlaceholder_DocumentWidgetFlag; | ||
2652 | setId_Widget(as_Widget(target), "swipeout"); | 3631 | setId_Widget(as_Widget(target), "swipeout"); |
2653 | /* The target takes the old document and jumps on top. */ | 3632 | /* "swipeout" takes `d`'s document and goes underneath. */ |
2654 | target->widget.rect.pos = windowToInner_Widget(swipeParent, localToWindow_Widget(w, w->rect.pos)); | 3633 | target->widget.rect.pos = windowToInner_Widget(swipeParent, localToWindow_Widget(w, w->rect.pos)); |
2655 | target->widget.rect.size = d->widget.rect.size; | 3634 | target->widget.rect.size = d->widget.rect.size; |
2656 | setFlags_Widget(as_Widget(target), fixedPosition_WidgetFlag | fixedSize_WidgetFlag, iTrue); | 3635 | setFlags_Widget(as_Widget(target), fixedPosition_WidgetFlag | fixedSize_WidgetFlag, iTrue); |
2657 | swap_DocumentWidget_(target, d->doc, d); | 3636 | swap_DocumentWidget_(target, d->view.doc, d); |
2658 | addChildPos_Widget(swipeParent, iClob(target), front_WidgetAddPos); | 3637 | addChildPos_Widget(swipeParent, iClob(target), front_WidgetAddPos); |
2659 | setFlags_Widget(as_Widget(target), refChildrenOffset_WidgetFlag, iTrue); | 3638 | setFlags_Widget(as_Widget(target), refChildrenOffset_WidgetFlag, iTrue); |
2660 | as_Widget(target)->offsetRef = parent_Widget(w); | 3639 | as_Widget(target)->offsetRef = parent_Widget(w); |
2661 | destroy_Widget(as_Widget(target)); /* will be actually deleted after animation finishes */ | 3640 | /* Mark it for deletion after animation finishes. */ |
3641 | destroy_Widget(as_Widget(target)); | ||
3642 | /* The `d` document will now navigate forward and be replaced with a cached | ||
3643 | copy. However, if a cached response isn't available, we'll need to show a | ||
3644 | blank page. */ | ||
3645 | setUrlAndSource_DocumentWidget(d, | ||
3646 | collectNewCStr_String("about:blank"), | ||
3647 | collectNewCStr_String("text/gemini"), | ||
3648 | collect_Block(new_Block(0))); | ||
2662 | } | 3649 | } |
2663 | if (flags_Widget(w) & dragged_WidgetFlag) { | 3650 | if (flags_Widget(w) & dragged_WidgetFlag) { |
2664 | setVisualOffset_Widget(w, width_Widget(w) + | 3651 | setVisualOffset_Widget(w, width_Widget(w) + |
@@ -2674,41 +3661,68 @@ static iBool handleSwipe_DocumentWidget_(iDocumentWidget *d, const char *cmd) { | |||
2674 | } | 3661 | } |
2675 | if (equal_Command(cmd, "edgeswipe.ended") && argLabel_Command(cmd, "side") == 2) { | 3662 | if (equal_Command(cmd, "edgeswipe.ended") && argLabel_Command(cmd, "side") == 2) { |
2676 | if (argLabel_Command(cmd, "abort") && flags_Widget(w) & dragged_WidgetFlag) { | 3663 | if (argLabel_Command(cmd, "abort") && flags_Widget(w) & dragged_WidgetFlag) { |
3664 | setFlags_Widget(w, dragged_WidgetFlag, iFalse); | ||
2677 | postCommand_Widget(d, "navigate.back"); | 3665 | postCommand_Widget(d, "navigate.back"); |
3666 | /* We must now undo the swap that was done when the drag started. */ | ||
3667 | /* TODO: Currently not animated! What exactly is the appropriate thing to do here? */ | ||
3668 | iWidget *swipeParent = swipeParent_DocumentWidget_(d); | ||
3669 | iDocumentWidget *swipeOut = findChild_Widget(swipeParent, "swipeout"); | ||
3670 | swap_DocumentWidget_(d, swipeOut->view.doc, swipeOut); | ||
3671 | // const int visOff = visualOffsetByReference_Widget(w); | ||
3672 | w->offsetRef = NULL; | ||
3673 | // setVisualOffset_Widget(w, visOff, 0, 0); | ||
3674 | // setVisualOffset_Widget(w, 0, 150, 0); | ||
3675 | setVisualOffset_Widget(w, 0, 0, 0); | ||
3676 | /* Make it an overlay instead. */ | ||
3677 | // removeChild_Widget(swipeParent, swipeOut); | ||
3678 | // addChildPos_Widget(swipeParent, iClob(swipeOut), back_WidgetAddPos); | ||
3679 | // setupSwipeOverlay_DocumentWidget_(d, as_Widget(swipeOut)); | ||
3680 | return iTrue; | ||
2678 | } | 3681 | } |
3682 | iAssert(~d->flags & animationPlaceholder_DocumentWidgetFlag); | ||
2679 | setFlags_Widget(w, dragged_WidgetFlag, iFalse); | 3683 | setFlags_Widget(w, dragged_WidgetFlag, iFalse); |
2680 | setVisualOffset_Widget(w, 0, 100, 0); | 3684 | setVisualOffset_Widget(w, 0, 250, easeOut_AnimFlag | softer_AnimFlag); |
2681 | return iTrue; | 3685 | return iTrue; |
2682 | } | 3686 | } |
2683 | if (equal_Command(cmd, "edgeswipe.ended") && argLabel_Command(cmd, "side") == 1) { | 3687 | if (equal_Command(cmd, "edgeswipe.ended") && argLabel_Command(cmd, "side") == 1) { |
2684 | iWidget *swipeParent = swipeParent_DocumentWidget_(d); | 3688 | iWidget *swipeParent = swipeParent_DocumentWidget_(d); |
2685 | iWidget *swipeIn = findChild_Widget(swipeParent, "swipein"); | 3689 | iDocumentWidget *swipeIn = findChild_Widget(swipeParent, "swipein"); |
3690 | d->swipeSpeed = argLabel_Command(cmd, "speed") / gap_UI; | ||
3691 | /* "swipe.back" will soon follow. The `d` document will do the actual back navigation, | ||
3692 | switching immediately to a cached page. However, if one is not available, we'll need | ||
3693 | to show a blank page for a while. */ | ||
2686 | if (swipeIn) { | 3694 | if (swipeIn) { |
2687 | swipeIn->offsetRef = NULL; | 3695 | if (!argLabel_Command(cmd, "abort")) { |
2688 | destroy_Widget(swipeIn); | 3696 | iWidget *swipeParent = swipeParent_DocumentWidget_(d); |
3697 | /* What was being shown in the `d` document is now being swapped to | ||
3698 | the outgoing page animation. */ | ||
3699 | iDocumentWidget *target = new_DocumentWidget(); | ||
3700 | target->flags |= animationPlaceholder_DocumentWidgetFlag; | ||
3701 | addChildPos_Widget(swipeParent, iClob(target), back_WidgetAddPos); | ||
3702 | setId_Widget(as_Widget(target), "swipeout"); | ||
3703 | setFlags_Widget(as_Widget(target), disabled_WidgetFlag, iTrue); | ||
3704 | swap_DocumentWidget_(target, d->view.doc, d); | ||
3705 | setUrlAndSource_DocumentWidget(d, | ||
3706 | swipeIn->mod.url, | ||
3707 | collectNewCStr_String("text/gemini"), | ||
3708 | collect_Block(new_Block(0))); | ||
3709 | as_Widget(swipeIn)->offsetRef = NULL; | ||
3710 | } | ||
3711 | destroy_Widget(as_Widget(swipeIn)); | ||
2689 | } | 3712 | } |
2690 | } | 3713 | } |
2691 | if (equal_Command(cmd, "swipe.back")) { | 3714 | if (equal_Command(cmd, "swipe.back")) { |
3715 | iWidget *swipeParent = swipeParent_DocumentWidget_(d); | ||
3716 | iDocumentWidget *target = findChild_Widget(swipeParent, "swipeout"); | ||
2692 | if (atOldest_History(d->mod.history)) { | 3717 | if (atOldest_History(d->mod.history)) { |
2693 | setVisualOffset_Widget(w, 0, 100, 0); | 3718 | setVisualOffset_Widget(w, 0, 100, 0); |
3719 | if (target) { | ||
3720 | destroy_Widget(as_Widget(target)); /* didn't need it after all */ | ||
3721 | } | ||
2694 | return iTrue; | 3722 | return iTrue; |
2695 | } | 3723 | } |
2696 | iWidget *swipeParent = swipeParent_DocumentWidget_(d); | 3724 | setupSwipeOverlay_DocumentWidget_(d, as_Widget(target)); |
2697 | iDocumentWidget *target = new_DocumentWidget(); | ||
2698 | setId_Widget(as_Widget(target), "swipeout"); | ||
2699 | /* The target takes the old document and jumps on top. */ | ||
2700 | target->widget.rect.pos = windowToInner_Widget(swipeParent, innerToWindow_Widget(w, zero_I2())); | ||
2701 | /* Note: `innerToWindow_Widget` does not apply visual offset. */ | ||
2702 | target->widget.rect.size = w->rect.size; | ||
2703 | setFlags_Widget(as_Widget(target), fixedPosition_WidgetFlag | fixedSize_WidgetFlag, iTrue); | ||
2704 | swap_DocumentWidget_(target, d->doc, d); | ||
2705 | addChildPos_Widget(swipeParent, iClob(target), back_WidgetAddPos); | ||
2706 | setFlags_Widget(as_Widget(d), refChildrenOffset_WidgetFlag, iTrue); | ||
2707 | as_Widget(d)->offsetRef = swipeParent; | ||
2708 | setVisualOffset_Widget(as_Widget(target), value_Anim(&w->visualOffset), 0, 0); | ||
2709 | setVisualOffset_Widget(as_Widget(target), width_Widget(target), 150, 0); | ||
2710 | destroy_Widget(as_Widget(target)); /* will be actually deleted after animation finishes */ | 3725 | destroy_Widget(as_Widget(target)); /* will be actually deleted after animation finishes */ |
2711 | setVisualOffset_Widget(w, 0, 0, 0); | ||
2712 | postCommand_Widget(d, "navigate.back"); | 3726 | postCommand_Widget(d, "navigate.back"); |
2713 | return iTrue; | 3727 | return iTrue; |
2714 | } | 3728 | } |
@@ -2733,27 +3747,34 @@ static iBool cancelRequest_DocumentWidget_(iDocumentWidget *d, iBool postBack) { | |||
2733 | return iFalse; | 3747 | return iFalse; |
2734 | } | 3748 | } |
2735 | 3749 | ||
3750 | static const int smoothDuration_DocumentWidget_(enum iScrollType type) { | ||
3751 | return 600 /* milliseconds */ * scrollSpeedFactor_Prefs(prefs_App(), type); | ||
3752 | } | ||
3753 | |||
2736 | static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) { | 3754 | static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) { |
2737 | iWidget *w = as_Widget(d); | 3755 | iWidget *w = as_Widget(d); |
2738 | if (equal_Command(cmd, "document.openurls.changed")) { | 3756 | if (equal_Command(cmd, "document.openurls.changed")) { |
3757 | if (d->flags & animationPlaceholder_DocumentWidgetFlag) { | ||
3758 | return iFalse; | ||
3759 | } | ||
2739 | /* When any tab changes its document URL, update the open link indicators. */ | 3760 | /* When any tab changes its document URL, update the open link indicators. */ |
2740 | if (updateOpenURLs_GmDocument(d->doc)) { | 3761 | if (updateOpenURLs_GmDocument(d->view.doc)) { |
2741 | invalidate_DocumentWidget_(d); | 3762 | invalidate_DocumentWidget_(d); |
2742 | refresh_Widget(d); | 3763 | refresh_Widget(d); |
2743 | } | 3764 | } |
2744 | return iFalse; | 3765 | return iFalse; |
2745 | } | 3766 | } |
2746 | if (equal_Command(cmd, "visited.changed")) { | 3767 | if (equal_Command(cmd, "visited.changed")) { |
2747 | updateVisitedLinks_GmDocument(d->doc); | 3768 | updateVisitedLinks_GmDocument(d->view.doc); |
2748 | invalidateVisibleLinks_DocumentWidget_(d); | 3769 | invalidateVisibleLinks_DocumentView_(&d->view); |
2749 | return iFalse; | 3770 | return iFalse; |
2750 | } | 3771 | } |
2751 | if (equal_Command(cmd, "document.render")) /* `Periodic` makes direct dispatch to here */ { | 3772 | if (equal_Command(cmd, "document.render")) /* `Periodic` makes direct dispatch to here */ { |
2752 | // printf("%u: document.render\n", SDL_GetTicks()); | 3773 | // printf("%u: document.render\n", SDL_GetTicks()); |
2753 | if (SDL_GetTicks() - d->drawBufs->lastRenderTime > 150) { | 3774 | if (SDL_GetTicks() - d->view.drawBufs->lastRenderTime > 150) { |
2754 | remove_Periodic(periodic_App(), d); | 3775 | remove_Periodic(periodic_App(), d); |
2755 | /* Scrolling has stopped, begin filling up the buffer. */ | 3776 | /* Scrolling has stopped, begin filling up the buffer. */ |
2756 | if (d->visBuf->buffers[0].texture) { | 3777 | if (d->view.visBuf->buffers[0].texture) { |
2757 | addTicker_App(prerender_DocumentWidget_, d); | 3778 | addTicker_App(prerender_DocumentWidget_, d); |
2758 | } | 3779 | } |
2759 | } | 3780 | } |
@@ -2762,17 +3783,17 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
2762 | else if (equal_Command(cmd, "window.resized") || equal_Command(cmd, "font.changed") || | 3783 | else if (equal_Command(cmd, "window.resized") || equal_Command(cmd, "font.changed") || |
2763 | equal_Command(cmd, "keyroot.changed")) { | 3784 | equal_Command(cmd, "keyroot.changed")) { |
2764 | if (equal_Command(cmd, "font.changed")) { | 3785 | if (equal_Command(cmd, "font.changed")) { |
2765 | invalidateCachedLayout_History(d->mod.history); | 3786 | invalidateCachedLayout_History(d->mod.history); |
2766 | } | 3787 | } |
2767 | /* Alt/Option key may be involved in window size changes. */ | 3788 | /* Alt/Option key may be involved in window size changes. */ |
2768 | setLinkNumberMode_DocumentWidget_(d, iFalse); | 3789 | setLinkNumberMode_DocumentWidget_(d, iFalse); |
2769 | d->phoneToolbar = findWidget_App("toolbar"); | 3790 | d->phoneToolbar = findWidget_App("toolbar"); |
2770 | const iBool keepCenter = equal_Command(cmd, "font.changed"); | 3791 | const iBool keepCenter = equal_Command(cmd, "font.changed"); |
2771 | updateDocumentWidthRetainingScrollPosition_DocumentWidget_(d, keepCenter); | 3792 | updateDocumentWidthRetainingScrollPosition_DocumentView_(&d->view, keepCenter); |
2772 | d->drawBufs->flags |= updateSideBuf_DrawBufsFlag; | 3793 | d->view.drawBufs->flags |= updateSideBuf_DrawBufsFlag; |
2773 | updateVisible_DocumentWidget_(d); | 3794 | updateVisible_DocumentView_(&d->view); |
2774 | invalidate_DocumentWidget_(d); | 3795 | invalidate_DocumentWidget_(d); |
2775 | dealloc_VisBuf(d->visBuf); | 3796 | dealloc_VisBuf(d->view.visBuf); |
2776 | updateWindowTitle_DocumentWidget_(d); | 3797 | updateWindowTitle_DocumentWidget_(d); |
2777 | showOrHidePinningIndicator_DocumentWidget_(d); | 3798 | showOrHidePinningIndicator_DocumentWidget_(d); |
2778 | refresh_Widget(w); | 3799 | refresh_Widget(w); |
@@ -2780,7 +3801,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
2780 | else if (equal_Command(cmd, "window.focus.lost")) { | 3801 | else if (equal_Command(cmd, "window.focus.lost")) { |
2781 | if (d->flags & showLinkNumbers_DocumentWidgetFlag) { | 3802 | if (d->flags & showLinkNumbers_DocumentWidgetFlag) { |
2782 | setLinkNumberMode_DocumentWidget_(d, iFalse); | 3803 | setLinkNumberMode_DocumentWidget_(d, iFalse); |
2783 | invalidateVisibleLinks_DocumentWidget_(d); | 3804 | invalidateVisibleLinks_DocumentView_(&d->view); |
2784 | refresh_Widget(w); | 3805 | refresh_Widget(w); |
2785 | } | 3806 | } |
2786 | return iFalse; | 3807 | return iFalse; |
@@ -2788,18 +3809,21 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
2788 | else if (equal_Command(cmd, "window.mouse.exited")) { | 3809 | else if (equal_Command(cmd, "window.mouse.exited")) { |
2789 | return iFalse; | 3810 | return iFalse; |
2790 | } | 3811 | } |
2791 | else if (equal_Command(cmd, "theme.changed") && document_App() == d) { | 3812 | else if (equal_Command(cmd, "theme.changed")) { |
2792 | // invalidateTheme_History(d->mod.history); /* cached colors */ | 3813 | invalidatePalette_GmDocument(d->view.doc); |
2793 | updateTheme_DocumentWidget_(d); | 3814 | invalidateTheme_History(d->mod.history); /* forget cached color palettes */ |
2794 | updateVisible_DocumentWidget_(d); | 3815 | if (document_App() == d) { |
2795 | updateTrust_DocumentWidget_(d, NULL); | 3816 | updateTheme_DocumentWidget_(d); |
2796 | d->drawBufs->flags |= updateSideBuf_DrawBufsFlag; | 3817 | updateVisible_DocumentView_(&d->view); |
2797 | invalidate_DocumentWidget_(d); | 3818 | updateTrust_DocumentWidget_(d, NULL); |
2798 | refresh_Widget(w); | 3819 | d->view.drawBufs->flags |= updateSideBuf_DrawBufsFlag; |
3820 | invalidate_DocumentWidget_(d); | ||
3821 | refresh_Widget(w); | ||
3822 | } | ||
2799 | } | 3823 | } |
2800 | else if (equal_Command(cmd, "document.layout.changed") && document_Root(get_Root()) == d) { | 3824 | else if (equal_Command(cmd, "document.layout.changed") && document_Root(get_Root()) == d) { |
2801 | if (argLabel_Command(cmd, "redo")) { | 3825 | if (argLabel_Command(cmd, "redo")) { |
2802 | redoLayout_GmDocument(d->doc); | 3826 | redoLayout_GmDocument(d->view.doc); |
2803 | } | 3827 | } |
2804 | updateSize_DocumentWidget(d); | 3828 | updateSize_DocumentWidget(d); |
2805 | } | 3829 | } |
@@ -2822,11 +3846,11 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
2822 | updateFetchProgress_DocumentWidget_(d); | 3846 | updateFetchProgress_DocumentWidget_(d); |
2823 | updateHover_Window(window_Widget(w)); | 3847 | updateHover_Window(window_Widget(w)); |
2824 | } | 3848 | } |
2825 | init_Anim(&d->sideOpacity, 0); | 3849 | init_Anim(&d->view.sideOpacity, 0); |
2826 | init_Anim(&d->altTextOpacity, 0); | 3850 | init_Anim(&d->view.altTextOpacity, 0); |
2827 | updateSideOpacity_DocumentWidget_(d, iFalse); | 3851 | updateSideOpacity_DocumentView_(&d->view, iFalse); |
2828 | updateWindowTitle_DocumentWidget_(d); | 3852 | updateWindowTitle_DocumentWidget_(d); |
2829 | allocVisBuffer_DocumentWidget_(d); | 3853 | allocVisBuffer_DocumentView_(&d->view); |
2830 | animateMedia_DocumentWidget_(d); | 3854 | animateMedia_DocumentWidget_(d); |
2831 | remove_Periodic(periodic_App(), d); | 3855 | remove_Periodic(periodic_App(), d); |
2832 | removeTicker_App(prerender_DocumentWidget_, d); | 3856 | removeTicker_App(prerender_DocumentWidget_, d); |
@@ -2850,8 +3874,8 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
2850 | selectWords_DocumentWidgetFlag; /* finger-based selection is imprecise */ | 3874 | selectWords_DocumentWidgetFlag; /* finger-based selection is imprecise */ |
2851 | d->flags &= ~selectLines_DocumentWidgetFlag; | 3875 | d->flags &= ~selectLines_DocumentWidgetFlag; |
2852 | setFadeEnabled_ScrollWidget(d->scroll, iFalse); | 3876 | setFadeEnabled_ScrollWidget(d->scroll, iFalse); |
2853 | d->selectMark = sourceLoc_DocumentWidget_(d, d->contextPos); | 3877 | d->selectMark = sourceLoc_DocumentView_(&d->view, d->contextPos); |
2854 | extendRange_Rangecc(&d->selectMark, range_String(source_GmDocument(d->doc)), | 3878 | extendRange_Rangecc(&d->selectMark, range_String(source_GmDocument(d->view.doc)), |
2855 | word_RangeExtension | bothStartAndEnd_RangeExtension); | 3879 | word_RangeExtension | bothStartAndEnd_RangeExtension); |
2856 | d->initialSelectMark = d->selectMark; | 3880 | d->initialSelectMark = d->selectMark; |
2857 | } | 3881 | } |
@@ -2865,7 +3889,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
2865 | timeVerified_GmCertFlag); | 3889 | timeVerified_GmCertFlag); |
2866 | const iBool canTrust = ~d->certFlags & trusted_GmCertFlag && | 3890 | const iBool canTrust = ~d->certFlags & trusted_GmCertFlag && |
2867 | ((d->certFlags & requiredForTrust) == requiredForTrust); | 3891 | ((d->certFlags & requiredForTrust) == requiredForTrust); |
2868 | const iRecentUrl *recent = findUrl_History(d->mod.history, d->mod.url); | 3892 | const iRecentUrl *recent = constMostRecentUrl_History(d->mod.history); |
2869 | const iString *meta = &d->sourceMime; | 3893 | const iString *meta = &d->sourceMime; |
2870 | if (recent && recent->cachedResponse) { | 3894 | if (recent && recent->cachedResponse) { |
2871 | meta = &recent->cachedResponse->meta; | 3895 | meta = &recent->cachedResponse->meta; |
@@ -2945,7 +3969,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
2945 | // setFixedSize_Widget(sizer, init_I2(gap_UI * 65, 1)); | 3969 | // setFixedSize_Widget(sizer, init_I2(gap_UI * 65, 1)); |
2946 | // addChildFlags_Widget(dlg, iClob(sizer), frameless_WidgetFlag); | 3970 | // addChildFlags_Widget(dlg, iClob(sizer), frameless_WidgetFlag); |
2947 | // setFlags_Widget(dlg, centerHorizontal_WidgetFlag, iFalse); | 3971 | // setFlags_Widget(dlg, centerHorizontal_WidgetFlag, iFalse); |
2948 | if (deviceType_App() != phone_AppDeviceType) { | 3972 | if (deviceType_App() == desktop_AppDeviceType) { |
2949 | const iWidget *lockButton = findWidget_Root("navbar.lock"); | 3973 | const iWidget *lockButton = findWidget_Root("navbar.lock"); |
2950 | setPos_Widget(dlg, windowToLocal_Widget(dlg, bottomLeft_Rect(bounds_Widget(lockButton)))); | 3974 | setPos_Widget(dlg, windowToLocal_Widget(dlg, bottomLeft_Rect(bounds_Widget(lockButton)))); |
2951 | } | 3975 | } |
@@ -2994,9 +4018,16 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
2994 | } | 4018 | } |
2995 | else { | 4019 | else { |
2996 | /* Full document. */ | 4020 | /* Full document. */ |
2997 | copied = copy_String(source_GmDocument(d->doc)); | 4021 | copied = copy_String(source_GmDocument(d->view.doc)); |
4022 | } | ||
4023 | if (argLabel_Command(cmd, "share")) { | ||
4024 | #if defined (iPlatformAppleMobile) | ||
4025 | openTextActivityView_iOS(copied); | ||
4026 | #endif | ||
4027 | } | ||
4028 | else { | ||
4029 | SDL_SetClipboardText(cstr_String(copied)); | ||
2998 | } | 4030 | } |
2999 | SDL_SetClipboardText(cstr_String(copied)); | ||
3000 | delete_String(copied); | 4031 | delete_String(copied); |
3001 | if (flags_Widget(w) & touchDrag_WidgetFlag) { | 4032 | if (flags_Widget(w) & touchDrag_WidgetFlag) { |
3002 | postCommand_Widget(w, "document.select arg:0"); | 4033 | postCommand_Widget(w, "document.select arg:0"); |
@@ -3006,7 +4037,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
3006 | else if (equal_Command(cmd, "document.copylink") && document_App() == d) { | 4037 | else if (equal_Command(cmd, "document.copylink") && document_App() == d) { |
3007 | if (d->contextLink) { | 4038 | if (d->contextLink) { |
3008 | SDL_SetClipboardText(cstr_String(canonicalUrl_String(absoluteUrl_String( | 4039 | SDL_SetClipboardText(cstr_String(canonicalUrl_String(absoluteUrl_String( |
3009 | d->mod.url, linkUrl_GmDocument(d->doc, d->contextLink->linkId))))); | 4040 | d->mod.url, linkUrl_GmDocument(d->view.doc, d->contextLink->linkId))))); |
3010 | } | 4041 | } |
3011 | else { | 4042 | else { |
3012 | SDL_SetClipboardText(cstr_String(canonicalUrl_String(d->mod.url))); | 4043 | SDL_SetClipboardText(cstr_String(canonicalUrl_String(d->mod.url))); |
@@ -3016,13 +4047,13 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
3016 | else if (equalWidget_Command(cmd, w, "document.downloadlink")) { | 4047 | else if (equalWidget_Command(cmd, w, "document.downloadlink")) { |
3017 | if (d->contextLink) { | 4048 | if (d->contextLink) { |
3018 | const iGmLinkId linkId = d->contextLink->linkId; | 4049 | const iGmLinkId linkId = d->contextLink->linkId; |
3019 | setUrl_Media(media_GmDocument(d->doc), | 4050 | setUrl_Media(media_GmDocument(d->view.doc), |
3020 | linkId, | 4051 | linkId, |
3021 | download_MediaType, | 4052 | download_MediaType, |
3022 | linkUrl_GmDocument(d->doc, linkId)); | 4053 | linkUrl_GmDocument(d->view.doc, linkId)); |
3023 | requestMedia_DocumentWidget_(d, linkId, iFalse /* no filters */); | 4054 | requestMedia_DocumentWidget_(d, linkId, iFalse /* no filters */); |
3024 | redoLayout_GmDocument(d->doc); /* inline downloader becomes visible */ | 4055 | redoLayout_GmDocument(d->view.doc); /* inline downloader becomes visible */ |
3025 | updateVisible_DocumentWidget_(d); | 4056 | updateVisible_DocumentView_(&d->view); |
3026 | invalidate_DocumentWidget_(d); | 4057 | invalidate_DocumentWidget_(d); |
3027 | refresh_Widget(w); | 4058 | refresh_Widget(w); |
3028 | } | 4059 | } |
@@ -3055,6 +4086,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
3055 | } | 4086 | } |
3056 | else if (equalWidget_Command(cmd, w, "document.request.finished") && | 4087 | else if (equalWidget_Command(cmd, w, "document.request.finished") && |
3057 | id_GmRequest(d->request) == argU32Label_Command(cmd, "reqid")) { | 4088 | id_GmRequest(d->request) == argU32Label_Command(cmd, "reqid")) { |
4089 | d->flags &= ~fromCache_DocumentWidgetFlag; | ||
3058 | set_Block(&d->sourceContent, body_GmRequest(d->request)); | 4090 | set_Block(&d->sourceContent, body_GmRequest(d->request)); |
3059 | if (!isSuccess_GmStatusCode(status_GmRequest(d->request))) { | 4091 | if (!isSuccess_GmStatusCode(status_GmRequest(d->request))) { |
3060 | /* TODO: Why is this here? Can it be removed? */ | 4092 | /* TODO: Why is this here? Can it be removed? */ |
@@ -3066,7 +4098,8 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
3066 | updateFetchProgress_DocumentWidget_(d); | 4098 | updateFetchProgress_DocumentWidget_(d); |
3067 | checkResponse_DocumentWidget_(d); | 4099 | checkResponse_DocumentWidget_(d); |
3068 | if (category_GmStatusCode(status_GmRequest(d->request)) == categorySuccess_GmStatusCode) { | 4100 | if (category_GmStatusCode(status_GmRequest(d->request)) == categorySuccess_GmStatusCode) { |
3069 | init_Anim(&d->scrollY.pos, d->initNormScrollY * pageHeight_DocumentWidget_(d)); /* TODO: unless user already scrolled! */ | 4101 | init_Anim(&d->view.scrollY.pos, d->initNormScrollY * pageHeight_DocumentView_(&d->view)); |
4102 | /* TODO: unless user already scrolled! */ | ||
3070 | } | 4103 | } |
3071 | addBannerWarnings_DocumentWidget_(d); | 4104 | addBannerWarnings_DocumentWidget_(d); |
3072 | iChangeFlags(d->flags, | 4105 | iChangeFlags(d->flags, |
@@ -3076,6 +4109,8 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
3076 | postProcessRequestContent_DocumentWidget_(d, iFalse); | 4109 | postProcessRequestContent_DocumentWidget_(d, iFalse); |
3077 | /* The response may be cached. */ | 4110 | /* The response may be cached. */ |
3078 | if (d->request) { | 4111 | if (d->request) { |
4112 | iAssert(~d->flags & animationPlaceholder_DocumentWidgetFlag); | ||
4113 | iAssert(~d->flags & fromCache_DocumentWidgetFlag); | ||
3079 | if (!equal_Rangecc(urlScheme_String(d->mod.url), "about") && | 4114 | if (!equal_Rangecc(urlScheme_String(d->mod.url), "about") && |
3080 | (startsWithCase_String(meta_GmRequest(d->request), "text/") || | 4115 | (startsWithCase_String(meta_GmRequest(d->request), "text/") || |
3081 | !cmp_String(&d->sourceMime, mimeType_Gempub))) { | 4116 | !cmp_String(&d->sourceMime, mimeType_Gempub))) { |
@@ -3084,8 +4119,8 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
3084 | } | 4119 | } |
3085 | } | 4120 | } |
3086 | iReleasePtr(&d->request); | 4121 | iReleasePtr(&d->request); |
3087 | updateVisible_DocumentWidget_(d); | 4122 | updateVisible_DocumentView_(&d->view); |
3088 | d->drawBufs->flags |= updateSideBuf_DrawBufsFlag; | 4123 | d->view.drawBufs->flags |= updateSideBuf_DrawBufsFlag; |
3089 | postCommandf_Root(w->root, | 4124 | postCommandf_Root(w->root, |
3090 | "document.changed doc:%p status:%d url:%s", | 4125 | "document.changed doc:%p status:%d url:%s", |
3091 | d, | 4126 | d, |
@@ -3093,7 +4128,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
3093 | cstr_String(d->mod.url)); | 4128 | cstr_String(d->mod.url)); |
3094 | /* Check for a pending goto. */ | 4129 | /* Check for a pending goto. */ |
3095 | if (!isEmpty_String(&d->pendingGotoHeading)) { | 4130 | if (!isEmpty_String(&d->pendingGotoHeading)) { |
3096 | scrollToHeading_DocumentWidget_(d, cstr_String(&d->pendingGotoHeading)); | 4131 | scrollToHeading_DocumentView_(&d->view, cstr_String(&d->pendingGotoHeading)); |
3097 | clear_String(&d->pendingGotoHeading); | 4132 | clear_String(&d->pendingGotoHeading); |
3098 | } | 4133 | } |
3099 | cacheDocumentGlyphs_DocumentWidget_(d); | 4134 | cacheDocumentGlyphs_DocumentWidget_(d); |
@@ -3102,6 +4137,11 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
3102 | else if (equal_Command(cmd, "document.translate") && d == document_App()) { | 4137 | else if (equal_Command(cmd, "document.translate") && d == document_App()) { |
3103 | if (!d->translation) { | 4138 | if (!d->translation) { |
3104 | d->translation = new_Translation(d); | 4139 | d->translation = new_Translation(d); |
4140 | if (isUsingPanelLayout_Mobile()) { | ||
4141 | const iRect safe = safeRect_Root(w->root); | ||
4142 | d->translation->dlg->rect.pos = windowToLocal_Widget(w, zero_I2()); | ||
4143 | d->translation->dlg->rect.size = safe.size; | ||
4144 | } | ||
3105 | } | 4145 | } |
3106 | return iTrue; | 4146 | return iTrue; |
3107 | } | 4147 | } |
@@ -3117,14 +4157,19 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
3117 | if (findChild_Widget(root_Widget(w), "upload")) { | 4157 | if (findChild_Widget(root_Widget(w), "upload")) { |
3118 | return iTrue; /* already open */ | 4158 | return iTrue; /* already open */ |
3119 | } | 4159 | } |
3120 | if (equalCase_Rangecc(urlScheme_String(d->mod.url), "gemini") || | 4160 | const iBool isGemini = equalCase_Rangecc(urlScheme_String(d->mod.url), "gemini"); |
3121 | equalCase_Rangecc(urlScheme_String(d->mod.url), "titan")) { | 4161 | if (isGemini || equalCase_Rangecc(urlScheme_String(d->mod.url), "titan")) { |
3122 | iUploadWidget *upload = new_UploadWidget(); | 4162 | iUploadWidget *upload = new_UploadWidget(); |
3123 | setUrl_UploadWidget(upload, d->mod.url); | 4163 | setUrl_UploadWidget(upload, d->mod.url); |
3124 | setResponseViewer_UploadWidget(upload, d); | 4164 | setResponseViewer_UploadWidget(upload, d); |
3125 | addChild_Widget(get_Root()->widget, iClob(upload)); | 4165 | addChild_Widget(get_Root()->widget, iClob(upload)); |
3126 | // finalizeSheet_Mobile(as_Widget(upload)); | ||
3127 | setupSheetTransition_Mobile(as_Widget(upload), iTrue); | 4166 | setupSheetTransition_Mobile(as_Widget(upload), iTrue); |
4167 | if (argLabel_Command(cmd, "copy") && isUtf8_Rangecc(range_Block(&d->sourceContent))) { | ||
4168 | iString text; | ||
4169 | initBlock_String(&text, &d->sourceContent); | ||
4170 | setText_UploadWidget(upload, &text); | ||
4171 | deinit_String(&text); | ||
4172 | } | ||
3128 | postRefresh_App(); | 4173 | postRefresh_App(); |
3129 | } | 4174 | } |
3130 | return iTrue; | 4175 | return iTrue; |
@@ -3135,7 +4180,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
3135 | else if (equal_Command(cmd, "media.player.started")) { | 4180 | else if (equal_Command(cmd, "media.player.started")) { |
3136 | /* When one media player starts, pause the others that may be playing. */ | 4181 | /* When one media player starts, pause the others that may be playing. */ |
3137 | const iPlayer *startedPlr = pointerLabel_Command(cmd, "player"); | 4182 | const iPlayer *startedPlr = pointerLabel_Command(cmd, "player"); |
3138 | const iMedia * media = media_GmDocument(d->doc); | 4183 | const iMedia * media = media_GmDocument(d->view.doc); |
3139 | const size_t num = numAudio_Media(media); | 4184 | const size_t num = numAudio_Media(media); |
3140 | for (size_t id = 1; id <= num; id++) { | 4185 | for (size_t id = 1; id <= num; id++) { |
3141 | iPlayer *plr = audioPlayer_Media(media, (iMediaId){ audio_MediaType, id }); | 4186 | iPlayer *plr = audioPlayer_Media(media, (iMediaId){ audio_MediaType, id }); |
@@ -3167,22 +4212,37 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
3167 | "${dlg.save.incomplete}"); | 4212 | "${dlg.save.incomplete}"); |
3168 | } | 4213 | } |
3169 | else if (!isEmpty_Block(&d->sourceContent)) { | 4214 | else if (!isEmpty_Block(&d->sourceContent)) { |
3170 | const iBool doOpen = argLabel_Command(cmd, "open"); | 4215 | if (argLabel_Command(cmd, "extview")) { |
3171 | const iString *savePath = saveToDownloads_(d->mod.url, &d->sourceMime, | 4216 | if (equalCase_Rangecc(urlScheme_String(d->mod.url), "file")) { |
3172 | &d->sourceContent, !doOpen); | 4217 | /* Already a file so just open it directly. */ |
3173 | if (!isEmpty_String(savePath) && doOpen) { | 4218 | postCommandf_Root(w->root, "!open default:1 url:%s", cstr_String(d->mod.url)); |
3174 | postCommandf_Root( | 4219 | } |
3175 | w->root, "!open url:%s", cstrCollect_String(makeFileUrl_String(savePath))); | 4220 | else { |
4221 | const iString *tmpPath = temporaryPathForUrl_App(d->mod.url, &d->sourceMime); | ||
4222 | if (saveToFile_(tmpPath, &d->sourceContent, iFalse)) { | ||
4223 | postCommandf_Root(w->root, "!open default:1 url:%s", | ||
4224 | cstrCollect_String(makeFileUrl_String(tmpPath))); | ||
4225 | } | ||
4226 | } | ||
4227 | } | ||
4228 | else { | ||
4229 | const iBool doOpen = argLabel_Command(cmd, "open"); | ||
4230 | const iString *savePath = saveToDownloads_(d->mod.url, &d->sourceMime, | ||
4231 | &d->sourceContent, !doOpen); | ||
4232 | if (!isEmpty_String(savePath) && doOpen) { | ||
4233 | postCommandf_Root( | ||
4234 | w->root, "!open url:%s", cstrCollect_String(makeFileUrl_String(savePath))); | ||
4235 | } | ||
3176 | } | 4236 | } |
3177 | } | 4237 | } |
3178 | return iTrue; | 4238 | return iTrue; |
3179 | } | 4239 | } |
3180 | else if (equal_Command(cmd, "document.reload") && document_Command(cmd) == d) { | 4240 | else if (equal_Command(cmd, "document.reload") && document_Command(cmd) == d) { |
3181 | d->initNormScrollY = normScrollPos_DocumentWidget_(d); | 4241 | d->initNormScrollY = normScrollPos_DocumentView_(&d->view); |
3182 | if (equalCase_Rangecc(urlScheme_String(d->mod.url), "titan")) { | 4242 | if (equalCase_Rangecc(urlScheme_String(d->mod.url), "titan")) { |
3183 | /* Reopen so the Upload dialog gets shown. */ | 4243 | /* Reopen so the Upload dialog gets shown. */ |
3184 | postCommandf_App("open url:%s", cstr_String(d->mod.url)); | 4244 | postCommandf_App("open url:%s", cstr_String(d->mod.url)); |
3185 | return iTrue; | 4245 | return iTrue; |
3186 | } | 4246 | } |
3187 | fetch_DocumentWidget_(d); | 4247 | fetch_DocumentWidget_(d); |
3188 | return iTrue; | 4248 | return iTrue; |
@@ -3195,13 +4255,13 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
3195 | if (d->flags & showLinkNumbers_DocumentWidgetFlag && | 4255 | if (d->flags & showLinkNumbers_DocumentWidgetFlag && |
3196 | d->ordinalMode == homeRow_DocumentLinkOrdinalMode) { | 4256 | d->ordinalMode == homeRow_DocumentLinkOrdinalMode) { |
3197 | const size_t numKeys = iElemCount(homeRowKeys_); | 4257 | const size_t numKeys = iElemCount(homeRowKeys_); |
3198 | const iGmRun *last = lastVisibleLink_DocumentWidget_(d); | 4258 | const iGmRun *last = lastVisibleLink_DocumentView_(&d->view); |
3199 | if (!last) { | 4259 | if (!last) { |
3200 | d->ordinalBase = 0; | 4260 | d->ordinalBase = 0; |
3201 | } | 4261 | } |
3202 | else { | 4262 | else { |
3203 | d->ordinalBase += numKeys; | 4263 | d->ordinalBase += numKeys; |
3204 | if (visibleLinkOrdinal_DocumentWidget_(d, last->linkId) < d->ordinalBase) { | 4264 | if (visibleLinkOrdinal_DocumentView_(&d->view, last->linkId) < d->ordinalBase) { |
3205 | d->ordinalBase = 0; | 4265 | d->ordinalBase = 0; |
3206 | } | 4266 | } |
3207 | } | 4267 | } |
@@ -3221,23 +4281,11 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
3221 | iChangeFlags(d->flags, newTabViaHomeKeys_DocumentWidgetFlag, | 4281 | iChangeFlags(d->flags, newTabViaHomeKeys_DocumentWidgetFlag, |
3222 | argLabel_Command(cmd, "newtab") != 0); | 4282 | argLabel_Command(cmd, "newtab") != 0); |
3223 | } | 4283 | } |
3224 | invalidateVisibleLinks_DocumentWidget_(d); | 4284 | invalidateVisibleLinks_DocumentView_(&d->view); |
3225 | refresh_Widget(d); | 4285 | refresh_Widget(d); |
3226 | return iTrue; | 4286 | return iTrue; |
3227 | } | 4287 | } |
3228 | else if (equal_Command(cmd, "navigate.back") && document_App() == d) { | 4288 | else if (equal_Command(cmd, "navigate.back") && document_App() == d) { |
3229 | if (isPortraitPhone_App()) { | ||
3230 | if (d->flags & openedFromSidebar_DocumentWidgetFlag && | ||
3231 | !isVisible_Widget(findWidget_App("sidebar"))) { | ||
3232 | postCommand_App("sidebar.toggle"); | ||
3233 | showToolbar_Root(get_Root(), iTrue); | ||
3234 | #if defined (iPlatformAppleMobile) | ||
3235 | playHapticEffect_iOS(gentleTap_HapticEffect); | ||
3236 | #endif | ||
3237 | return iTrue; | ||
3238 | } | ||
3239 | d->flags &= ~openedFromSidebar_DocumentWidgetFlag; | ||
3240 | } | ||
3241 | if (d->request) { | 4289 | if (d->request) { |
3242 | postCommandf_Root(w->root, | 4290 | postCommandf_Root(w->root, |
3243 | "document.request.cancelled doc:%p url:%s", d, cstr_String(d->mod.url)); | 4291 | "document.request.cancelled doc:%p url:%s", d, cstr_String(d->mod.url)); |
@@ -3274,8 +4322,8 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
3274 | return iTrue; | 4322 | return iTrue; |
3275 | } | 4323 | } |
3276 | else if (equalWidget_Command(cmd, w, "scroll.moved")) { | 4324 | else if (equalWidget_Command(cmd, w, "scroll.moved")) { |
3277 | init_Anim(&d->scrollY.pos, arg_Command(cmd)); | 4325 | init_Anim(&d->view.scrollY.pos, arg_Command(cmd)); |
3278 | updateVisible_DocumentWidget_(d); | 4326 | updateVisible_DocumentView_(&d->view); |
3279 | return iTrue; | 4327 | return iTrue; |
3280 | } | 4328 | } |
3281 | else if (equal_Command(cmd, "scroll.page") && document_App() == d) { | 4329 | else if (equal_Command(cmd, "scroll.page") && document_App() == d) { |
@@ -3286,25 +4334,26 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
3286 | return iTrue; | 4334 | return iTrue; |
3287 | } | 4335 | } |
3288 | const float amount = argLabel_Command(cmd, "full") != 0 ? 1.0f : 0.5f; | 4336 | const float amount = argLabel_Command(cmd, "full") != 0 ? 1.0f : 0.5f; |
3289 | smoothScroll_DocumentWidget_(d, | 4337 | smoothScroll_DocumentView_(&d->view, |
3290 | dir * amount * height_Rect(documentBounds_DocumentWidget_(d)), | 4338 | dir * amount * |
3291 | smoothDuration_DocumentWidget_(keyboard_ScrollType)); | 4339 | height_Rect(documentBounds_DocumentView_(&d->view)), |
4340 | smoothDuration_DocumentWidget_(keyboard_ScrollType)); | ||
3292 | return iTrue; | 4341 | return iTrue; |
3293 | } | 4342 | } |
3294 | else if (equal_Command(cmd, "scroll.top") && document_App() == d) { | 4343 | else if (equal_Command(cmd, "scroll.top") && document_App() == d) { |
3295 | init_Anim(&d->scrollY.pos, 0); | 4344 | init_Anim(&d->view.scrollY.pos, 0); |
3296 | invalidate_VisBuf(d->visBuf); | 4345 | invalidate_VisBuf(d->view.visBuf); |
3297 | clampScroll_DocumentWidget_(d); | 4346 | clampScroll_DocumentView_(&d->view); |
3298 | updateVisible_DocumentWidget_(d); | 4347 | updateVisible_DocumentView_(&d->view); |
3299 | refresh_Widget(w); | 4348 | refresh_Widget(w); |
3300 | return iTrue; | 4349 | return iTrue; |
3301 | } | 4350 | } |
3302 | else if (equal_Command(cmd, "scroll.bottom") && document_App() == d) { | 4351 | else if (equal_Command(cmd, "scroll.bottom") && document_App() == d) { |
3303 | updateScrollMax_DocumentWidget_(d); /* scrollY.max might not be fully updated */ | 4352 | updateScrollMax_DocumentView_(&d->view); /* scrollY.max might not be fully updated */ |
3304 | init_Anim(&d->scrollY.pos, d->scrollY.max); | 4353 | init_Anim(&d->view.scrollY.pos, d->view.scrollY.max); |
3305 | invalidate_VisBuf(d->visBuf); | 4354 | invalidate_VisBuf(d->view.visBuf); |
3306 | clampScroll_DocumentWidget_(d); | 4355 | clampScroll_DocumentView_(&d->view); |
3307 | updateVisible_DocumentWidget_(d); | 4356 | updateVisible_DocumentView_(&d->view); |
3308 | refresh_Widget(w); | 4357 | refresh_Widget(w); |
3309 | return iTrue; | 4358 | return iTrue; |
3310 | } | 4359 | } |
@@ -3315,9 +4364,9 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
3315 | fetchNextUnfetchedImage_DocumentWidget_(d)) { | 4364 | fetchNextUnfetchedImage_DocumentWidget_(d)) { |
3316 | return iTrue; | 4365 | return iTrue; |
3317 | } | 4366 | } |
3318 | smoothScroll_DocumentWidget_(d, | 4367 | smoothScroll_DocumentView_(&d->view, |
3319 | 3 * lineHeight_Text(paragraph_FontId) * dir, | 4368 | 3 * lineHeight_Text(paragraph_FontId) * dir, |
3320 | smoothDuration_DocumentWidget_(keyboard_ScrollType)); | 4369 | smoothDuration_DocumentWidget_(keyboard_ScrollType)); |
3321 | return iTrue; | 4370 | return iTrue; |
3322 | } | 4371 | } |
3323 | else if (equal_Command(cmd, "document.goto") && document_App() == d) { | 4372 | else if (equal_Command(cmd, "document.goto") && document_App() == d) { |
@@ -3328,13 +4377,13 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
3328 | setCStr_String(&d->pendingGotoHeading, heading); | 4377 | setCStr_String(&d->pendingGotoHeading, heading); |
3329 | return iTrue; | 4378 | return iTrue; |
3330 | } | 4379 | } |
3331 | scrollToHeading_DocumentWidget_(d, heading); | 4380 | scrollToHeading_DocumentView_(&d->view, heading); |
3332 | return iTrue; | 4381 | return iTrue; |
3333 | } | 4382 | } |
3334 | const char *loc = pointerLabel_Command(cmd, "loc"); | 4383 | const char *loc = pointerLabel_Command(cmd, "loc"); |
3335 | const iGmRun *run = findRunAtLoc_GmDocument(d->doc, loc); | 4384 | const iGmRun *run = findRunAtLoc_GmDocument(d->view.doc, loc); |
3336 | if (run) { | 4385 | if (run) { |
3337 | scrollTo_DocumentWidget_(d, run->visBounds.pos.y, iFalse); | 4386 | scrollTo_DocumentView_(&d->view, run->visBounds.pos.y, iFalse); |
3338 | } | 4387 | } |
3339 | return iTrue; | 4388 | return iTrue; |
3340 | } | 4389 | } |
@@ -3349,24 +4398,24 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
3349 | } | 4398 | } |
3350 | else { | 4399 | else { |
3351 | const iBool wrap = d->foundMark.start != NULL; | 4400 | const iBool wrap = d->foundMark.start != NULL; |
3352 | d->foundMark = finder(d->doc, text_InputWidget(find), dir > 0 ? d->foundMark.end | 4401 | d->foundMark = finder(d->view.doc, text_InputWidget(find), dir > 0 ? d->foundMark.end |
3353 | : d->foundMark.start); | 4402 | : d->foundMark.start); |
3354 | if (!d->foundMark.start && wrap) { | 4403 | if (!d->foundMark.start && wrap) { |
3355 | /* Wrap around. */ | 4404 | /* Wrap around. */ |
3356 | d->foundMark = finder(d->doc, text_InputWidget(find), NULL); | 4405 | d->foundMark = finder(d->view.doc, text_InputWidget(find), NULL); |
3357 | } | 4406 | } |
3358 | if (d->foundMark.start) { | 4407 | if (d->foundMark.start) { |
3359 | const iGmRun *found; | 4408 | const iGmRun *found; |
3360 | if ((found = findRunAtLoc_GmDocument(d->doc, d->foundMark.start)) != NULL) { | 4409 | if ((found = findRunAtLoc_GmDocument(d->view.doc, d->foundMark.start)) != NULL) { |
3361 | scrollTo_DocumentWidget_(d, mid_Rect(found->bounds).y, iTrue); | 4410 | scrollTo_DocumentView_(&d->view, mid_Rect(found->bounds).y, iTrue); |
3362 | } | 4411 | } |
3363 | } | 4412 | } |
3364 | } | 4413 | } |
3365 | if (flags_Widget(w) & touchDrag_WidgetFlag) { | 4414 | if (flags_Widget(w) & touchDrag_WidgetFlag) { |
3366 | postCommand_Root(w->root, "document.select arg:0"); /* we can't handle both at the same time */ | 4415 | postCommand_Root(w->root, "document.select arg:0"); /* we can't handle both at the same time */ |
3367 | } | 4416 | } |
3368 | invalidateWideRunsWithNonzeroOffset_DocumentWidget_(d); /* markers don't support offsets */ | 4417 | invalidateWideRunsWithNonzeroOffset_DocumentView_(&d->view); /* markers don't support offsets */ |
3369 | resetWideRuns_DocumentWidget_(d); | 4418 | resetWideRuns_DocumentView_(&d->view); |
3370 | refresh_Widget(w); | 4419 | refresh_Widget(w); |
3371 | return iTrue; | 4420 | return iTrue; |
3372 | } | 4421 | } |
@@ -3379,16 +4428,16 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
3379 | } | 4428 | } |
3380 | else if (equal_Command(cmd, "bookmark.links") && document_App() == d) { | 4429 | else if (equal_Command(cmd, "bookmark.links") && document_App() == d) { |
3381 | iPtrArray *links = collectNew_PtrArray(); | 4430 | iPtrArray *links = collectNew_PtrArray(); |
3382 | render_GmDocument(d->doc, (iRangei){ 0, size_GmDocument(d->doc).y }, addAllLinks_, links); | 4431 | render_GmDocument(d->view.doc, (iRangei){ 0, size_GmDocument(d->view.doc).y }, addAllLinks_, links); |
3383 | /* Find links that aren't already bookmarked. */ | 4432 | /* Find links that aren't already bookmarked. */ |
3384 | iForEach(PtrArray, i, links) { | 4433 | iForEach(PtrArray, i, links) { |
3385 | const iGmRun *run = i.ptr; | 4434 | const iGmRun *run = i.ptr; |
3386 | uint32_t bmid; | 4435 | uint32_t bmid; |
3387 | if ((bmid = findUrl_Bookmarks(bookmarks_App(), | 4436 | if ((bmid = findUrl_Bookmarks(bookmarks_App(), |
3388 | linkUrl_GmDocument(d->doc, run->linkId))) != 0) { | 4437 | linkUrl_GmDocument(d->view.doc, run->linkId))) != 0) { |
3389 | const iBookmark *bm = get_Bookmarks(bookmarks_App(), bmid); | 4438 | const iBookmark *bm = get_Bookmarks(bookmarks_App(), bmid); |
3390 | /* We can import local copies of remote bookmarks. */ | 4439 | /* We can import local copies of remote bookmarks. */ |
3391 | if (!hasTag_Bookmark(bm, remote_BookmarkTag)) { | 4440 | if (~bm->flags & remote_BookmarkFlag) { |
3392 | remove_PtrArrayIterator(&i); | 4441 | remove_PtrArrayIterator(&i); |
3393 | } | 4442 | } |
3394 | } | 4443 | } |
@@ -3412,7 +4461,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
3412 | iConstForEach(PtrArray, j, links) { | 4461 | iConstForEach(PtrArray, j, links) { |
3413 | const iGmRun *run = j.ptr; | 4462 | const iGmRun *run = j.ptr; |
3414 | add_Bookmarks(bookmarks_App(), | 4463 | add_Bookmarks(bookmarks_App(), |
3415 | linkUrl_GmDocument(d->doc, run->linkId), | 4464 | linkUrl_GmDocument(d->view.doc, run->linkId), |
3416 | collect_String(newRange_String(run->text)), | 4465 | collect_String(newRange_String(run->text)), |
3417 | NULL, | 4466 | NULL, |
3418 | 0x1f588 /* pin */); | 4467 | 0x1f588 /* pin */); |
@@ -3427,7 +4476,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
3427 | return iTrue; | 4476 | return iTrue; |
3428 | } | 4477 | } |
3429 | else if (equalWidget_Command(cmd, w, "menu.closed")) { | 4478 | else if (equalWidget_Command(cmd, w, "menu.closed")) { |
3430 | updateHover_DocumentWidget_(d, mouseCoord_Window(get_Window(), 0)); | 4479 | updateHover_DocumentView_(&d->view, mouseCoord_Window(get_Window(), 0)); |
3431 | } | 4480 | } |
3432 | else if (equal_Command(cmd, "document.autoreload")) { | 4481 | else if (equal_Command(cmd, "document.autoreload")) { |
3433 | if (d->mod.reloadInterval) { | 4482 | if (d->mod.reloadInterval) { |
@@ -3485,7 +4534,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
3485 | if (argLabel_Command(cmd, "ttf")) { | 4534 | if (argLabel_Command(cmd, "ttf")) { |
3486 | iAssert(!cmp_String(&d->sourceMime, "font/ttf")); | 4535 | iAssert(!cmp_String(&d->sourceMime, "font/ttf")); |
3487 | installFontFile_Fonts(collect_String(suffix_Command(cmd, "name")), &d->sourceContent); | 4536 | installFontFile_Fonts(collect_String(suffix_Command(cmd, "name")), &d->sourceContent); |
3488 | postCommand_App("open url:about:fonts"); | 4537 | postCommand_App("open url:about:fonts"); |
3489 | } | 4538 | } |
3490 | else { | 4539 | else { |
3491 | const iString *id = idFromUrl_FontPack(d->mod.url); | 4540 | const iString *id = idFromUrl_FontPack(d->mod.url); |
@@ -3497,14 +4546,9 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
3497 | return iFalse; | 4546 | return iFalse; |
3498 | } | 4547 | } |
3499 | 4548 | ||
3500 | static iRect runRect_DocumentWidget_(const iDocumentWidget *d, const iGmRun *run) { | ||
3501 | const iRect docBounds = documentBounds_DocumentWidget_(d); | ||
3502 | return moved_Rect(run->bounds, addY_I2(topLeft_Rect(docBounds), viewPos_DocumentWidget_(d))); | ||
3503 | } | ||
3504 | |||
3505 | static void setGrabbedPlayer_DocumentWidget_(iDocumentWidget *d, const iGmRun *run) { | 4549 | static void setGrabbedPlayer_DocumentWidget_(iDocumentWidget *d, const iGmRun *run) { |
3506 | if (run && run->mediaType == audio_MediaType) { | 4550 | if (run && run->mediaType == audio_MediaType) { |
3507 | iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run)); | 4551 | iPlayer *plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run)); |
3508 | setFlags_Player(plr, volumeGrabbed_PlayerFlag, iTrue); | 4552 | setFlags_Player(plr, volumeGrabbed_PlayerFlag, iTrue); |
3509 | d->grabbedStartVolume = volume_Player(plr); | 4553 | d->grabbedStartVolume = volume_Player(plr); |
3510 | d->grabbedPlayer = run; | 4554 | d->grabbedPlayer = run; |
@@ -3512,7 +4556,7 @@ static void setGrabbedPlayer_DocumentWidget_(iDocumentWidget *d, const iGmRun *r | |||
3512 | } | 4556 | } |
3513 | else if (d->grabbedPlayer) { | 4557 | else if (d->grabbedPlayer) { |
3514 | setFlags_Player( | 4558 | setFlags_Player( |
3515 | audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(d->grabbedPlayer)), | 4559 | audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(d->grabbedPlayer)), |
3516 | volumeGrabbed_PlayerFlag, | 4560 | volumeGrabbed_PlayerFlag, |
3517 | iFalse); | 4561 | iFalse); |
3518 | d->grabbedPlayer = NULL; | 4562 | d->grabbedPlayer = NULL; |
@@ -3528,24 +4572,33 @@ static iBool processMediaEvents_DocumentWidget_(iDocumentWidget *d, const SDL_Ev | |||
3528 | ev->type != SDL_MOUSEMOTION) { | 4572 | ev->type != SDL_MOUSEMOTION) { |
3529 | return iFalse; | 4573 | return iFalse; |
3530 | } | 4574 | } |
3531 | if (ev->type == SDL_MOUSEBUTTONDOWN || ev->type == SDL_MOUSEBUTTONUP) { | ||
3532 | if (ev->button.button != SDL_BUTTON_LEFT) { | ||
3533 | return iFalse; | ||
3534 | } | ||
3535 | } | ||
3536 | if (d->grabbedPlayer) { | 4575 | if (d->grabbedPlayer) { |
3537 | /* Updated in the drag. */ | 4576 | /* Updated in the drag. */ |
3538 | return iFalse; | 4577 | return iFalse; |
3539 | } | 4578 | } |
3540 | const iInt2 mouse = init_I2(ev->button.x, ev->button.y); | 4579 | const iInt2 mouse = init_I2(ev->button.x, ev->button.y); |
3541 | iConstForEach(PtrArray, i, &d->visibleMedia) { | 4580 | iConstForEach(PtrArray, i, &d->view.visibleMedia) { |
3542 | const iGmRun *run = i.ptr; | 4581 | const iGmRun *run = i.ptr; |
4582 | if (run->mediaType == download_MediaType) { | ||
4583 | iDownloadUI ui; | ||
4584 | init_DownloadUI(&ui, media_GmDocument(d->view.doc), mediaId_GmRun(run).id, | ||
4585 | runRect_DocumentView_(&d->view, run)); | ||
4586 | if (processEvent_DownloadUI(&ui, ev)) { | ||
4587 | return iTrue; | ||
4588 | } | ||
4589 | continue; | ||
4590 | } | ||
3543 | if (run->mediaType != audio_MediaType) { | 4591 | if (run->mediaType != audio_MediaType) { |
3544 | continue; | 4592 | continue; |
3545 | } | 4593 | } |
4594 | if (ev->type == SDL_MOUSEBUTTONDOWN || ev->type == SDL_MOUSEBUTTONUP) { | ||
4595 | if (ev->button.button != SDL_BUTTON_LEFT) { | ||
4596 | return iFalse; | ||
4597 | } | ||
4598 | } | ||
3546 | /* TODO: move this to mediaui.c */ | 4599 | /* TODO: move this to mediaui.c */ |
3547 | const iRect rect = runRect_DocumentWidget_(d, run); | 4600 | const iRect rect = runRect_DocumentView_(&d->view, run); |
3548 | iPlayer * plr = audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run)); | 4601 | iPlayer * plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run)); |
3549 | if (contains_Rect(rect, mouse)) { | 4602 | if (contains_Rect(rect, mouse)) { |
3550 | iPlayerUI ui; | 4603 | iPlayerUI ui; |
3551 | init_PlayerUI(&ui, plr, rect); | 4604 | init_PlayerUI(&ui, plr, rect); |
@@ -3610,83 +4663,140 @@ static iBool processMediaEvents_DocumentWidget_(iDocumentWidget *d, const SDL_Ev | |||
3610 | return iFalse; | 4663 | return iFalse; |
3611 | } | 4664 | } |
3612 | 4665 | ||
3613 | static size_t linkOrdinalFromKey_DocumentWidget_(const iDocumentWidget *d, int key) { | 4666 | static void beginMarkingSelection_DocumentWidget_(iDocumentWidget *d, iInt2 pos) { |
3614 | size_t ord = iInvalidPos; | 4667 | setFocus_Widget(NULL); /* TODO: Focus this document? */ |
3615 | if (d->ordinalMode == numbersAndAlphabet_DocumentLinkOrdinalMode) { | 4668 | invalidateWideRunsWithNonzeroOffset_DocumentView_(&d->view); |
3616 | if (key >= '1' && key <= '9') { | 4669 | resetWideRuns_DocumentView_(&d->view); /* Selections don't support horizontal scrolling. */ |
3617 | return key - '1'; | 4670 | iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iTrue); |
3618 | } | 4671 | d->initialSelectMark = d->selectMark = sourceLoc_DocumentView_(&d->view, pos); |
3619 | if (key < 'a' || key > 'z') { | 4672 | refresh_Widget(as_Widget(d)); |
3620 | return iInvalidPos; | 4673 | } |
3621 | } | 4674 | |
3622 | ord = key - 'a' + 9; | 4675 | static void interactingWithLink_DocumentWidget_(iDocumentWidget *d, iGmLinkId id) { |
3623 | #if defined (iPlatformApple) | 4676 | iRangecc loc = linkUrlRange_GmDocument(d->view.doc, id); |
3624 | /* Skip keys that would conflict with default system shortcuts: hide, minimize, quit, close. */ | 4677 | if (!loc.start) { |
3625 | if (key == 'h' || key == 'm' || key == 'q' || key == 'w') { | 4678 | clear_String(&d->linePrecedingLink); |
3626 | return iInvalidPos; | 4679 | return; |
3627 | } | ||
3628 | if (key > 'h') ord--; | ||
3629 | if (key > 'm') ord--; | ||
3630 | if (key > 'q') ord--; | ||
3631 | if (key > 'w') ord--; | ||
3632 | #endif | ||
3633 | } | 4680 | } |
3634 | else { | 4681 | d->requestLinkId = id; |
3635 | iForIndices(i, homeRowKeys_) { | 4682 | const char *start = range_String(source_GmDocument(d->view.doc)).start; |
3636 | if (homeRowKeys_[i] == key) { | 4683 | /* Find the preceding line. This is offered as a prefill option for a possible input query. */ |
3637 | return i; | 4684 | while (loc.start > start && *loc.start != '\n') { |
3638 | } | 4685 | loc.start--; |
3639 | } | ||
3640 | } | 4686 | } |
3641 | return ord; | 4687 | loc.end = loc.start; /* End of the preceding line. */ |
4688 | if (loc.start > start) { | ||
4689 | loc.start--; | ||
4690 | } | ||
4691 | while (loc.start > start && *loc.start != '\n') { | ||
4692 | loc.start--; | ||
4693 | } | ||
4694 | if (*loc.start == '\n') { | ||
4695 | loc.start++; /* Start of the preceding line. */ | ||
4696 | } | ||
4697 | setRange_String(&d->linePrecedingLink, loc); | ||
3642 | } | 4698 | } |
3643 | 4699 | ||
3644 | static iChar linkOrdinalChar_DocumentWidget_(const iDocumentWidget *d, size_t ord) { | 4700 | iLocalDef int wheelSwipeSide_DocumentWidget_(const iDocumentWidget *d) { |
3645 | if (d->ordinalMode == numbersAndAlphabet_DocumentLinkOrdinalMode) { | 4701 | return (d->flags & rightWheelSwipe_DocumentWidgetFlag ? 2 |
3646 | if (ord < 9) { | 4702 | : d->flags & leftWheelSwipe_DocumentWidgetFlag ? 1 |
3647 | return '1' + ord; | 4703 | : 0); |
3648 | } | 4704 | } |
3649 | #if defined (iPlatformApple) | 4705 | |
3650 | if (ord < 9 + 22) { | 4706 | static void finishWheelSwipe_DocumentWidget_(iDocumentWidget *d) { |
3651 | int key = 'a' + ord - 9; | 4707 | if (d->flags & eitherWheelSwipe_DocumentWidgetFlag && |
3652 | if (key >= 'h') key++; | 4708 | d->wheelSwipeState == direct_WheelSwipeState) { |
3653 | if (key >= 'm') key++; | 4709 | const int side = wheelSwipeSide_DocumentWidget_(d); |
3654 | if (key >= 'q') key++; | 4710 | int abort = ((side == 1 && d->swipeSpeed < 0) || (side == 2 && d->swipeSpeed > 0)); |
3655 | if (key >= 'w') key++; | 4711 | if (iAbs(d->wheelSwipeDistance) < width_Widget(d) / 4 && iAbs(d->swipeSpeed) < 4 * gap_UI) { |
3656 | return 'A' + key - 'a'; | 4712 | abort = 1; |
3657 | } | ||
3658 | #else | ||
3659 | if (ord < 9 + 26) { | ||
3660 | return 'A' + ord - 9; | ||
3661 | } | ||
3662 | #endif | ||
3663 | } | ||
3664 | else { | ||
3665 | if (ord < iElemCount(homeRowKeys_)) { | ||
3666 | return 'A' + homeRowKeys_[ord] - 'a'; | ||
3667 | } | 4713 | } |
4714 | postCommand_Widget(d, "edgeswipe.ended side:%d abort:%d", side, abort); | ||
4715 | d->flags &= ~eitherWheelSwipe_DocumentWidgetFlag; | ||
3668 | } | 4716 | } |
3669 | return 0; | ||
3670 | } | 4717 | } |
3671 | 4718 | ||
3672 | static void beginMarkingSelection_DocumentWidget_(iDocumentWidget *d, iInt2 pos) { | 4719 | static iBool handleWheelSwipe_DocumentWidget_(iDocumentWidget *d, const SDL_MouseWheelEvent *ev) { |
3673 | setFocus_Widget(NULL); /* TODO: Focus this document? */ | 4720 | iWidget *w = as_Widget(d); |
3674 | invalidateWideRunsWithNonzeroOffset_DocumentWidget_(d); | 4721 | if (deviceType_App() != desktop_AppDeviceType) { |
3675 | resetWideRuns_DocumentWidget_(d); /* Selections don't support horizontal scrolling. */ | 4722 | return iFalse; |
3676 | iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iTrue); | 4723 | } |
3677 | d->initialSelectMark = d->selectMark = sourceLoc_DocumentWidget_(d, pos); | 4724 | if (~flags_Widget(w) & horizontalOffset_WidgetFlag) { |
3678 | refresh_Widget(as_Widget(d)); | 4725 | return iFalse; |
4726 | } | ||
4727 | iAssert(~d->flags & animationPlaceholder_DocumentWidgetFlag); | ||
4728 | // printf("STATE:%d wheel x:%d inert:%d end:%d\n", d->wheelSwipeState, | ||
4729 | // ev->x, isInertia_MouseWheelEvent(ev), | ||
4730 | // isScrollFinished_MouseWheelEvent(ev)); | ||
4731 | // fflush(stdout); | ||
4732 | switch (d->wheelSwipeState) { | ||
4733 | case none_WheelSwipeState: | ||
4734 | /* A new swipe starts. */ | ||
4735 | if (!isInertia_MouseWheelEvent(ev) && !isScrollFinished_MouseWheelEvent(ev)) { | ||
4736 | int side = ev->x > 0 ? 1 : 2; | ||
4737 | d->wheelSwipeDistance = ev->x * 2; | ||
4738 | d->flags &= ~eitherWheelSwipe_DocumentWidgetFlag; | ||
4739 | d->flags |= (side == 1 ? leftWheelSwipe_DocumentWidgetFlag | ||
4740 | : rightWheelSwipe_DocumentWidgetFlag); | ||
4741 | // printf("swipe starts at %d, side %d\n", d->wheelSwipeDistance, side); | ||
4742 | d->wheelSwipeState = direct_WheelSwipeState; | ||
4743 | d->swipeSpeed = 0; | ||
4744 | postCommand_Widget(d, "edgeswipe.moved arg:%d side:%d", d->wheelSwipeDistance, side); | ||
4745 | return iTrue; | ||
4746 | } | ||
4747 | break; | ||
4748 | case direct_WheelSwipeState: | ||
4749 | if (isInertia_MouseWheelEvent(ev) || isScrollFinished_MouseWheelEvent(ev)) { | ||
4750 | finishWheelSwipe_DocumentWidget_(d); | ||
4751 | d->wheelSwipeState = none_WheelSwipeState; | ||
4752 | } | ||
4753 | else { | ||
4754 | int step = ev->x * 2; | ||
4755 | d->wheelSwipeDistance += step; | ||
4756 | /* Remember the maximum speed. */ | ||
4757 | if (d->swipeSpeed < 0 && step < 0) { | ||
4758 | d->swipeSpeed = iMin(d->swipeSpeed, step); | ||
4759 | } | ||
4760 | else if (d->swipeSpeed > 0 && step > 0) { | ||
4761 | d->swipeSpeed = iMax(d->swipeSpeed, step); | ||
4762 | } | ||
4763 | else { | ||
4764 | d->swipeSpeed = step; | ||
4765 | } | ||
4766 | switch (wheelSwipeSide_DocumentWidget_(d)) { | ||
4767 | case 1: | ||
4768 | d->wheelSwipeDistance = iMax(0, d->wheelSwipeDistance); | ||
4769 | d->wheelSwipeDistance = iMin(width_Widget(d), d->wheelSwipeDistance); | ||
4770 | break; | ||
4771 | case 2: | ||
4772 | d->wheelSwipeDistance = iMin(0, d->wheelSwipeDistance); | ||
4773 | d->wheelSwipeDistance = iMax(-width_Widget(d), d->wheelSwipeDistance); | ||
4774 | break; | ||
4775 | } | ||
4776 | /* TODO: calculate speed, rememeber direction */ | ||
4777 | //printf("swipe moved to %d, side %d\n", d->wheelSwipeDistance, side); | ||
4778 | postCommand_Widget(d, "edgeswipe.moved arg:%d side:%d", d->wheelSwipeDistance, | ||
4779 | wheelSwipeSide_DocumentWidget_(d)); | ||
4780 | } | ||
4781 | return iTrue; | ||
4782 | } | ||
4783 | return iFalse; | ||
3679 | } | 4784 | } |
3680 | 4785 | ||
3681 | static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) { | 4786 | static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) { |
3682 | iWidget *w = as_Widget(d); | 4787 | iWidget *w = as_Widget(d); |
4788 | iDocumentView *view = &d->view; | ||
3683 | if (isMetricsChange_UserEvent(ev)) { | 4789 | if (isMetricsChange_UserEvent(ev)) { |
3684 | updateSize_DocumentWidget(d); | 4790 | updateSize_DocumentWidget(d); |
3685 | } | 4791 | } |
3686 | else if (processEvent_SmoothScroll(&d->scrollY, ev)) { | 4792 | else if (processEvent_SmoothScroll(&d->view.scrollY, ev)) { |
3687 | return iTrue; | 4793 | return iTrue; |
3688 | } | 4794 | } |
3689 | else if (ev->type == SDL_USEREVENT && ev->user.code == command_UserEventCode) { | 4795 | else if (ev->type == SDL_USEREVENT && ev->user.code == command_UserEventCode) { |
4796 | if (isCommand_Widget(w, ev, "pullaction")) { | ||
4797 | postCommand_Widget(w, "navigate.reload"); | ||
4798 | return iTrue; | ||
4799 | } | ||
3690 | if (!handleCommand_DocumentWidget_(d, command_UserEvent(ev))) { | 4800 | if (!handleCommand_DocumentWidget_(d, command_UserEvent(ev))) { |
3691 | /* Base class commands. */ | 4801 | /* Base class commands. */ |
3692 | return processEvent_Widget(w, ev); | 4802 | return processEvent_Widget(w, ev); |
@@ -3698,27 +4808,28 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
3698 | if ((d->flags & showLinkNumbers_DocumentWidgetFlag) && | 4808 | if ((d->flags & showLinkNumbers_DocumentWidgetFlag) && |
3699 | ((key >= '1' && key <= '9') || (key >= 'a' && key <= 'z'))) { | 4809 | ((key >= '1' && key <= '9') || (key >= 'a' && key <= 'z'))) { |
3700 | const size_t ord = linkOrdinalFromKey_DocumentWidget_(d, key) + d->ordinalBase; | 4810 | const size_t ord = linkOrdinalFromKey_DocumentWidget_(d, key) + d->ordinalBase; |
3701 | iConstForEach(PtrArray, i, &d->visibleLinks) { | 4811 | iConstForEach(PtrArray, i, &d->view.visibleLinks) { |
3702 | if (ord == iInvalidPos) break; | 4812 | if (ord == iInvalidPos) break; |
3703 | const iGmRun *run = i.ptr; | 4813 | const iGmRun *run = i.ptr; |
3704 | if (run->flags & decoration_GmRunFlag && | 4814 | if (run->flags & decoration_GmRunFlag && |
3705 | visibleLinkOrdinal_DocumentWidget_(d, run->linkId) == ord) { | 4815 | visibleLinkOrdinal_DocumentView_(view, run->linkId) == ord) { |
3706 | if (d->flags & setHoverViaKeys_DocumentWidgetFlag) { | 4816 | if (d->flags & setHoverViaKeys_DocumentWidgetFlag) { |
3707 | d->hoverLink = run; | 4817 | view->hoverLink = run; |
3708 | } | 4818 | } |
3709 | else { | 4819 | else { |
3710 | postCommandf_Root(w->root, | 4820 | postCommandf_Root( |
3711 | "open newtab:%d url:%s", | 4821 | w->root, |
3712 | (isPinned_DocumentWidget_(d) ? otherRoot_OpenTabFlag : 0) ^ | 4822 | "open newtab:%d url:%s", |
3713 | (d->ordinalMode == | 4823 | (isPinned_DocumentWidget_(d) ? otherRoot_OpenTabFlag : 0) ^ |
3714 | numbersAndAlphabet_DocumentLinkOrdinalMode | 4824 | (d->ordinalMode == numbersAndAlphabet_DocumentLinkOrdinalMode |
3715 | ? openTabMode_Sym(modState_Keys()) | 4825 | ? openTabMode_Sym(modState_Keys()) |
3716 | : (d->flags & newTabViaHomeKeys_DocumentWidgetFlag ? 1 : 0)), | 4826 | : (d->flags & newTabViaHomeKeys_DocumentWidgetFlag ? 1 : 0)), |
3717 | cstr_String(absoluteUrl_String( | 4827 | cstr_String(absoluteUrl_String( |
3718 | d->mod.url, linkUrl_GmDocument(d->doc, run->linkId)))); | 4828 | d->mod.url, linkUrl_GmDocument(view->doc, run->linkId)))); |
4829 | interactingWithLink_DocumentWidget_(d, run->linkId); | ||
3719 | } | 4830 | } |
3720 | setLinkNumberMode_DocumentWidget_(d, iFalse); | 4831 | setLinkNumberMode_DocumentWidget_(d, iFalse); |
3721 | invalidateVisibleLinks_DocumentWidget_(d); | 4832 | invalidateVisibleLinks_DocumentView_(view); |
3722 | refresh_Widget(d); | 4833 | refresh_Widget(d); |
3723 | return iTrue; | 4834 | return iTrue; |
3724 | } | 4835 | } |
@@ -3728,7 +4839,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
3728 | case SDLK_ESCAPE: | 4839 | case SDLK_ESCAPE: |
3729 | if (d->flags & showLinkNumbers_DocumentWidgetFlag && document_App() == d) { | 4840 | if (d->flags & showLinkNumbers_DocumentWidgetFlag && document_App() == d) { |
3730 | setLinkNumberMode_DocumentWidget_(d, iFalse); | 4841 | setLinkNumberMode_DocumentWidget_(d, iFalse); |
3731 | invalidateVisibleLinks_DocumentWidget_(d); | 4842 | invalidateVisibleLinks_DocumentView_(view); |
3732 | refresh_Widget(d); | 4843 | refresh_Widget(d); |
3733 | return iTrue; | 4844 | return iTrue; |
3734 | } | 4845 | } |
@@ -3740,7 +4851,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
3740 | for (size_t i = 0; i < 64; ++i) { | 4851 | for (size_t i = 0; i < 64; ++i) { |
3741 | setByte_Block(seed, i, iRandom(0, 256)); | 4852 | setByte_Block(seed, i, iRandom(0, 256)); |
3742 | } | 4853 | } |
3743 | setThemeSeed_GmDocument(d->doc, seed); | 4854 | setThemeSeed_GmDocument(view->doc, seed); |
3744 | delete_Block(seed); | 4855 | delete_Block(seed); |
3745 | invalidate_DocumentWidget_(d); | 4856 | invalidate_DocumentWidget_(d); |
3746 | refresh_Widget(w); | 4857 | refresh_Widget(w); |
@@ -3770,13 +4881,24 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
3770 | #endif | 4881 | #endif |
3771 | } | 4882 | } |
3772 | } | 4883 | } |
4884 | #if defined (iPlatformAppleDesktop) | ||
4885 | else if (ev->type == SDL_MOUSEWHEEL && | ||
4886 | ev->wheel.y == 0 && | ||
4887 | d->wheelSwipeState == direct_WheelSwipeState && | ||
4888 | handleWheelSwipe_DocumentWidget_(d, &ev->wheel)) { | ||
4889 | return iTrue; | ||
4890 | } | ||
4891 | #endif | ||
3773 | else if (ev->type == SDL_MOUSEWHEEL && isHover_Widget(w)) { | 4892 | else if (ev->type == SDL_MOUSEWHEEL && isHover_Widget(w)) { |
3774 | const iInt2 mouseCoord = coord_MouseWheelEvent(&ev->wheel); | 4893 | const iInt2 mouseCoord = coord_MouseWheelEvent(&ev->wheel); |
3775 | if (isPerPixel_MouseWheelEvent(&ev->wheel)) { | 4894 | if (isPerPixel_MouseWheelEvent(&ev->wheel)) { |
3776 | const iInt2 wheel = init_I2(ev->wheel.x, ev->wheel.y); | 4895 | const iInt2 wheel = init_I2(ev->wheel.x, ev->wheel.y); |
3777 | stop_Anim(&d->scrollY.pos); | 4896 | stop_Anim(&d->view.scrollY.pos); |
3778 | immediateScroll_DocumentWidget_(d, -wheel.y); | 4897 | immediateScroll_DocumentView_(view, -wheel.y); |
3779 | scrollWideBlock_DocumentWidget_(d, mouseCoord, -wheel.x, 0); | 4898 | if (!scrollWideBlock_DocumentView_(view, mouseCoord, -wheel.x, 0) && |
4899 | wheel.x) { | ||
4900 | handleWheelSwipe_DocumentWidget_(d, &ev->wheel); | ||
4901 | } | ||
3780 | } | 4902 | } |
3781 | else { | 4903 | else { |
3782 | /* Traditional mouse wheel. */ | 4904 | /* Traditional mouse wheel. */ |
@@ -3785,16 +4907,11 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
3785 | postCommandf_App("zoom.delta arg:%d", amount > 0 ? 10 : -10); | 4907 | postCommandf_App("zoom.delta arg:%d", amount > 0 ? 10 : -10); |
3786 | return iTrue; | 4908 | return iTrue; |
3787 | } | 4909 | } |
3788 | smoothScroll_DocumentWidget_( | 4910 | smoothScroll_DocumentView_(view, |
3789 | d, | 4911 | -3 * amount * lineHeight_Text(paragraph_FontId), |
3790 | -3 * amount * lineHeight_Text(paragraph_FontId), | 4912 | smoothDuration_DocumentWidget_(mouse_ScrollType)); |
3791 | smoothDuration_DocumentWidget_(mouse_ScrollType)); | 4913 | scrollWideBlock_DocumentView_( |
3792 | /* accelerated speed for repeated wheelings */ | 4914 | view, mouseCoord, -3 * ev->wheel.x * lineHeight_Text(paragraph_FontId), 167); |
3793 | // * (!isFinished_SmoothScroll(&d->scrollY) && pos_Anim(&d->scrollY.pos) < 0.25f | ||
3794 | // ? 0.5f | ||
3795 | // : 1.0f)); | ||
3796 | scrollWideBlock_DocumentWidget_( | ||
3797 | d, mouseCoord, -3 * ev->wheel.x * lineHeight_Text(paragraph_FontId), 167); | ||
3798 | } | 4915 | } |
3799 | iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iTrue); | 4916 | iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iTrue); |
3800 | return iTrue; | 4917 | return iTrue; |
@@ -3813,16 +4930,19 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
3813 | } | 4930 | } |
3814 | #endif | 4931 | #endif |
3815 | else { | 4932 | else { |
3816 | if (value_Anim(&d->altTextOpacity) < 0.833f) { | 4933 | if (value_Anim(&view->altTextOpacity) < 0.833f) { |
3817 | setValue_Anim(&d->altTextOpacity, 0, 0); /* keep it hidden while moving */ | 4934 | setValue_Anim(&view->altTextOpacity, 0, 0); /* keep it hidden while moving */ |
3818 | } | 4935 | } |
3819 | updateHover_DocumentWidget_(d, mpos); | 4936 | updateHover_DocumentView_(view, mpos); |
3820 | } | 4937 | } |
3821 | } | 4938 | } |
3822 | if (ev->type == SDL_USEREVENT && ev->user.code == widgetTapBegins_UserEventCode) { | 4939 | if (ev->type == SDL_USEREVENT && ev->user.code == widgetTapBegins_UserEventCode) { |
3823 | iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iFalse); | 4940 | iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iFalse); |
3824 | return iTrue; | 4941 | return iTrue; |
3825 | } | 4942 | } |
4943 | if (processMediaEvents_DocumentWidget_(d, ev)) { | ||
4944 | return iTrue; | ||
4945 | } | ||
3826 | if (ev->type == SDL_MOUSEBUTTONDOWN) { | 4946 | if (ev->type == SDL_MOUSEBUTTONDOWN) { |
3827 | if (ev->button.button == SDL_BUTTON_X1) { | 4947 | if (ev->button.button == SDL_BUTTON_X1) { |
3828 | postCommand_Root(w->root, "navigate.back"); | 4948 | postCommand_Root(w->root, "navigate.back"); |
@@ -3832,17 +4952,18 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
3832 | postCommand_Root(w->root, "navigate.forward"); | 4952 | postCommand_Root(w->root, "navigate.forward"); |
3833 | return iTrue; | 4953 | return iTrue; |
3834 | } | 4954 | } |
3835 | if (ev->button.button == SDL_BUTTON_MIDDLE && d->hoverLink) { | 4955 | if (ev->button.button == SDL_BUTTON_MIDDLE && view->hoverLink) { |
4956 | interactingWithLink_DocumentWidget_(d, view->hoverLink->linkId); | ||
3836 | postCommandf_Root(w->root, "open newtab:%d url:%s", | 4957 | postCommandf_Root(w->root, "open newtab:%d url:%s", |
3837 | (isPinned_DocumentWidget_(d) ? otherRoot_OpenTabFlag : 0) | | 4958 | (isPinned_DocumentWidget_(d) ? otherRoot_OpenTabFlag : 0) | |
3838 | (modState_Keys() & KMOD_SHIFT ? new_OpenTabFlag : newBackground_OpenTabFlag), | 4959 | (modState_Keys() & KMOD_SHIFT ? new_OpenTabFlag : newBackground_OpenTabFlag), |
3839 | cstr_String(linkUrl_GmDocument(d->doc, d->hoverLink->linkId))); | 4960 | cstr_String(linkUrl_GmDocument(view->doc, view->hoverLink->linkId))); |
3840 | return iTrue; | 4961 | return iTrue; |
3841 | } | 4962 | } |
3842 | if (ev->button.button == SDL_BUTTON_RIGHT && | 4963 | if (ev->button.button == SDL_BUTTON_RIGHT && |
3843 | contains_Widget(w, init_I2(ev->button.x, ev->button.y))) { | 4964 | contains_Widget(w, init_I2(ev->button.x, ev->button.y))) { |
3844 | if (!isVisible_Widget(d->menu)) { | 4965 | if (!isVisible_Widget(d->menu)) { |
3845 | d->contextLink = d->hoverLink; | 4966 | d->contextLink = view->hoverLink; |
3846 | d->contextPos = init_I2(ev->button.x, ev->button.y); | 4967 | d->contextPos = init_I2(ev->button.x, ev->button.y); |
3847 | if (d->menu) { | 4968 | if (d->menu) { |
3848 | destroy_Widget(d->menu); | 4969 | destroy_Widget(d->menu); |
@@ -3853,15 +4974,18 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
3853 | init_Array(&items, sizeof(iMenuItem)); | 4974 | init_Array(&items, sizeof(iMenuItem)); |
3854 | if (d->contextLink) { | 4975 | if (d->contextLink) { |
3855 | /* Context menu for a link. */ | 4976 | /* Context menu for a link. */ |
3856 | const iString *linkUrl = linkUrl_GmDocument(d->doc, d->contextLink->linkId); | 4977 | interactingWithLink_DocumentWidget_(d, d->contextLink->linkId); /* perhaps will be triggered */ |
4978 | const iString *linkUrl = linkUrl_GmDocument(view->doc, d->contextLink->linkId); | ||
3857 | // const int linkFlags = linkFlags_GmDocument(d->doc, d->contextLink->linkId); | 4979 | // const int linkFlags = linkFlags_GmDocument(d->doc, d->contextLink->linkId); |
3858 | const iRangecc scheme = urlScheme_String(linkUrl); | 4980 | const iRangecc scheme = urlScheme_String(linkUrl); |
3859 | const iBool isGemini = equalCase_Rangecc(scheme, "gemini"); | 4981 | const iBool isGemini = equalCase_Rangecc(scheme, "gemini"); |
3860 | iBool isNative = iFalse; | 4982 | iBool isNative = iFalse; |
3861 | if (deviceType_App() != desktop_AppDeviceType) { | 4983 | if (deviceType_App() != desktop_AppDeviceType) { |
3862 | /* Show the link as the first, non-interactive item. */ | 4984 | /* Show the link as the first, non-interactive item. */ |
4985 | iString *infoText = collectNew_String(); | ||
4986 | infoText_LinkInfo(d->view.doc, d->contextLink->linkId, infoText); | ||
3863 | pushBack_Array(&items, &(iMenuItem){ | 4987 | pushBack_Array(&items, &(iMenuItem){ |
3864 | format_CStr("```%s", cstr_String(linkUrl)), | 4988 | format_CStr("```%s", cstr_String(infoText)), |
3865 | 0, 0, NULL }); | 4989 | 0, 0, NULL }); |
3866 | } | 4990 | } |
3867 | if (willUseProxy_App(scheme) || isGemini || | 4991 | if (willUseProxy_App(scheme) || isGemini || |
@@ -3872,27 +4996,59 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
3872 | /* Regular links that we can open. */ | 4996 | /* Regular links that we can open. */ |
3873 | pushBackN_Array( | 4997 | pushBackN_Array( |
3874 | &items, | 4998 | &items, |
3875 | (iMenuItem[]){ | 4999 | (iMenuItem[]){ { openTab_Icon " ${link.newtab}", |
3876 | { openTab_Icon " ${link.newtab}", | 5000 | 0, |
3877 | 0, | 5001 | 0, |
3878 | 0, | 5002 | format_CStr("!open newtab:1 origin:%s url:%s", |
3879 | format_CStr("!open newtab:1 url:%s", cstr_String(linkUrl)) }, | 5003 | cstr_String(id_Widget(w)), |
3880 | { openTabBg_Icon " ${link.newtab.background}", | 5004 | cstr_String(linkUrl)) }, |
3881 | 0, | 5005 | { openTabBg_Icon " ${link.newtab.background}", |
3882 | 0, | 5006 | 0, |
3883 | format_CStr("!open newtab:2 url:%s", cstr_String(linkUrl)) }, | 5007 | 0, |
3884 | { "${link.side}", | 5008 | format_CStr("!open newtab:2 origin:%s url:%s", |
3885 | 0, | 5009 | cstr_String(id_Widget(w)), |
3886 | 0, | 5010 | cstr_String(linkUrl)) }, |
3887 | format_CStr("!open newtab:4 url:%s", cstr_String(linkUrl)) }, | 5011 | { "${link.side}", |
3888 | { "${link.side.newtab}", | 5012 | 0, |
3889 | 0, | 5013 | 0, |
3890 | 0, | 5014 | format_CStr("!open newtab:4 origin:%s url:%s", |
3891 | format_CStr("!open newtab:5 url:%s", cstr_String(linkUrl)) } }, | 5015 | cstr_String(id_Widget(w)), |
5016 | cstr_String(linkUrl)) }, | ||
5017 | { "${link.side.newtab}", | ||
5018 | 0, | ||
5019 | 0, | ||
5020 | format_CStr("!open newtab:5 origin:%s url:%s", | ||
5021 | cstr_String(id_Widget(w)), | ||
5022 | cstr_String(linkUrl)) } }, | ||
3892 | 4); | 5023 | 4); |
3893 | if (deviceType_App() == phone_AppDeviceType) { | 5024 | if (deviceType_App() == phone_AppDeviceType) { |
3894 | removeN_Array(&items, size_Array(&items) - 2, iInvalidSize); | 5025 | removeN_Array(&items, size_Array(&items) - 2, iInvalidSize); |
3895 | } | 5026 | } |
5027 | if (equalCase_Rangecc(scheme, "file")) { | ||
5028 | pushBack_Array(&items, &(iMenuItem){ "---" }); | ||
5029 | pushBack_Array(&items, | ||
5030 | &(iMenuItem){ export_Icon " ${menu.open.external}", | ||
5031 | 0, | ||
5032 | 0, | ||
5033 | format_CStr("!open default:1 url:%s", | ||
5034 | cstr_String(linkUrl)) }); | ||
5035 | #if defined (iPlatformAppleDesktop) | ||
5036 | pushBack_Array(&items, | ||
5037 | &(iMenuItem){ "${menu.reveal.macos}", | ||
5038 | 0, | ||
5039 | 0, | ||
5040 | format_CStr("!reveal url:%s", | ||
5041 | cstr_String(linkUrl)) }); | ||
5042 | #endif | ||
5043 | #if defined (iPlatformLinux) | ||
5044 | pushBack_Array(&items, | ||
5045 | &(iMenuItem){ "${menu.reveal.filemgr}", | ||
5046 | 0, | ||
5047 | 0, | ||
5048 | format_CStr("!reveal url:%s", | ||
5049 | cstr_String(linkUrl)) }); | ||
5050 | #endif | ||
5051 | } | ||
3896 | } | 5052 | } |
3897 | else if (!willUseProxy_App(scheme)) { | 5053 | else if (!willUseProxy_App(scheme)) { |
3898 | pushBack_Array( | 5054 | pushBack_Array( |
@@ -3910,11 +5066,13 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
3910 | { isGemini ? "${link.noproxy}" : openExt_Icon " ${link.browser}", | 5066 | { isGemini ? "${link.noproxy}" : openExt_Icon " ${link.browser}", |
3911 | 0, | 5067 | 0, |
3912 | 0, | 5068 | 0, |
3913 | format_CStr("!open noproxy:1 url:%s", cstr_String(linkUrl)) } }, | 5069 | format_CStr("!open origin:%s noproxy:1 url:%s", |
5070 | cstr_String(id_Widget(w)), | ||
5071 | cstr_String(linkUrl)) } }, | ||
3914 | 2); | 5072 | 2); |
3915 | } | 5073 | } |
3916 | iString *linkLabel = collectNewRange_String( | 5074 | iString *linkLabel = collectNewRange_String( |
3917 | linkLabel_GmDocument(d->doc, d->contextLink->linkId)); | 5075 | linkLabel_GmDocument(view->doc, d->contextLink->linkId)); |
3918 | urlEncodeSpaces_String(linkLabel); | 5076 | urlEncodeSpaces_String(linkLabel); |
3919 | pushBackN_Array(&items, | 5077 | pushBackN_Array(&items, |
3920 | (iMenuItem[]){ { "---" }, | 5078 | (iMenuItem[]){ { "---" }, |
@@ -3927,7 +5085,8 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
3927 | cstr_String(linkUrl)) }, | 5085 | cstr_String(linkUrl)) }, |
3928 | }, | 5086 | }, |
3929 | 3); | 5087 | 3); |
3930 | if (isNative && d->contextLink->mediaType != download_MediaType) { | 5088 | if (isNative && d->contextLink->mediaType != download_MediaType && |
5089 | !equalCase_Rangecc(scheme, "file")) { | ||
3931 | pushBackN_Array(&items, (iMenuItem[]){ | 5090 | pushBackN_Array(&items, (iMenuItem[]){ |
3932 | { "---" }, | 5091 | { "---" }, |
3933 | { download_Icon " ${link.download}", 0, 0, "document.downloadlink" }, | 5092 | { download_Icon " ${link.download}", 0, 0, "document.downloadlink" }, |
@@ -3947,6 +5106,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
3947 | } | 5106 | } |
3948 | if (equalCase_Rangecc(scheme, "file")) { | 5107 | if (equalCase_Rangecc(scheme, "file")) { |
3949 | /* Local files may be deleted. */ | 5108 | /* Local files may be deleted. */ |
5109 | pushBack_Array(&items, &(iMenuItem){ "---" }); | ||
3950 | pushBack_Array( | 5110 | pushBack_Array( |
3951 | &items, | 5111 | &items, |
3952 | &(iMenuItem){ delete_Icon " " uiTextCaution_ColorEscape | 5112 | &(iMenuItem){ delete_Icon " " uiTextCaution_ColorEscape |
@@ -3982,9 +5142,10 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
3982 | { book_Icon " ${menu.page.import}", 0, 0, "bookmark.links confirm:1" }, | 5142 | { book_Icon " ${menu.page.import}", 0, 0, "bookmark.links confirm:1" }, |
3983 | { globe_Icon " ${menu.page.translate}", 0, 0, "document.translate" }, | 5143 | { globe_Icon " ${menu.page.translate}", 0, 0, "document.translate" }, |
3984 | { upload_Icon " ${menu.page.upload}", 0, 0, "document.upload" }, | 5144 | { upload_Icon " ${menu.page.upload}", 0, 0, "document.upload" }, |
5145 | { "${menu.page.upload.edit}", 0, 0, "document.upload copy:1" }, | ||
3985 | { "---" }, | 5146 | { "---" }, |
3986 | { "${menu.page.copyurl}", 0, 0, "document.copylink" } }, | 5147 | { "${menu.page.copyurl}", 0, 0, "document.copylink" } }, |
3987 | 16); | 5148 | 17); |
3988 | if (isEmpty_Range(&d->selectMark)) { | 5149 | if (isEmpty_Range(&d->selectMark)) { |
3989 | pushBackN_Array( | 5150 | pushBackN_Array( |
3990 | &items, | 5151 | &items, |
@@ -4020,9 +5181,6 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
4020 | processContextMenuEvent_Widget(d->menu, ev, {}); | 5181 | processContextMenuEvent_Widget(d->menu, ev, {}); |
4021 | } | 5182 | } |
4022 | } | 5183 | } |
4023 | if (processMediaEvents_DocumentWidget_(d, ev)) { | ||
4024 | return iTrue; | ||
4025 | } | ||
4026 | if (processEvent_Banner(d->banner, ev)) { | 5184 | if (processEvent_Banner(d->banner, ev)) { |
4027 | return iTrue; | 5185 | return iTrue; |
4028 | } | 5186 | } |
@@ -4035,7 +5193,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
4035 | /* Enable hover state now that scrolling has surely finished. */ | 5193 | /* Enable hover state now that scrolling has surely finished. */ |
4036 | if (d->flags & noHoverWhileScrolling_DocumentWidgetFlag) { | 5194 | if (d->flags & noHoverWhileScrolling_DocumentWidgetFlag) { |
4037 | d->flags &= ~noHoverWhileScrolling_DocumentWidgetFlag; | 5195 | d->flags &= ~noHoverWhileScrolling_DocumentWidgetFlag; |
4038 | updateHover_DocumentWidget_(d, mouseCoord_Window(get_Window(), ev->button.which)); | 5196 | updateHover_DocumentView_(view, mouseCoord_Window(get_Window(), ev->button.which)); |
4039 | } | 5197 | } |
4040 | if (~flags_Widget(w) & touchDrag_WidgetFlag) { | 5198 | if (~flags_Widget(w) & touchDrag_WidgetFlag) { |
4041 | iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iFalse); | 5199 | iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iFalse); |
@@ -4046,7 +5204,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
4046 | beginMarkingSelection_DocumentWidget_(d, d->click.startPos); | 5204 | beginMarkingSelection_DocumentWidget_(d, d->click.startPos); |
4047 | extendRange_Rangecc( | 5205 | extendRange_Rangecc( |
4048 | &d->selectMark, | 5206 | &d->selectMark, |
4049 | range_String(source_GmDocument(d->doc)), | 5207 | range_String(source_GmDocument(view->doc)), |
4050 | bothStartAndEnd_RangeExtension | | 5208 | bothStartAndEnd_RangeExtension | |
4051 | (d->click.count == 2 ? word_RangeExtension : line_RangeExtension)); | 5209 | (d->click.count == 2 ? word_RangeExtension : line_RangeExtension)); |
4052 | d->initialSelectMark = d->selectMark; | 5210 | d->initialSelectMark = d->selectMark; |
@@ -4060,24 +5218,24 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
4060 | case drag_ClickResult: { | 5218 | case drag_ClickResult: { |
4061 | if (d->grabbedPlayer) { | 5219 | if (d->grabbedPlayer) { |
4062 | iPlayer *plr = | 5220 | iPlayer *plr = |
4063 | audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(d->grabbedPlayer)); | 5221 | audioPlayer_Media(media_GmDocument(view->doc), mediaId_GmRun(d->grabbedPlayer)); |
4064 | iPlayerUI ui; | 5222 | iPlayerUI ui; |
4065 | init_PlayerUI(&ui, plr, runRect_DocumentWidget_(d, d->grabbedPlayer)); | 5223 | init_PlayerUI(&ui, plr, runRect_DocumentView_(view, d->grabbedPlayer)); |
4066 | float off = (float) delta_Click(&d->click).x / (float) width_Rect(ui.volumeSlider); | 5224 | float off = (float) delta_Click(&d->click).x / (float) width_Rect(ui.volumeSlider); |
4067 | setVolume_Player(plr, d->grabbedStartVolume + off); | 5225 | setVolume_Player(plr, d->grabbedStartVolume + off); |
4068 | refresh_Widget(w); | 5226 | refresh_Widget(w); |
4069 | return iTrue; | 5227 | return iTrue; |
4070 | } | 5228 | } |
4071 | /* Fold/unfold a preformatted block. */ | 5229 | /* Fold/unfold a preformatted block. */ |
4072 | if (~d->flags & selecting_DocumentWidgetFlag && d->hoverPre && | 5230 | if (~d->flags & selecting_DocumentWidgetFlag && view->hoverPre && |
4073 | preIsFolded_GmDocument(d->doc, preId_GmRun(d->hoverPre))) { | 5231 | preIsFolded_GmDocument(view->doc, preId_GmRun(view->hoverPre))) { |
4074 | return iTrue; | 5232 | return iTrue; |
4075 | } | 5233 | } |
4076 | /* Begin selecting a range of text. */ | 5234 | /* Begin selecting a range of text. */ |
4077 | if (~d->flags & selecting_DocumentWidgetFlag) { | 5235 | if (~d->flags & selecting_DocumentWidgetFlag) { |
4078 | beginMarkingSelection_DocumentWidget_(d, d->click.startPos); | 5236 | beginMarkingSelection_DocumentWidget_(d, d->click.startPos); |
4079 | } | 5237 | } |
4080 | iRangecc loc = sourceLoc_DocumentWidget_(d, pos_Click(&d->click)); | 5238 | iRangecc loc = sourceLoc_DocumentView_(view, pos_Click(&d->click)); |
4081 | if (d->selectMark.start == NULL) { | 5239 | if (d->selectMark.start == NULL) { |
4082 | d->selectMark = loc; | 5240 | d->selectMark = loc; |
4083 | } | 5241 | } |
@@ -4088,7 +5246,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
4088 | movingSelectMarkEnd_DocumentWidgetFlag))) { | 5246 | movingSelectMarkEnd_DocumentWidgetFlag))) { |
4089 | const iRangecc mark = selectMark_DocumentWidget_(d); | 5247 | const iRangecc mark = selectMark_DocumentWidget_(d); |
4090 | const char * midMark = mark.start + size_Range(&mark) / 2; | 5248 | const char * midMark = mark.start + size_Range(&mark) / 2; |
4091 | const iRangecc loc = sourceLoc_DocumentWidget_(d, pos_Click(&d->click)); | 5249 | const iRangecc loc = sourceLoc_DocumentView_(view, pos_Click(&d->click)); |
4092 | const iBool isCloserToStart = d->selectMark.start > d->selectMark.end ? | 5250 | const iBool isCloserToStart = d->selectMark.start > d->selectMark.end ? |
4093 | (loc.start > midMark) : (loc.start < midMark); | 5251 | (loc.start > midMark) : (loc.start < midMark); |
4094 | iChangeFlags(d->flags, movingSelectMarkStart_DocumentWidgetFlag, isCloserToStart); | 5252 | iChangeFlags(d->flags, movingSelectMarkStart_DocumentWidgetFlag, isCloserToStart); |
@@ -4118,7 +5276,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
4118 | if (d->flags & (selectWords_DocumentWidgetFlag | selectLines_DocumentWidgetFlag)) { | 5276 | if (d->flags & (selectWords_DocumentWidgetFlag | selectLines_DocumentWidgetFlag)) { |
4119 | extendRange_Rangecc( | 5277 | extendRange_Rangecc( |
4120 | &d->selectMark, | 5278 | &d->selectMark, |
4121 | range_String(source_GmDocument(d->doc)), | 5279 | range_String(source_GmDocument(view->doc)), |
4122 | (d->flags & movingSelectMarkStart_DocumentWidgetFlag ? moveStart_RangeExtension | 5280 | (d->flags & movingSelectMarkStart_DocumentWidgetFlag ? moveStart_RangeExtension |
4123 | : moveEnd_RangeExtension) | | 5281 | : moveEnd_RangeExtension) | |
4124 | (d->flags & selectWords_DocumentWidgetFlag ? word_RangeExtension | 5282 | (d->flags & selectWords_DocumentWidgetFlag ? word_RangeExtension |
@@ -4156,7 +5314,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
4156 | setFocus_Widget(NULL); | 5314 | setFocus_Widget(NULL); |
4157 | /* Tap in tap selection mode. */ | 5315 | /* Tap in tap selection mode. */ |
4158 | if (flags_Widget(w) & touchDrag_WidgetFlag) { | 5316 | if (flags_Widget(w) & touchDrag_WidgetFlag) { |
4159 | const iRangecc tapLoc = sourceLoc_DocumentWidget_(d, pos_Click(&d->click)); | 5317 | const iRangecc tapLoc = sourceLoc_DocumentView_(view, pos_Click(&d->click)); |
4160 | /* Tapping on the selection will show a menu. */ | 5318 | /* Tapping on the selection will show a menu. */ |
4161 | const iRangecc mark = selectMark_DocumentWidget_(d); | 5319 | const iRangecc mark = selectMark_DocumentWidget_(d); |
4162 | if (tapLoc.start >= mark.start && tapLoc.end <= mark.end) { | 5320 | if (tapLoc.start >= mark.start && tapLoc.end <= mark.end) { |
@@ -4165,11 +5323,15 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
4165 | destroy_Widget(d->copyMenu); | 5323 | destroy_Widget(d->copyMenu); |
4166 | d->copyMenu = NULL; | 5324 | d->copyMenu = NULL; |
4167 | } | 5325 | } |
4168 | d->copyMenu = makeMenu_Widget(w, (iMenuItem[]){ | 5326 | const iMenuItem items[] = { |
4169 | { clipCopy_Icon " ${menu.copy}", 0, 0, "copy" }, | 5327 | { clipCopy_Icon " ${menu.copy}", 0, 0, "copy" }, |
5328 | #if defined (iPlatformAppleMobile) | ||
5329 | { export_Icon " ${menu.share}", 0, 0, "copy share:1" }, | ||
5330 | #endif | ||
4170 | { "---" }, | 5331 | { "---" }, |
4171 | { close_Icon " ${menu.select.clear}", 0, 0, "document.select arg:0" }, | 5332 | { close_Icon " ${menu.select.clear}", 0, 0, "document.select arg:0" }, |
4172 | }, 3); | 5333 | }; |
5334 | d->copyMenu = makeMenu_Widget(w, items, iElemCount(items)); | ||
4173 | setFlags_Widget(d->copyMenu, noFadeBackground_WidgetFlag, iTrue); | 5335 | setFlags_Widget(d->copyMenu, noFadeBackground_WidgetFlag, iTrue); |
4174 | openMenu_Widget(d->copyMenu, pos_Click(&d->click)); | 5336 | openMenu_Widget(d->copyMenu, pos_Click(&d->click)); |
4175 | return iTrue; | 5337 | return iTrue; |
@@ -4180,18 +5342,18 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
4180 | return iTrue; | 5342 | return iTrue; |
4181 | } | 5343 | } |
4182 | } | 5344 | } |
4183 | if (d->hoverPre) { | 5345 | if (view->hoverPre) { |
4184 | togglePreFold_DocumentWidget_(d, preId_GmRun(d->hoverPre)); | 5346 | togglePreFold_DocumentWidget_(d, preId_GmRun(view->hoverPre)); |
4185 | return iTrue; | 5347 | return iTrue; |
4186 | } | 5348 | } |
4187 | if (d->hoverLink) { | 5349 | if (view->hoverLink) { |
4188 | /* TODO: Move this to a method. */ | 5350 | /* TODO: Move this to a method. */ |
4189 | const iGmLinkId linkId = d->hoverLink->linkId; | 5351 | const iGmLinkId linkId = view->hoverLink->linkId; |
4190 | const iMediaId linkMedia = mediaId_GmRun(d->hoverLink); | 5352 | const iMediaId linkMedia = mediaId_GmRun(view->hoverLink); |
4191 | const int linkFlags = linkFlags_GmDocument(d->doc, linkId); | 5353 | const int linkFlags = linkFlags_GmDocument(view->doc, linkId); |
4192 | iAssert(linkId); | 5354 | iAssert(linkId); |
4193 | /* Media links are opened inline by default. */ | 5355 | /* Media links are opened inline by default. */ |
4194 | if (isMediaLink_GmDocument(d->doc, linkId)) { | 5356 | if (isMediaLink_GmDocument(view->doc, linkId)) { |
4195 | if (linkFlags & content_GmLinkFlag && linkFlags & permanent_GmLinkFlag) { | 5357 | if (linkFlags & content_GmLinkFlag && linkFlags & permanent_GmLinkFlag) { |
4196 | /* We have the content and it cannot be dismissed, so nothing | 5358 | /* We have the content and it cannot be dismissed, so nothing |
4197 | further to do. */ | 5359 | further to do. */ |
@@ -4200,7 +5362,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
4200 | if (!requestMedia_DocumentWidget_(d, linkId, iTrue)) { | 5362 | if (!requestMedia_DocumentWidget_(d, linkId, iTrue)) { |
4201 | if (linkFlags & content_GmLinkFlag) { | 5363 | if (linkFlags & content_GmLinkFlag) { |
4202 | /* Dismiss shown content on click. */ | 5364 | /* Dismiss shown content on click. */ |
4203 | setData_Media(media_GmDocument(d->doc), | 5365 | setData_Media(media_GmDocument(view->doc), |
4204 | linkId, | 5366 | linkId, |
4205 | NULL, | 5367 | NULL, |
4206 | NULL, | 5368 | NULL, |
@@ -4214,10 +5376,10 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
4214 | be redone. */ | 5376 | be redone. */ |
4215 | } | 5377 | } |
4216 | } | 5378 | } |
4217 | redoLayout_GmDocument(d->doc); | 5379 | redoLayout_GmDocument(view->doc); |
4218 | d->hoverLink = NULL; | 5380 | view->hoverLink = NULL; |
4219 | clampScroll_DocumentWidget_(d); | 5381 | clampScroll_DocumentView_(view); |
4220 | updateVisible_DocumentWidget_(d); | 5382 | updateVisible_DocumentView_(view); |
4221 | invalidate_DocumentWidget_(d); | 5383 | invalidate_DocumentWidget_(d); |
4222 | refresh_Widget(w); | 5384 | refresh_Widget(w); |
4223 | return iTrue; | 5385 | return iTrue; |
@@ -4226,13 +5388,13 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
4226 | /* Show the existing content again if we have it. */ | 5388 | /* Show the existing content again if we have it. */ |
4227 | iMediaRequest *req = findMediaRequest_DocumentWidget_(d, linkId); | 5389 | iMediaRequest *req = findMediaRequest_DocumentWidget_(d, linkId); |
4228 | if (req) { | 5390 | if (req) { |
4229 | setData_Media(media_GmDocument(d->doc), | 5391 | setData_Media(media_GmDocument(view->doc), |
4230 | linkId, | 5392 | linkId, |
4231 | meta_GmRequest(req->req), | 5393 | meta_GmRequest(req->req), |
4232 | body_GmRequest(req->req), | 5394 | body_GmRequest(req->req), |
4233 | allowHide_MediaFlag); | 5395 | allowHide_MediaFlag); |
4234 | redoLayout_GmDocument(d->doc); | 5396 | redoLayout_GmDocument(view->doc); |
4235 | updateVisible_DocumentWidget_(d); | 5397 | updateVisible_DocumentView_(view); |
4236 | invalidate_DocumentWidget_(d); | 5398 | invalidate_DocumentWidget_(d); |
4237 | refresh_Widget(w); | 5399 | refresh_Widget(w); |
4238 | return iTrue; | 5400 | return iTrue; |
@@ -4252,14 +5414,15 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
4252 | if (isPinned_DocumentWidget_(d)) { | 5414 | if (isPinned_DocumentWidget_(d)) { |
4253 | tabMode ^= otherRoot_OpenTabFlag; | 5415 | tabMode ^= otherRoot_OpenTabFlag; |
4254 | } | 5416 | } |
5417 | interactingWithLink_DocumentWidget_(d, linkId); | ||
4255 | postCommandf_Root(w->root, "open newtab:%d url:%s", | 5418 | postCommandf_Root(w->root, "open newtab:%d url:%s", |
4256 | tabMode, | 5419 | tabMode, |
4257 | cstr_String(absoluteUrl_String( | 5420 | cstr_String(absoluteUrl_String( |
4258 | d->mod.url, linkUrl_GmDocument(d->doc, linkId)))); | 5421 | d->mod.url, linkUrl_GmDocument(view->doc, linkId)))); |
4259 | } | 5422 | } |
4260 | else { | 5423 | else { |
4261 | const iString *url = absoluteUrl_String( | 5424 | const iString *url = absoluteUrl_String( |
4262 | d->mod.url, linkUrl_GmDocument(d->doc, linkId)); | 5425 | d->mod.url, linkUrl_GmDocument(view->doc, linkId)); |
4263 | makeQuestion_Widget( | 5426 | makeQuestion_Widget( |
4264 | uiTextCaution_ColorEscape "${heading.openlink}", | 5427 | uiTextCaution_ColorEscape "${heading.openlink}", |
4265 | format_CStr( | 5428 | format_CStr( |
@@ -4292,705 +5455,18 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
4292 | return processEvent_Widget(w, ev); | 5455 | return processEvent_Widget(w, ev); |
4293 | } | 5456 | } |
4294 | 5457 | ||
4295 | iDeclareType(DrawContext) | 5458 | static void checkPendingInvalidation_DocumentWidget_(const iDocumentWidget *d) { |
4296 | 5459 | if (d->flags & invalidationPending_DocumentWidgetFlag && | |
4297 | struct Impl_DrawContext { | 5460 | !isAffectedByVisualOffset_Widget(constAs_Widget(d))) { |
4298 | const iDocumentWidget *widget; | 5461 | // printf("%p visoff: %d\n", d, left_Rect(bounds_Widget(w)) - left_Rect(boundsWithoutVisualOffset_Widget(w))); |
4299 | iRect widgetBounds; | 5462 | iDocumentWidget *m = (iDocumentWidget *) d; /* Hrrm, not const... */ |
4300 | iRect docBounds; | 5463 | m->flags &= ~invalidationPending_DocumentWidgetFlag; |
4301 | iRangei vis; | 5464 | invalidate_DocumentWidget_(m); |
4302 | iInt2 viewPos; /* document area origin */ | ||
4303 | iPaint paint; | ||
4304 | iBool inSelectMark; | ||
4305 | iBool inFoundMark; | ||
4306 | iBool showLinkNumbers; | ||
4307 | iRect firstMarkRect; | ||
4308 | iRect lastMarkRect; | ||
4309 | iGmRunRange runsDrawn; | ||
4310 | }; | ||
4311 | |||
4312 | static void fillRange_DrawContext_(iDrawContext *d, const iGmRun *run, enum iColorId color, | ||
4313 | iRangecc mark, iBool *isInside) { | ||
4314 | if (mark.start > mark.end) { | ||
4315 | /* Selection may be done in either direction. */ | ||
4316 | iSwap(const char *, mark.start, mark.end); | ||
4317 | } | ||
4318 | if (*isInside || (contains_Range(&run->text, mark.start) || | ||
4319 | contains_Range(&mark, run->text.start))) { | ||
4320 | int x = 0; | ||
4321 | if (!*isInside) { | ||
4322 | x = measureRange_Text(run->font, | ||
4323 | (iRangecc){ run->text.start, iMax(run->text.start, mark.start) }) | ||
4324 | .advance.x; | ||
4325 | } | ||
4326 | int w = width_Rect(run->visBounds) - x; | ||
4327 | if (contains_Range(&run->text, mark.end) || mark.end < run->text.start) { | ||
4328 | iRangecc mk = !*isInside ? mark | ||
4329 | : (iRangecc){ run->text.start, iMax(run->text.start, mark.end) }; | ||
4330 | mk.start = iMax(mk.start, run->text.start); | ||
4331 | w = measureRange_Text(run->font, mk).advance.x; | ||
4332 | *isInside = iFalse; | ||
4333 | } | ||
4334 | else { | ||
4335 | *isInside = iTrue; /* at least until the next run */ | ||
4336 | } | ||
4337 | if (w > width_Rect(run->visBounds) - x) { | ||
4338 | w = width_Rect(run->visBounds) - x; | ||
4339 | } | ||
4340 | if (~run->flags & decoration_GmRunFlag) { | ||
4341 | const iInt2 visPos = | ||
4342 | add_I2(run->bounds.pos, addY_I2(d->viewPos, viewPos_DocumentWidget_(d->widget))); | ||
4343 | const iRect rangeRect = { addX_I2(visPos, x), init_I2(w, height_Rect(run->bounds)) }; | ||
4344 | if (rangeRect.size.x) { | ||
4345 | fillRect_Paint(&d->paint, rangeRect, color); | ||
4346 | /* Keep track of the first and last marked rects. */ | ||
4347 | if (d->firstMarkRect.size.x == 0) { | ||
4348 | d->firstMarkRect = rangeRect; | ||
4349 | } | ||
4350 | d->lastMarkRect = rangeRect; | ||
4351 | } | ||
4352 | } | ||
4353 | } | ||
4354 | /* Link URLs are not part of the visible document, so they are ignored above. Handle | ||
4355 | these ranges as a special case. */ | ||
4356 | if (run->linkId && run->flags & decoration_GmRunFlag) { | ||
4357 | const iRangecc url = linkUrlRange_GmDocument(d->widget->doc, run->linkId); | ||
4358 | if (contains_Range(&url, mark.start) && | ||
4359 | (contains_Range(&url, mark.end) || url.end == mark.end)) { | ||
4360 | fillRect_Paint( | ||
4361 | &d->paint, | ||
4362 | moved_Rect(run->visBounds, addY_I2(d->viewPos, viewPos_DocumentWidget_(d->widget))), | ||
4363 | color); | ||
4364 | } | ||
4365 | } | ||
4366 | } | ||
4367 | |||
4368 | static void drawMark_DrawContext_(void *context, const iGmRun *run) { | ||
4369 | iDrawContext *d = context; | ||
4370 | if (!isMedia_GmRun(run)) { | ||
4371 | fillRange_DrawContext_(d, run, uiMatching_ColorId, d->widget->foundMark, &d->inFoundMark); | ||
4372 | fillRange_DrawContext_(d, run, uiMarked_ColorId, d->widget->selectMark, &d->inSelectMark); | ||
4373 | } | ||
4374 | } | ||
4375 | |||
4376 | static void drawRun_DrawContext_(void *context, const iGmRun *run) { | ||
4377 | iDrawContext *d = context; | ||
4378 | const iInt2 origin = d->viewPos; | ||
4379 | /* Keep track of the drawn visible runs. */ { | ||
4380 | if (!d->runsDrawn.start || run < d->runsDrawn.start) { | ||
4381 | d->runsDrawn.start = run; | ||
4382 | } | ||
4383 | if (!d->runsDrawn.end || run > d->runsDrawn.end) { | ||
4384 | d->runsDrawn.end = run; | ||
4385 | } | ||
4386 | } | ||
4387 | if (run->mediaType == image_MediaType) { | ||
4388 | SDL_Texture *tex = imageTexture_Media(media_GmDocument(d->widget->doc), mediaId_GmRun(run)); | ||
4389 | const iRect dst = moved_Rect(run->visBounds, origin); | ||
4390 | if (tex) { | ||
4391 | fillRect_Paint(&d->paint, dst, tmBackground_ColorId); /* in case the image has alpha */ | ||
4392 | SDL_RenderCopy(d->paint.dst->render, tex, NULL, | ||
4393 | &(SDL_Rect){ dst.pos.x, dst.pos.y, dst.size.x, dst.size.y }); | ||
4394 | } | ||
4395 | else { | ||
4396 | drawRect_Paint(&d->paint, dst, tmQuoteIcon_ColorId); | ||
4397 | drawCentered_Text(uiLabel_FontId, | ||
4398 | dst, | ||
4399 | iFalse, | ||
4400 | tmQuote_ColorId, | ||
4401 | explosion_Icon " Error Loading Image"); | ||
4402 | } | ||
4403 | return; | ||
4404 | } | ||
4405 | else if (isMedia_GmRun(run)) { | ||
4406 | /* Media UIs are drawn afterwards as a dynamic overlay. */ | ||
4407 | return; | ||
4408 | } | ||
4409 | enum iColorId fg = run->color; | ||
4410 | const iGmDocument *doc = d->widget->doc; | ||
4411 | const int linkFlags = linkFlags_GmDocument(doc, run->linkId); | ||
4412 | /* Hover state of a link. */ | ||
4413 | iBool isHover = | ||
4414 | (run->linkId && d->widget->hoverLink && run->linkId == d->widget->hoverLink->linkId && | ||
4415 | ~run->flags & decoration_GmRunFlag); | ||
4416 | /* Visible (scrolled) position of the run. */ | ||
4417 | const iInt2 visPos = addX_I2(add_I2(run->visBounds.pos, origin), | ||
4418 | /* Preformatted runs can be scrolled. */ | ||
4419 | runOffset_DocumentWidget_(d->widget, run)); | ||
4420 | const iRect visRect = { visPos, run->visBounds.size }; | ||
4421 | #if 0 | ||
4422 | if (run->flags & footer_GmRunFlag) { | ||
4423 | iRect footerBack = | ||
4424 | (iRect){ visPos, init_I2(width_Rect(d->widgetBounds), run->visBounds.size.y) }; | ||
4425 | footerBack.pos.x = left_Rect(d->widgetBounds); | ||
4426 | fillRect_Paint(&d->paint, footerBack, tmBackground_ColorId); | ||
4427 | return; | ||
4428 | } | ||
4429 | #endif | ||
4430 | /* Fill the background. */ { | ||
4431 | if (run->linkId && linkFlags & isOpen_GmLinkFlag && ~linkFlags & content_GmLinkFlag) { | ||
4432 | /* Open links get a highlighted background. */ | ||
4433 | int bg = tmBackgroundOpenLink_ColorId; | ||
4434 | const int frame = tmFrameOpenLink_ColorId; | ||
4435 | iRect wideRect = { init_I2(left_Rect(d->widgetBounds), visPos.y), | ||
4436 | init_I2(width_Rect(d->widgetBounds) + | ||
4437 | width_Widget(d->widget->scroll), | ||
4438 | height_Rect(run->visBounds)) }; | ||
4439 | /* The first line is composed of two runs that may be drawn in either order, so | ||
4440 | only draw half of the background. */ | ||
4441 | if (run->flags & decoration_GmRunFlag) { | ||
4442 | wideRect.size.x = right_Rect(visRect) - left_Rect(wideRect); | ||
4443 | } | ||
4444 | else if (run->flags & startOfLine_GmRunFlag) { | ||
4445 | wideRect.size.x = right_Rect(wideRect) - left_Rect(visRect); | ||
4446 | wideRect.pos.x = left_Rect(visRect); | ||
4447 | } | ||
4448 | fillRect_Paint(&d->paint, wideRect, bg); | ||
4449 | if (run->flags & (startOfLine_GmRunFlag | decoration_GmRunFlag)) { | ||
4450 | drawHLine_Paint(&d->paint, topLeft_Rect(wideRect), width_Rect(wideRect), frame); | ||
4451 | } | ||
4452 | /* TODO: The decoration is not marked as endOfLine, so it lacks the bottom line. */ | ||
4453 | // if (run->flags & endOfLine_GmRunFlag) { | ||
4454 | // drawHLine_Paint( | ||
4455 | // &d->paint, addY_I2(bottomLeft_Rect(wideRect), -1), width_Rect(wideRect), frame); | ||
4456 | // } | ||
4457 | } | ||
4458 | else { | ||
4459 | /* Normal background for other runs. There are cases when runs get drawn multiple times, | ||
4460 | e.g., at the buffer boundary, and there are slightly overlapping characters in | ||
4461 | monospace blocks. Clearing the background here ensures a cleaner visual appearance | ||
4462 | since only one glyph is visible at any given point. */ | ||
4463 | fillRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmBackground_ColorId); | ||
4464 | } | ||
4465 | } | ||
4466 | if (run->linkId && ~run->flags & decoration_GmRunFlag) { | ||
4467 | fg = linkColor_GmDocument(doc, run->linkId, isHover ? textHover_GmLinkPart : text_GmLinkPart); | ||
4468 | if (linkFlags & content_GmLinkFlag) { | ||
4469 | fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart); /* link is inactive */ | ||
4470 | } | ||
4471 | } | ||
4472 | if (run->flags & altText_GmRunFlag) { | ||
4473 | const iInt2 margin = preRunMargin_GmDocument(doc, preId_GmRun(run)); | ||
4474 | fillRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmBackgroundAltText_ColorId); | ||
4475 | drawRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmFrameAltText_ColorId); | ||
4476 | drawWrapRange_Text(run->font, | ||
4477 | add_I2(visPos, margin), | ||
4478 | run->visBounds.size.x - 2 * margin.x, | ||
4479 | run->color, | ||
4480 | run->text); | ||
4481 | } | ||
4482 | #if 0 | ||
4483 | else if (run->flags & siteBanner_GmRunFlag) { | ||
4484 | /* Banner background. */ | ||
4485 | iRect bannerBack = initCorners_Rect(topLeft_Rect(d->widgetBounds), | ||
4486 | init_I2(right_Rect(bounds_Widget(constAs_Widget(d->widget))), | ||
4487 | visPos.y + height_Rect(run->visBounds))); | ||
4488 | fillRect_Paint(&d->paint, bannerBack, tmBannerBackground_ColorId); | ||
4489 | drawBannerRun_DrawContext_(d, run, visPos); | ||
4490 | } | ||
4491 | #endif | ||
4492 | else { | ||
4493 | if (d->showLinkNumbers && run->linkId && run->flags & decoration_GmRunFlag) { | ||
4494 | const size_t ord = visibleLinkOrdinal_DocumentWidget_(d->widget, run->linkId); | ||
4495 | if (ord >= d->widget->ordinalBase) { | ||
4496 | const iChar ordChar = | ||
4497 | linkOrdinalChar_DocumentWidget_(d->widget, ord - d->widget->ordinalBase); | ||
4498 | if (ordChar) { | ||
4499 | const char *circle = "\u25ef"; /* Large Circle */ | ||
4500 | const int circleFont = FONT_ID(default_FontId, regular_FontStyle, contentRegular_FontSize); | ||
4501 | iRect nbArea = { init_I2(d->viewPos.x - gap_UI / 3, visPos.y), | ||
4502 | init_I2(3.95f * gap_Text, 1.0f * lineHeight_Text(circleFont)) }; | ||
4503 | drawRange_Text( | ||
4504 | circleFont, topLeft_Rect(nbArea), tmQuote_ColorId, range_CStr(circle)); | ||
4505 | iRect circleArea = visualBounds_Text(circleFont, range_CStr(circle)); | ||
4506 | addv_I2(&circleArea.pos, topLeft_Rect(nbArea)); | ||
4507 | drawCentered_Text(FONT_ID(default_FontId, regular_FontStyle, contentSmall_FontSize), | ||
4508 | circleArea, | ||
4509 | iTrue, | ||
4510 | tmQuote_ColorId, | ||
4511 | "%lc", | ||
4512 | (int) ordChar); | ||
4513 | goto runDrawn; | ||
4514 | } | ||
4515 | } | ||
4516 | } | ||
4517 | if (run->flags & quoteBorder_GmRunFlag) { | ||
4518 | drawVLine_Paint(&d->paint, | ||
4519 | addX_I2(visPos, | ||
4520 | !run->isRTL | ||
4521 | ? -gap_Text * 5 / 2 | ||
4522 | : (width_Rect(run->visBounds) + gap_Text * 5 / 2)), | ||
4523 | height_Rect(run->visBounds), | ||
4524 | tmQuoteIcon_ColorId); | ||
4525 | } | ||
4526 | /* Base attributes. */ { | ||
4527 | int f, c; | ||
4528 | runBaseAttributes_GmDocument(doc, run, &f, &c); | ||
4529 | setBaseAttributes_Text(f, c); | ||
4530 | } | ||
4531 | drawBoundRange_Text(run->font, | ||
4532 | visPos, | ||
4533 | (run->isRTL ? -1 : 1) * width_Rect(run->visBounds), | ||
4534 | fg, | ||
4535 | run->text); | ||
4536 | setBaseAttributes_Text(-1, -1); | ||
4537 | runDrawn:; | ||
4538 | } | ||
4539 | /* Presentation of links. */ | ||
4540 | if (run->linkId && ~run->flags & decoration_GmRunFlag) { | ||
4541 | const int metaFont = paragraph_FontId; | ||
4542 | /* TODO: Show status of an ongoing media request. */ | ||
4543 | const int flags = linkFlags; | ||
4544 | const iRect linkRect = moved_Rect(run->visBounds, origin); | ||
4545 | iMediaRequest *mr = NULL; | ||
4546 | /* Show metadata about inline content. */ | ||
4547 | if (flags & content_GmLinkFlag && run->flags & endOfLine_GmRunFlag) { | ||
4548 | fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart); | ||
4549 | iString text; | ||
4550 | init_String(&text); | ||
4551 | const iMediaId linkMedia = findMediaForLink_Media(constMedia_GmDocument(doc), | ||
4552 | run->linkId, none_MediaType); | ||
4553 | iAssert(linkMedia.type != none_MediaType); | ||
4554 | iGmMediaInfo info; | ||
4555 | info_Media(constMedia_GmDocument(doc), linkMedia, &info); | ||
4556 | switch (linkMedia.type) { | ||
4557 | case image_MediaType: { | ||
4558 | iAssert(!isEmpty_Rect(run->bounds)); | ||
4559 | const iInt2 imgSize = imageSize_Media(constMedia_GmDocument(doc), linkMedia); | ||
4560 | format_String(&text, "%s \u2014 %d x %d \u2014 %.1f%s", | ||
4561 | info.type, imgSize.x, imgSize.y, info.numBytes / 1.0e6f, | ||
4562 | cstr_Lang("mb")); | ||
4563 | break; | ||
4564 | } | ||
4565 | case audio_MediaType: | ||
4566 | format_String(&text, "%s", info.type); | ||
4567 | break; | ||
4568 | case download_MediaType: | ||
4569 | format_String(&text, "%s", info.type); | ||
4570 | break; | ||
4571 | default: | ||
4572 | break; | ||
4573 | } | ||
4574 | if (linkMedia.type != download_MediaType && /* can't cancel downloads currently */ | ||
4575 | findMediaRequest_DocumentWidget_(d->widget, run->linkId)) { | ||
4576 | appendFormat_String( | ||
4577 | &text, " %s" close_Icon, isHover ? escape_Color(tmLinkText_ColorId) : ""); | ||
4578 | } | ||
4579 | const iInt2 size = measureRange_Text(metaFont, range_String(&text)).bounds.size; | ||
4580 | fillRect_Paint( | ||
4581 | &d->paint, | ||
4582 | (iRect){ add_I2(origin, addX_I2(topRight_Rect(run->bounds), -size.x - gap_UI)), | ||
4583 | addX_I2(size, 2 * gap_UI) }, | ||
4584 | tmBackground_ColorId); | ||
4585 | drawAlign_Text(metaFont, | ||
4586 | add_I2(topRight_Rect(run->bounds), origin), | ||
4587 | fg, | ||
4588 | right_Alignment, | ||
4589 | "%s", cstr_String(&text)); | ||
4590 | deinit_String(&text); | ||
4591 | } | ||
4592 | else if (run->flags & endOfLine_GmRunFlag && | ||
4593 | (mr = findMediaRequest_DocumentWidget_(d->widget, run->linkId)) != NULL) { | ||
4594 | if (!isFinished_GmRequest(mr->req)) { | ||
4595 | draw_Text(metaFont, | ||
4596 | topRight_Rect(linkRect), | ||
4597 | tmInlineContentMetadata_ColorId, | ||
4598 | translateCStr_Lang(" \u2014 ${doc.fetching}\u2026 (%.1f ${mb})"), | ||
4599 | (float) bodySize_GmRequest(mr->req) / 1.0e6f); | ||
4600 | } | ||
4601 | } | ||
4602 | else if (isHover) { | ||
4603 | const iGmLinkId linkId = d->widget->hoverLink->linkId; | ||
4604 | const iString * url = linkUrl_GmDocument(doc, linkId); | ||
4605 | const int flags = linkFlags; | ||
4606 | iUrl parts; | ||
4607 | init_Url(&parts, url); | ||
4608 | fg = linkColor_GmDocument(doc, linkId, textHover_GmLinkPart); | ||
4609 | const enum iGmLinkScheme scheme = scheme_GmLinkFlag(flags); | ||
4610 | const iBool showHost = (flags & humanReadable_GmLinkFlag && | ||
4611 | (!isEmpty_Range(&parts.host) || | ||
4612 | scheme == mailto_GmLinkScheme)); | ||
4613 | const iBool showImage = (flags & imageFileExtension_GmLinkFlag) != 0; | ||
4614 | const iBool showAudio = (flags & audioFileExtension_GmLinkFlag) != 0; | ||
4615 | iString str; | ||
4616 | init_String(&str); | ||
4617 | /* Show scheme and host. */ | ||
4618 | if (run->flags & endOfLine_GmRunFlag && | ||
4619 | (flags & (imageFileExtension_GmLinkFlag | audioFileExtension_GmLinkFlag) || | ||
4620 | showHost)) { | ||
4621 | format_String( | ||
4622 | &str, | ||
4623 | "%s%s%s%s%s", | ||
4624 | showHost ? "" : "", | ||
4625 | showHost | ||
4626 | ? (scheme == mailto_GmLinkScheme ? cstr_String(url) | ||
4627 | : scheme != gemini_GmLinkScheme ? format_CStr("%s://%s", | ||
4628 | cstr_Rangecc(parts.scheme), | ||
4629 | cstr_Rangecc(parts.host)) | ||
4630 | : cstr_Rangecc(parts.host)) | ||
4631 | : "", | ||
4632 | showHost && (showImage || showAudio) ? " \u2014" : "", | ||
4633 | showImage || showAudio | ||
4634 | ? escape_Color(fg) | ||
4635 | : escape_Color(linkColor_GmDocument(doc, run->linkId, domain_GmLinkPart)), | ||
4636 | showImage || showAudio | ||
4637 | ? format_CStr(showImage ? " %s " photo_Icon : " %s \U0001f3b5", | ||
4638 | cstr_Lang(showImage ? "link.hint.image" : "link.hint.audio")) | ||
4639 | : ""); | ||
4640 | } | ||
4641 | if (run->flags & endOfLine_GmRunFlag && flags & visited_GmLinkFlag) { | ||
4642 | iDate date; | ||
4643 | init_Date(&date, linkTime_GmDocument(doc, run->linkId)); | ||
4644 | appendCStr_String(&str, " \u2014 "); | ||
4645 | appendCStr_String( | ||
4646 | &str, escape_Color(linkColor_GmDocument(doc, run->linkId, visited_GmLinkPart))); | ||
4647 | iString *dateStr = format_Date(&date, "%b %d"); | ||
4648 | append_String(&str, dateStr); | ||
4649 | delete_String(dateStr); | ||
4650 | } | ||
4651 | if (!isEmpty_String(&str)) { | ||
4652 | if (run->isRTL) { | ||
4653 | appendCStr_String(&str, " \u2014 "); | ||
4654 | } | ||
4655 | else { | ||
4656 | prependCStr_String(&str, " \u2014 "); | ||
4657 | } | ||
4658 | const iInt2 textSize = measure_Text(metaFont, cstr_String(&str)).bounds.size; | ||
4659 | int tx = topRight_Rect(linkRect).x; | ||
4660 | const char *msg = cstr_String(&str); | ||
4661 | if (run->isRTL) { | ||
4662 | tx = topLeft_Rect(linkRect).x - textSize.x; | ||
4663 | } | ||
4664 | if (tx + textSize.x > right_Rect(d->widgetBounds)) { | ||
4665 | tx = right_Rect(d->widgetBounds) - textSize.x; | ||
4666 | fillRect_Paint(&d->paint, (iRect){ init_I2(tx, top_Rect(linkRect)), textSize }, | ||
4667 | uiBackground_ColorId); | ||
4668 | msg += 4; /* skip the space and dash */ | ||
4669 | tx += measure_Text(metaFont, " \u2014").advance.x / 2; | ||
4670 | } | ||
4671 | drawAlign_Text(metaFont, | ||
4672 | init_I2(tx, top_Rect(linkRect)), | ||
4673 | linkColor_GmDocument(doc, run->linkId, domain_GmLinkPart), | ||
4674 | left_Alignment, | ||
4675 | "%s", | ||
4676 | msg); | ||
4677 | deinit_String(&str); | ||
4678 | } | ||
4679 | } | ||
4680 | } | ||
4681 | if (0) { | ||
4682 | drawRect_Paint(&d->paint, (iRect){ visPos, run->bounds.size }, green_ColorId); | ||
4683 | drawRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, red_ColorId); | ||
4684 | } | ||
4685 | } | ||
4686 | |||
4687 | static int drawSideRect_(iPaint *p, iRect rect) { | ||
4688 | int bg = tmBannerBackground_ColorId; | ||
4689 | int fg = tmBannerIcon_ColorId; | ||
4690 | if (equal_Color(get_Color(bg), get_Color(tmBackground_ColorId))) { | ||
4691 | bg = tmBannerIcon_ColorId; | ||
4692 | fg = tmBannerBackground_ColorId; | ||
4693 | } | ||
4694 | fillRect_Paint(p, rect, bg); | ||
4695 | return fg; | ||
4696 | } | ||
4697 | |||
4698 | static int sideElementAvailWidth_DocumentWidget_(const iDocumentWidget *d) { | ||
4699 | return left_Rect(documentBounds_DocumentWidget_(d)) - | ||
4700 | left_Rect(bounds_Widget(constAs_Widget(d))) - 2 * d->pageMargin * gap_UI; | ||
4701 | } | ||
4702 | |||
4703 | static iBool isSideHeadingVisible_DocumentWidget_(const iDocumentWidget *d) { | ||
4704 | return sideElementAvailWidth_DocumentWidget_(d) >= lineHeight_Text(banner_FontId) * 4.5f; | ||
4705 | } | ||
4706 | |||
4707 | static void updateSideIconBuf_DocumentWidget_(const iDocumentWidget *d) { | ||
4708 | if (!isExposed_Window(get_Window())) { | ||
4709 | return; | ||
4710 | } | ||
4711 | iDrawBufs *dbuf = d->drawBufs; | ||
4712 | dbuf->flags &= ~updateSideBuf_DrawBufsFlag; | ||
4713 | if (dbuf->sideIconBuf) { | ||
4714 | SDL_DestroyTexture(dbuf->sideIconBuf); | ||
4715 | dbuf->sideIconBuf = NULL; | ||
4716 | } | ||
4717 | // const iGmRun *banner = siteBanner_GmDocument(d->doc); | ||
4718 | if (isEmpty_Banner(d->banner)) { | ||
4719 | return; | ||
4720 | } | ||
4721 | const int margin = gap_UI * d->pageMargin; | ||
4722 | const int minBannerSize = lineHeight_Text(banner_FontId) * 2; | ||
4723 | const iChar icon = siteIcon_GmDocument(d->doc); | ||
4724 | const int avail = sideElementAvailWidth_DocumentWidget_(d) - margin; | ||
4725 | iBool isHeadingVisible = isSideHeadingVisible_DocumentWidget_(d); | ||
4726 | /* Determine the required size. */ | ||
4727 | iInt2 bufSize = init1_I2(minBannerSize); | ||
4728 | const int sideHeadingFont = FONT_ID(documentHeading_FontId, regular_FontStyle, contentBig_FontSize); | ||
4729 | if (isHeadingVisible) { | ||
4730 | const iInt2 headingSize = measureWrapRange_Text(sideHeadingFont, avail, | ||
4731 | currentHeading_DocumentWidget_(d)).bounds.size; | ||
4732 | if (headingSize.x > 0) { | ||
4733 | bufSize.y += gap_Text + headingSize.y; | ||
4734 | bufSize.x = iMax(bufSize.x, headingSize.x); | ||
4735 | } | ||
4736 | else { | ||
4737 | isHeadingVisible = iFalse; | ||
4738 | } | ||
4739 | } | ||
4740 | SDL_Renderer *render = renderer_Window(get_Window()); | ||
4741 | dbuf->sideIconBuf = SDL_CreateTexture(render, | ||
4742 | SDL_PIXELFORMAT_RGBA4444, | ||
4743 | SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET, | ||
4744 | bufSize.x, bufSize.y); | ||
4745 | iPaint p; | ||
4746 | init_Paint(&p); | ||
4747 | beginTarget_Paint(&p, dbuf->sideIconBuf); | ||
4748 | const iColor back = get_Color(tmBannerSideTitle_ColorId); | ||
4749 | SDL_SetRenderDrawColor(render, back.r, back.g, back.b, 0); /* better blending of the edge */ | ||
4750 | SDL_RenderClear(render); | ||
4751 | const iRect iconRect = { zero_I2(), init1_I2(minBannerSize) }; | ||
4752 | int fg = drawSideRect_(&p, iconRect); | ||
4753 | iString str; | ||
4754 | initUnicodeN_String(&str, &icon, 1); | ||
4755 | drawCentered_Text(banner_FontId, iconRect, iTrue, fg, "%s", cstr_String(&str)); | ||
4756 | deinit_String(&str); | ||
4757 | if (isHeadingVisible) { | ||
4758 | iRangecc text = currentHeading_DocumentWidget_(d); | ||
4759 | iInt2 pos = addY_I2(bottomLeft_Rect(iconRect), gap_Text); | ||
4760 | const int font = sideHeadingFont; | ||
4761 | drawWrapRange_Text(font, pos, avail, tmBannerSideTitle_ColorId, text); | ||
4762 | } | ||
4763 | endTarget_Paint(&p); | ||
4764 | SDL_SetTextureBlendMode(dbuf->sideIconBuf, SDL_BLENDMODE_BLEND); | ||
4765 | } | ||
4766 | |||
4767 | static void drawSideElements_DocumentWidget_(const iDocumentWidget *d) { | ||
4768 | const iWidget *w = constAs_Widget(d); | ||
4769 | const iRect bounds = bounds_Widget(w); | ||
4770 | const iRect docBounds = documentBounds_DocumentWidget_(d); | ||
4771 | const int margin = gap_UI * d->pageMargin; | ||
4772 | float opacity = value_Anim(&d->sideOpacity); | ||
4773 | const int avail = left_Rect(docBounds) - left_Rect(bounds) - 2 * margin; | ||
4774 | iDrawBufs * dbuf = d->drawBufs; | ||
4775 | iPaint p; | ||
4776 | init_Paint(&p); | ||
4777 | setClip_Paint(&p, boundsWithoutVisualOffset_Widget(w)); | ||
4778 | /* Side icon and current heading. */ | ||
4779 | if (prefs_App()->sideIcon && opacity > 0 && dbuf->sideIconBuf) { | ||
4780 | const iInt2 texSize = size_SDLTexture(dbuf->sideIconBuf); | ||
4781 | if (avail > texSize.x) { | ||
4782 | const int minBannerSize = lineHeight_Text(banner_FontId) * 2; | ||
4783 | iInt2 pos = addY_I2(add_I2(topLeft_Rect(bounds), init_I2(margin, 0)), | ||
4784 | height_Rect(bounds) / 2 - minBannerSize / 2 - | ||
4785 | (texSize.y > minBannerSize | ||
4786 | ? (gap_Text + lineHeight_Text(heading3_FontId)) / 2 | ||
4787 | : 0)); | ||
4788 | SDL_SetTextureAlphaMod(dbuf->sideIconBuf, 255 * opacity); | ||
4789 | SDL_RenderCopy(renderer_Window(get_Window()), | ||
4790 | dbuf->sideIconBuf, NULL, | ||
4791 | &(SDL_Rect){ pos.x, pos.y, texSize.x, texSize.y }); | ||
4792 | } | ||
4793 | } | ||
4794 | /* Reception timestamp. */ | ||
4795 | if (dbuf->timestampBuf && dbuf->timestampBuf->size.x <= avail) { | ||
4796 | draw_TextBuf( | ||
4797 | dbuf->timestampBuf, | ||
4798 | add_I2( | ||
4799 | bottomLeft_Rect(bounds), | ||
4800 | init_I2(margin, | ||
4801 | -margin + -dbuf->timestampBuf->size.y + | ||
4802 | iMax(0, d->scrollY.max - pos_SmoothScroll(&d->scrollY)))), | ||
4803 | tmQuoteIcon_ColorId); | ||
4804 | } | ||
4805 | unsetClip_Paint(&p); | ||
4806 | } | ||
4807 | |||
4808 | static void drawMedia_DocumentWidget_(const iDocumentWidget *d, iPaint *p) { | ||
4809 | iConstForEach(PtrArray, i, &d->visibleMedia) { | ||
4810 | const iGmRun * run = i.ptr; | ||
4811 | if (run->mediaType == audio_MediaType) { | ||
4812 | iPlayerUI ui; | ||
4813 | init_PlayerUI(&ui, | ||
4814 | audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run)), | ||
4815 | runRect_DocumentWidget_(d, run)); | ||
4816 | draw_PlayerUI(&ui, p); | ||
4817 | } | ||
4818 | else if (run->mediaType == download_MediaType) { | ||
4819 | iDownloadUI ui; | ||
4820 | init_DownloadUI(&ui, constMedia_GmDocument(d->doc), run->mediaId, | ||
4821 | runRect_DocumentWidget_(d, run)); | ||
4822 | draw_DownloadUI(&ui, p); | ||
4823 | } | ||
4824 | } | ||
4825 | } | ||
4826 | |||
4827 | static void extend_GmRunRange_(iGmRunRange *runs) { | ||
4828 | if (runs->start) { | ||
4829 | runs->start--; | ||
4830 | runs->end++; | ||
4831 | } | 5465 | } |
4832 | } | 5466 | } |
4833 | 5467 | ||
4834 | static iBool render_DocumentWidget_(const iDocumentWidget *d, iDrawContext *ctx, iBool prerenderExtra) { | ||
4835 | iBool didDraw = iFalse; | ||
4836 | const iRect bounds = bounds_Widget(constAs_Widget(d)); | ||
4837 | const iRect ctxWidgetBounds = init_Rect( | ||
4838 | 0, 0, width_Rect(bounds) - constAs_Widget(d->scroll)->rect.size.x, height_Rect(bounds)); | ||
4839 | const iRangei full = { 0, size_GmDocument(d->doc).y }; | ||
4840 | const iRangei vis = ctx->vis; | ||
4841 | iVisBuf *visBuf = d->visBuf; /* will be updated now */ | ||
4842 | d->drawBufs->lastRenderTime = SDL_GetTicks(); | ||
4843 | /* Swap buffers around to have room available both before and after the visible region. */ | ||
4844 | allocVisBuffer_DocumentWidget_(d); | ||
4845 | reposition_VisBuf(visBuf, vis); | ||
4846 | /* Redraw the invalid ranges. */ | ||
4847 | if (~flags_Widget(constAs_Widget(d)) & destroyPending_WidgetFlag) { | ||
4848 | iPaint *p = &ctx->paint; | ||
4849 | init_Paint(p); | ||
4850 | iForIndices(i, visBuf->buffers) { | ||
4851 | iVisBufTexture *buf = &visBuf->buffers[i]; | ||
4852 | iVisBufMeta *meta = buf->user; | ||
4853 | const iRangei bufRange = intersect_Rangei(bufferRange_VisBuf(visBuf, i), full); | ||
4854 | const iRangei bufVisRange = intersect_Rangei(bufRange, vis); | ||
4855 | ctx->widgetBounds = moved_Rect(ctxWidgetBounds, init_I2(0, -buf->origin)); | ||
4856 | ctx->viewPos = init_I2(left_Rect(ctx->docBounds) - left_Rect(bounds), -buf->origin); | ||
4857 | // printf(" buffer %zu: buf vis range %d...%d\n", i, bufVisRange.start, bufVisRange.end); | ||
4858 | if (!prerenderExtra && !isEmpty_Range(&bufVisRange)) { | ||
4859 | didDraw = iTrue; | ||
4860 | if (isEmpty_Rangei(buf->validRange)) { | ||
4861 | /* Fill the required currently visible range (vis). */ | ||
4862 | const iRangei bufVisRange = intersect_Rangei(bufRange, vis); | ||
4863 | if (!isEmpty_Range(&bufVisRange)) { | ||
4864 | beginTarget_Paint(p, buf->texture); | ||
4865 | fillRect_Paint(p, (iRect){ zero_I2(), visBuf->texSize }, tmBackground_ColorId); | ||
4866 | iZap(ctx->runsDrawn); | ||
4867 | render_GmDocument(d->doc, bufVisRange, drawRun_DrawContext_, ctx); | ||
4868 | meta->runsDrawn = ctx->runsDrawn; | ||
4869 | extend_GmRunRange_(&meta->runsDrawn); | ||
4870 | buf->validRange = bufVisRange; | ||
4871 | // printf(" buffer %zu valid %d...%d\n", i, bufRange.start, bufRange.end); | ||
4872 | } | ||
4873 | } | ||
4874 | else { | ||
4875 | /* Progressively fill the required runs. */ | ||
4876 | if (meta->runsDrawn.start) { | ||
4877 | beginTarget_Paint(p, buf->texture); | ||
4878 | meta->runsDrawn.start = renderProgressive_GmDocument(d->doc, meta->runsDrawn.start, | ||
4879 | -1, iInvalidSize, | ||
4880 | bufVisRange, | ||
4881 | drawRun_DrawContext_, | ||
4882 | ctx); | ||
4883 | buf->validRange.start = bufVisRange.start; | ||
4884 | } | ||
4885 | if (meta->runsDrawn.end) { | ||
4886 | beginTarget_Paint(p, buf->texture); | ||
4887 | meta->runsDrawn.end = renderProgressive_GmDocument(d->doc, meta->runsDrawn.end, | ||
4888 | +1, iInvalidSize, | ||
4889 | bufVisRange, | ||
4890 | drawRun_DrawContext_, | ||
4891 | ctx); | ||
4892 | buf->validRange.end = bufVisRange.end; | ||
4893 | } | ||
4894 | } | ||
4895 | } | ||
4896 | /* Progressively draw the rest of the buffer if it isn't fully valid. */ | ||
4897 | if (prerenderExtra && !equal_Rangei(bufRange, buf->validRange)) { | ||
4898 | const iGmRun *next; | ||
4899 | // printf("%zu: prerenderExtra (start:%p end:%p)\n", i, meta->runsDrawn.start, meta->runsDrawn.end); | ||
4900 | if (meta->runsDrawn.start == NULL) { | ||
4901 | /* Haven't drawn anything yet in this buffer, so let's try seeding it. */ | ||
4902 | const int rh = lineHeight_Text(paragraph_FontId); | ||
4903 | const int y = i >= iElemCount(visBuf->buffers) / 2 ? bufRange.start : (bufRange.end - rh); | ||
4904 | beginTarget_Paint(p, buf->texture); | ||
4905 | fillRect_Paint(p, (iRect){ zero_I2(), visBuf->texSize }, tmBackground_ColorId); | ||
4906 | buf->validRange = (iRangei){ y, y + rh }; | ||
4907 | iZap(ctx->runsDrawn); | ||
4908 | render_GmDocument(d->doc, buf->validRange, drawRun_DrawContext_, ctx); | ||
4909 | meta->runsDrawn = ctx->runsDrawn; | ||
4910 | extend_GmRunRange_(&meta->runsDrawn); | ||
4911 | // printf("%zu: seeded, next %p:%p\n", i, meta->runsDrawn.start, meta->runsDrawn.end); | ||
4912 | didDraw = iTrue; | ||
4913 | } | ||
4914 | else { | ||
4915 | if (meta->runsDrawn.start) { | ||
4916 | const iRangei upper = intersect_Rangei(bufRange, (iRangei){ full.start, buf->validRange.start }); | ||
4917 | if (upper.end > upper.start) { | ||
4918 | beginTarget_Paint(p, buf->texture); | ||
4919 | next = renderProgressive_GmDocument(d->doc, meta->runsDrawn.start, | ||
4920 | -1, 1, upper, | ||
4921 | drawRun_DrawContext_, | ||
4922 | ctx); | ||
4923 | if (next && meta->runsDrawn.start != next) { | ||
4924 | meta->runsDrawn.start = next; | ||
4925 | buf->validRange.start = bottom_Rect(next->visBounds); | ||
4926 | didDraw = iTrue; | ||
4927 | } | ||
4928 | else { | ||
4929 | buf->validRange.start = bufRange.start; | ||
4930 | } | ||
4931 | } | ||
4932 | } | ||
4933 | if (!didDraw && meta->runsDrawn.end) { | ||
4934 | const iRangei lower = intersect_Rangei(bufRange, (iRangei){ buf->validRange.end, full.end }); | ||
4935 | if (lower.end > lower.start) { | ||
4936 | beginTarget_Paint(p, buf->texture); | ||
4937 | next = renderProgressive_GmDocument(d->doc, meta->runsDrawn.end, | ||
4938 | +1, 1, lower, | ||
4939 | drawRun_DrawContext_, | ||
4940 | ctx); | ||
4941 | if (next && meta->runsDrawn.end != next) { | ||
4942 | meta->runsDrawn.end = next; | ||
4943 | buf->validRange.end = top_Rect(next->visBounds); | ||
4944 | didDraw = iTrue; | ||
4945 | } | ||
4946 | else { | ||
4947 | buf->validRange.end = bufRange.end; | ||
4948 | } | ||
4949 | } | ||
4950 | } | ||
4951 | } | ||
4952 | } | ||
4953 | /* Draw any invalidated runs that fall within this buffer. */ | ||
4954 | if (!prerenderExtra) { | ||
4955 | const iRangei bufRange = { buf->origin, buf->origin + visBuf->texSize.y }; | ||
4956 | /* Clear full-width backgrounds first in case there are any dynamic elements. */ { | ||
4957 | iConstForEach(PtrSet, r, d->invalidRuns) { | ||
4958 | const iGmRun *run = *r.value; | ||
4959 | if (isOverlapping_Rangei(bufRange, ySpan_Rect(run->visBounds))) { | ||
4960 | beginTarget_Paint(p, buf->texture); | ||
4961 | fillRect_Paint(p, | ||
4962 | init_Rect(0, | ||
4963 | run->visBounds.pos.y - buf->origin, | ||
4964 | visBuf->texSize.x, | ||
4965 | run->visBounds.size.y), | ||
4966 | tmBackground_ColorId); | ||
4967 | } | ||
4968 | } | ||
4969 | } | ||
4970 | setAnsiFlags_Text(ansiEscapes_GmDocument(d->doc)); | ||
4971 | iConstForEach(PtrSet, r, d->invalidRuns) { | ||
4972 | const iGmRun *run = *r.value; | ||
4973 | if (isOverlapping_Rangei(bufRange, ySpan_Rect(run->visBounds))) { | ||
4974 | beginTarget_Paint(p, buf->texture); | ||
4975 | drawRun_DrawContext_(ctx, run); | ||
4976 | } | ||
4977 | } | ||
4978 | setAnsiFlags_Text(allowAll_AnsiFlag); | ||
4979 | } | ||
4980 | endTarget_Paint(p); | ||
4981 | if (prerenderExtra && didDraw) { | ||
4982 | /* Just a run at a time. */ | ||
4983 | break; | ||
4984 | } | ||
4985 | } | ||
4986 | if (!prerenderExtra) { | ||
4987 | clear_PtrSet(d->invalidRuns); | ||
4988 | } | ||
4989 | } | ||
4990 | return didDraw; | ||
4991 | } | ||
4992 | |||
4993 | static void prerender_DocumentWidget_(iAny *context) { | 5468 | static void prerender_DocumentWidget_(iAny *context) { |
5469 | iAssert(isInstance_Object(context, &Class_DocumentWidget)); | ||
4994 | if (current_Root() == NULL) { | 5470 | if (current_Root() == NULL) { |
4995 | /* The widget has probably been removed from the widget tree, pending destruction. | 5471 | /* The widget has probably been removed from the widget tree, pending destruction. |
4996 | Tickers are not cancelled until the widget is actually destroyed. */ | 5472 | Tickers are not cancelled until the widget is actually destroyed. */ |
@@ -4998,15 +5474,16 @@ static void prerender_DocumentWidget_(iAny *context) { | |||
4998 | } | 5474 | } |
4999 | const iDocumentWidget *d = context; | 5475 | const iDocumentWidget *d = context; |
5000 | iDrawContext ctx = { | 5476 | iDrawContext ctx = { |
5001 | .widget = d, | 5477 | .view = &d->view, |
5002 | .docBounds = documentBounds_DocumentWidget_(d), | 5478 | .docBounds = documentBounds_DocumentView_(&d->view), |
5003 | .vis = visibleRange_DocumentWidget_(d), | 5479 | .vis = visibleRange_DocumentView_(&d->view), |
5004 | .showLinkNumbers = (d->flags & showLinkNumbers_DocumentWidgetFlag) != 0 | 5480 | .showLinkNumbers = (d->flags & showLinkNumbers_DocumentWidgetFlag) != 0 |
5005 | }; | 5481 | }; |
5006 | // printf("%u prerendering\n", SDL_GetTicks()); | 5482 | // printf("%u prerendering\n", SDL_GetTicks()); |
5007 | if (d->visBuf->buffers[0].texture) { | 5483 | if (d->view.visBuf->buffers[0].texture) { |
5008 | if (render_DocumentWidget_(d, &ctx, iTrue /* just fill up progressively */)) { | 5484 | makePaletteGlobal_GmDocument(d->view.doc); |
5009 | /* Something was drawn, should check if there is still more to do. */ | 5485 | if (render_DocumentView_(&d->view, &ctx, iTrue /* just fill up progressively */)) { |
5486 | /* Something was drawn, should check later if there is still more to do. */ | ||
5010 | addTicker_App(prerender_DocumentWidget_, context); | 5487 | addTicker_App(prerender_DocumentWidget_, context); |
5011 | } | 5488 | } |
5012 | } | 5489 | } |
@@ -5020,108 +5497,54 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) { | |||
5020 | if (width_Rect(bounds) <= 0) { | 5497 | if (width_Rect(bounds) <= 0) { |
5021 | return; | 5498 | return; |
5022 | } | 5499 | } |
5023 | /* TODO: Come up with a better palette caching system. | 5500 | checkPendingInvalidation_DocumentWidget_(d); |
5024 | It should be able to recompute cached colors in `History` when the theme has changed. | 5501 | draw_DocumentView_(&d->view); |
5025 | Cache the theme seed in `GmDocument`? */ | 5502 | iPaint p; |
5026 | // makePaletteGlobal_GmDocument(d->doc); | 5503 | init_Paint(&p); |
5027 | if (d->drawBufs->flags & updateTimestampBuf_DrawBufsFlag) { | ||
5028 | updateTimestampBuf_DocumentWidget_(d); | ||
5029 | } | ||
5030 | if (d->drawBufs->flags & updateSideBuf_DrawBufsFlag) { | ||
5031 | updateSideIconBuf_DocumentWidget_(d); | ||
5032 | } | ||
5033 | const iRect docBounds = documentBounds_DocumentWidget_(d); | ||
5034 | const iRangei vis = visibleRange_DocumentWidget_(d); | ||
5035 | iDrawContext ctx = { | ||
5036 | .widget = d, | ||
5037 | .docBounds = docBounds, | ||
5038 | .vis = vis, | ||
5039 | .showLinkNumbers = (d->flags & showLinkNumbers_DocumentWidgetFlag) != 0, | ||
5040 | }; | ||
5041 | init_Paint(&ctx.paint); | ||
5042 | render_DocumentWidget_(d, &ctx, iFalse /* just the mandatory parts */); | ||
5043 | int yTop = docBounds.pos.y + viewPos_DocumentWidget_(d); | ||
5044 | const iBool isDocEmpty = size_GmDocument(d->doc).y == 0; | ||
5045 | const iBool isTouchSelecting = (flags_Widget(w) & touchDrag_WidgetFlag) != 0; | ||
5046 | if (!isDocEmpty || !isEmpty_Banner(d->banner)) { | ||
5047 | const int docBgColor = isDocEmpty ? tmBannerBackground_ColorId : tmBackground_ColorId; | ||
5048 | setClip_Paint(&ctx.paint, clipBounds); | ||
5049 | if (!isDocEmpty) { | ||
5050 | draw_VisBuf(d->visBuf, init_I2(bounds.pos.x, yTop), ySpan_Rect(bounds)); | ||
5051 | } | ||
5052 | /* Text markers. */ | ||
5053 | if (!isEmpty_Range(&d->foundMark) || !isEmpty_Range(&d->selectMark)) { | ||
5054 | SDL_Renderer *render = renderer_Window(get_Window()); | ||
5055 | ctx.firstMarkRect = zero_Rect(); | ||
5056 | ctx.lastMarkRect = zero_Rect(); | ||
5057 | SDL_SetRenderDrawBlendMode(render, | ||
5058 | isDark_ColorTheme(colorTheme_App()) ? SDL_BLENDMODE_ADD | ||
5059 | : SDL_BLENDMODE_BLEND); | ||
5060 | ctx.viewPos = topLeft_Rect(docBounds); | ||
5061 | /* Marker starting outside the visible range? */ | ||
5062 | if (d->visibleRuns.start) { | ||
5063 | if (!isEmpty_Range(&d->selectMark) && | ||
5064 | d->selectMark.start < d->visibleRuns.start->text.start && | ||
5065 | d->selectMark.end > d->visibleRuns.start->text.start) { | ||
5066 | ctx.inSelectMark = iTrue; | ||
5067 | } | ||
5068 | if (isEmpty_Range(&d->foundMark) && | ||
5069 | d->foundMark.start < d->visibleRuns.start->text.start && | ||
5070 | d->foundMark.end > d->visibleRuns.start->text.start) { | ||
5071 | ctx.inFoundMark = iTrue; | ||
5072 | } | ||
5073 | } | ||
5074 | render_GmDocument(d->doc, vis, drawMark_DrawContext_, &ctx); | ||
5075 | SDL_SetRenderDrawBlendMode(render, SDL_BLENDMODE_NONE); | ||
5076 | /* Selection range pins. */ | ||
5077 | if (isTouchSelecting) { | ||
5078 | drawPin_Paint(&ctx.paint, ctx.firstMarkRect, 0, tmQuote_ColorId); | ||
5079 | drawPin_Paint(&ctx.paint, ctx.lastMarkRect, 1, tmQuote_ColorId); | ||
5080 | } | ||
5081 | } | ||
5082 | drawMedia_DocumentWidget_(d, &ctx.paint); | ||
5083 | /* Fill the top and bottom, in case the document is short. */ | ||
5084 | if (yTop > top_Rect(bounds)) { | ||
5085 | fillRect_Paint(&ctx.paint, | ||
5086 | (iRect){ bounds.pos, init_I2(bounds.size.x, yTop - top_Rect(bounds)) }, | ||
5087 | !isEmpty_Banner(d->banner) ? tmBannerBackground_ColorId | ||
5088 | : docBgColor); | ||
5089 | } | ||
5090 | /* Banner. */ | ||
5091 | if (!isDocEmpty || numItems_Banner(d->banner) > 0) { | ||
5092 | /* Fill the part between the banner and the top of the document. */ | ||
5093 | fillRect_Paint(&ctx.paint, | ||
5094 | (iRect){ init_I2(left_Rect(bounds), | ||
5095 | top_Rect(docBounds) + viewPos_DocumentWidget_(d) - | ||
5096 | documentTopPad_DocumentWidget_(d)), | ||
5097 | init_I2(bounds.size.x, documentTopPad_DocumentWidget_(d)) }, | ||
5098 | docBgColor); | ||
5099 | setPos_Banner(d->banner, addY_I2(topLeft_Rect(docBounds), | ||
5100 | -pos_SmoothScroll(&d->scrollY))); | ||
5101 | draw_Banner(d->banner); | ||
5102 | } | ||
5103 | const int yBottom = yTop + size_GmDocument(d->doc).y; | ||
5104 | if (yBottom < bottom_Rect(bounds)) { | ||
5105 | fillRect_Paint(&ctx.paint, | ||
5106 | init_Rect(bounds.pos.x, yBottom, bounds.size.x, bottom_Rect(bounds) - yBottom), | ||
5107 | !isDocEmpty ? docBgColor : tmBannerBackground_ColorId); | ||
5108 | } | ||
5109 | unsetClip_Paint(&ctx.paint); | ||
5110 | drawSideElements_DocumentWidget_(d); | ||
5111 | if (prefs_App()->hoverLink && d->hoverLink) { | ||
5112 | const int font = uiLabel_FontId; | ||
5113 | const iRangecc linkUrl = range_String(linkUrl_GmDocument(d->doc, d->hoverLink->linkId)); | ||
5114 | const iInt2 size = measureRange_Text(font, linkUrl).bounds.size; | ||
5115 | const iRect linkRect = { addY_I2(bottomLeft_Rect(bounds), -size.y), | ||
5116 | addX_I2(size, 2 * gap_UI) }; | ||
5117 | fillRect_Paint(&ctx.paint, linkRect, tmBackground_ColorId); | ||
5118 | drawRange_Text(font, addX_I2(topLeft_Rect(linkRect), gap_UI), tmParagraph_ColorId, linkUrl); | ||
5119 | } | ||
5120 | } | ||
5121 | if (colorTheme_App() == pureWhite_ColorTheme) { | 5504 | if (colorTheme_App() == pureWhite_ColorTheme) { |
5122 | drawHLine_Paint(&ctx.paint, topLeft_Rect(bounds), width_Rect(bounds), uiSeparator_ColorId); | 5505 | drawHLine_Paint(&p, topLeft_Rect(bounds), width_Rect(bounds), uiSeparator_ColorId); |
5123 | } | 5506 | } |
5507 | /* Pull action indicator. */ | ||
5508 | if (deviceType_App() != desktop_AppDeviceType) { | ||
5509 | float pullPos = pullActionPos_SmoothScroll(&d->view.scrollY); | ||
5510 | /* Account for the part where the indicator isn't yet visible. */ | ||
5511 | pullPos = (pullPos - 0.2f) / 0.8f; | ||
5512 | iRect indRect = initCentered_Rect(init_I2(mid_Rect(bounds).x, | ||
5513 | top_Rect(bounds) - 5 * gap_UI - | ||
5514 | pos_SmoothScroll(&d->view.scrollY)), | ||
5515 | init_I2(20 * gap_UI, 2 * gap_UI)); | ||
5516 | setClip_Paint(&p, clipBounds); | ||
5517 | int color = pullPos < 1.0f ? tmBannerItemFrame_ColorId : tmBannerItemText_ColorId; | ||
5518 | drawRect_Paint(&p, indRect, color); | ||
5519 | if (pullPos > 0) { | ||
5520 | shrink_Rect(&indRect, divi_I2(gap2_UI, 2)); | ||
5521 | indRect.size.x *= pullPos; | ||
5522 | fillRect_Paint(&p, indRect, color); | ||
5523 | } | ||
5524 | unsetClip_Paint(&p); | ||
5525 | } | ||
5526 | /* Scroll bar. */ | ||
5124 | drawChildren_Widget(w); | 5527 | drawChildren_Widget(w); |
5528 | /* Information about the hovered link. */ | ||
5529 | if (deviceType_App() == desktop_AppDeviceType && prefs_App()->hoverLink && d->linkInfo) { | ||
5530 | const int pad = 0; /*gap_UI;*/ | ||
5531 | update_LinkInfo(d->linkInfo, | ||
5532 | d->view.doc, | ||
5533 | d->view.hoverLink ? d->view.hoverLink->linkId : 0, | ||
5534 | width_Rect(bounds) - 2 * pad); | ||
5535 | const iInt2 infoSize = size_LinkInfo(d->linkInfo); | ||
5536 | iInt2 infoPos = add_I2(bottomLeft_Rect(bounds), init_I2(pad, -infoSize.y - pad)); | ||
5537 | if (d->view.hoverLink) { | ||
5538 | const iRect runRect = runRect_DocumentView_(&d->view, d->view.hoverLink); | ||
5539 | d->linkInfo->isAltPos = | ||
5540 | (bottom_Rect(runRect) >= infoPos.y - lineHeight_Text(paragraph_FontId)); | ||
5541 | } | ||
5542 | if (d->linkInfo->isAltPos) { | ||
5543 | infoPos.y = top_Rect(bounds) + pad; | ||
5544 | } | ||
5545 | draw_LinkInfo(d->linkInfo, infoPos); | ||
5546 | } | ||
5547 | /* Full-sized download indicator. */ | ||
5125 | if (d->flags & drawDownloadCounter_DocumentWidgetFlag && isRequestOngoing_DocumentWidget(d)) { | 5548 | if (d->flags & drawDownloadCounter_DocumentWidgetFlag && isRequestOngoing_DocumentWidget(d)) { |
5126 | const int font = uiLabelLarge_FontId; | 5549 | const int font = uiLabelLarge_FontId; |
5127 | const iInt2 sevenSegWidth = measureRange_Text(font, range_CStr("\U0001fbf0")).bounds.size; | 5550 | const iInt2 sevenSegWidth = measureRange_Text(font, range_CStr("\U0001fbf0")).bounds.size; |
@@ -5131,62 +5554,26 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) { | |||
5131 | tmQuote_ColorId, tmQuoteIcon_ColorId, | 5554 | tmQuote_ColorId, tmQuoteIcon_ColorId, |
5132 | bodySize_GmRequest(d->request)); | 5555 | bodySize_GmRequest(d->request)); |
5133 | } | 5556 | } |
5134 | /* Alt text. */ | ||
5135 | const float altTextOpacity = value_Anim(&d->altTextOpacity) * 6 - 5; | ||
5136 | if (d->hoverAltPre && altTextOpacity > 0) { | ||
5137 | const iGmPreMeta *meta = preMeta_GmDocument(d->doc, preId_GmRun(d->hoverAltPre)); | ||
5138 | if (meta->flags & topLeft_GmPreMetaFlag && ~meta->flags & decoration_GmRunFlag && | ||
5139 | !isEmpty_Range(&meta->altText)) { | ||
5140 | const int margin = 3 * gap_UI / 2; | ||
5141 | const int altFont = uiLabel_FontId; | ||
5142 | const int wrap = docBounds.size.x - 2 * margin; | ||
5143 | iInt2 pos = addY_I2(add_I2(docBounds.pos, meta->pixelRect.pos), | ||
5144 | viewPos_DocumentWidget_(d)); | ||
5145 | const iInt2 textSize = measureWrapRange_Text(altFont, wrap, meta->altText).bounds.size; | ||
5146 | pos.y -= textSize.y + gap_UI; | ||
5147 | pos.y = iMax(pos.y, top_Rect(bounds)); | ||
5148 | const iRect altRect = { pos, init_I2(docBounds.size.x, textSize.y) }; | ||
5149 | ctx.paint.alpha = altTextOpacity * 255; | ||
5150 | if (altTextOpacity < 1) { | ||
5151 | SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND); | ||
5152 | } | ||
5153 | fillRect_Paint(&ctx.paint, altRect, tmBackgroundAltText_ColorId); | ||
5154 | drawRect_Paint(&ctx.paint, altRect, tmFrameAltText_ColorId); | ||
5155 | setOpacity_Text(altTextOpacity); | ||
5156 | drawWrapRange_Text(altFont, addX_I2(pos, margin), wrap, | ||
5157 | tmQuote_ColorId, meta->altText); | ||
5158 | SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE); | ||
5159 | setOpacity_Text(1.0f); | ||
5160 | } | ||
5161 | } | ||
5162 | /* Pinch zoom indicator. */ | 5557 | /* Pinch zoom indicator. */ |
5163 | if (d->flags & pinchZoom_DocumentWidgetFlag) { | 5558 | if (d->flags & pinchZoom_DocumentWidgetFlag) { |
5164 | const int font = uiLabelLargeBold_FontId; | 5559 | const int font = uiLabelLargeBold_FontId; |
5165 | const int height = lineHeight_Text(font) * 2; | 5560 | const int height = lineHeight_Text(font) * 2; |
5166 | const iInt2 size = init_I2(height * 2, height); | 5561 | const iInt2 size = init_I2(height * 2, height); |
5167 | const iRect rect = { sub_I2(mid_Rect(bounds), divi_I2(size, 2)), size }; | 5562 | const iRect rect = { sub_I2(mid_Rect(bounds), divi_I2(size, 2)), size }; |
5168 | fillRect_Paint(&ctx.paint, rect, d->pinchZoomPosted == 100 ? uiTextCaution_ColorId : uiTextAction_ColorId); | 5563 | fillRect_Paint(&p, rect, d->pinchZoomPosted == 100 ? uiTextCaution_ColorId : uiTextAction_ColorId); |
5169 | drawCentered_Text(font, bounds, iFalse, uiBackground_ColorId, "%d %%", | 5564 | drawCentered_Text(font, bounds, iFalse, uiBackground_ColorId, "%d %%", |
5170 | d->pinchZoomPosted); | 5565 | d->pinchZoomPosted); |
5171 | } | 5566 | } |
5172 | /* Touch selection indicator. */ | 5567 | /* Dimming during swipe animation. */ |
5173 | if (isTouchSelecting) { | ||
5174 | iRect rect = { topLeft_Rect(bounds), | ||
5175 | init_I2(width_Rect(bounds), lineHeight_Text(uiLabelBold_FontId)) }; | ||
5176 | fillRect_Paint(&ctx.paint, rect, uiTextAction_ColorId); | ||
5177 | const iRangecc mark = selectMark_DocumentWidget_(d); | ||
5178 | drawCentered_Text(uiLabelBold_FontId, rect, iFalse, uiBackground_ColorId, "%zu bytes selected", | ||
5179 | size_Range(&mark)); | ||
5180 | } | ||
5181 | if (w->offsetRef) { | 5568 | if (w->offsetRef) { |
5182 | const int offX = visualOffsetByReference_Widget(w); | 5569 | const int offX = visualOffsetByReference_Widget(w); |
5183 | if (offX) { | 5570 | if (offX) { |
5184 | setClip_Paint(&ctx.paint, clipBounds); | 5571 | setClip_Paint(&p, clipBounds); |
5185 | SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND); | 5572 | SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND); |
5186 | ctx.paint.alpha = iAbs(offX) / (float) get_Window()->size.x * 300; | 5573 | p.alpha = iAbs(offX) / (float) get_Window()->size.x * 300; |
5187 | fillRect_Paint(&ctx.paint, bounds, backgroundFadeColor_Widget()); | 5574 | fillRect_Paint(&p, bounds, backgroundFadeColor_Widget()); |
5188 | SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE); | 5575 | SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE); |
5189 | unsetClip_Paint(&ctx.paint); | 5576 | unsetClip_Paint(&p); |
5190 | } | 5577 | } |
5191 | else { | 5578 | else { |
5192 | /* TODO: Should have a better place to do this; drawing is supposed to be immutable. */ | 5579 | /* TODO: Should have a better place to do this; drawing is supposed to be immutable. */ |
@@ -5195,11 +5582,139 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) { | |||
5195 | mut->flags &= ~refChildrenOffset_WidgetFlag; | 5582 | mut->flags &= ~refChildrenOffset_WidgetFlag; |
5196 | } | 5583 | } |
5197 | } | 5584 | } |
5198 | // drawRect_Paint(&ctx.paint, docBounds, red_ColorId); | 5585 | // drawRect_Paint(&p, docBounds, red_ColorId); |
5586 | if (deviceType_App() == phone_AppDeviceType) { | ||
5587 | /* The phone toolbar uses the palette of the active tab, but there may be other | ||
5588 | documents drawn before the toolbar, causing the colors to be incorrect. */ | ||
5589 | makePaletteGlobal_GmDocument(document_App()->view.doc); | ||
5590 | } | ||
5199 | } | 5591 | } |
5200 | 5592 | ||
5201 | /*----------------------------------------------------------------------------------------------*/ | 5593 | /*----------------------------------------------------------------------------------------------*/ |
5202 | 5594 | ||
5595 | void init_DocumentWidget(iDocumentWidget *d) { | ||
5596 | iWidget *w = as_Widget(d); | ||
5597 | init_Widget(w); | ||
5598 | setId_Widget(w, format_CStr("document%03d", ++docEnum_)); | ||
5599 | setFlags_Widget(w, hover_WidgetFlag | noBackground_WidgetFlag, iTrue); | ||
5600 | #if defined (iPlatformAppleDesktop) | ||
5601 | iBool enableSwipeNavigation = iTrue; /* swipes on the trackpad */ | ||
5602 | #else | ||
5603 | iBool enableSwipeNavigation = (deviceType_App() != desktop_AppDeviceType); | ||
5604 | #endif | ||
5605 | if (enableSwipeNavigation) { | ||
5606 | setFlags_Widget(w, leftEdgeDraggable_WidgetFlag | rightEdgeDraggable_WidgetFlag | | ||
5607 | horizontalOffset_WidgetFlag, iTrue); | ||
5608 | } | ||
5609 | init_PersistentDocumentState(&d->mod); | ||
5610 | d->flags = 0; | ||
5611 | d->phoneToolbar = findWidget_App("toolbar"); | ||
5612 | d->footerButtons = NULL; | ||
5613 | iZap(d->certExpiry); | ||
5614 | d->certFingerprint = new_Block(0); | ||
5615 | d->certFlags = 0; | ||
5616 | d->certSubject = new_String(); | ||
5617 | d->state = blank_RequestState; | ||
5618 | d->titleUser = new_String(); | ||
5619 | d->request = NULL; | ||
5620 | d->requestLinkId = 0; | ||
5621 | d->isRequestUpdated = iFalse; | ||
5622 | d->media = new_ObjectList(); | ||
5623 | d->banner = new_Banner(); | ||
5624 | setOwner_Banner(d->banner, d); | ||
5625 | d->redirectCount = 0; | ||
5626 | d->ordinalBase = 0; | ||
5627 | d->wheelSwipeState = none_WheelSwipeState; | ||
5628 | d->selectMark = iNullRange; | ||
5629 | d->foundMark = iNullRange; | ||
5630 | d->contextLink = NULL; | ||
5631 | d->sourceStatus = none_GmStatusCode; | ||
5632 | init_String(&d->sourceHeader); | ||
5633 | init_String(&d->sourceMime); | ||
5634 | init_Block(&d->sourceContent, 0); | ||
5635 | iZap(d->sourceTime); | ||
5636 | d->sourceGempub = NULL; | ||
5637 | d->initNormScrollY = 0; | ||
5638 | d->grabbedPlayer = NULL; | ||
5639 | d->mediaTimer = 0; | ||
5640 | init_String(&d->pendingGotoHeading); | ||
5641 | init_String(&d->linePrecedingLink); | ||
5642 | init_Click(&d->click, d, SDL_BUTTON_LEFT); | ||
5643 | d->linkInfo = (deviceType_App() == desktop_AppDeviceType ? new_LinkInfo() : NULL); | ||
5644 | init_DocumentView(&d->view); | ||
5645 | setOwner_DocumentView_(&d->view, d); | ||
5646 | addChild_Widget(w, iClob(d->scroll = new_ScrollWidget())); | ||
5647 | d->menu = NULL; /* created when clicking */ | ||
5648 | d->playerMenu = NULL; | ||
5649 | d->copyMenu = NULL; | ||
5650 | d->translation = NULL; | ||
5651 | addChildFlags_Widget(w, | ||
5652 | iClob(new_IndicatorWidget()), | ||
5653 | resizeToParentWidth_WidgetFlag | resizeToParentHeight_WidgetFlag); | ||
5654 | #if !defined (iPlatformAppleDesktop) /* in system menu */ | ||
5655 | addAction_Widget(w, reload_KeyShortcut, "navigate.reload"); | ||
5656 | addAction_Widget(w, closeTab_KeyShortcut, "tabs.close"); | ||
5657 | addAction_Widget(w, SDLK_d, KMOD_PRIMARY, "bookmark.add"); | ||
5658 | addAction_Widget(w, subscribeToPage_KeyModifier, "feeds.subscribe"); | ||
5659 | #endif | ||
5660 | addAction_Widget(w, navigateBack_KeyShortcut, "navigate.back"); | ||
5661 | addAction_Widget(w, navigateForward_KeyShortcut, "navigate.forward"); | ||
5662 | addAction_Widget(w, navigateParent_KeyShortcut, "navigate.parent"); | ||
5663 | addAction_Widget(w, navigateRoot_KeyShortcut, "navigate.root"); | ||
5664 | } | ||
5665 | |||
5666 | void cancelAllRequests_DocumentWidget(iDocumentWidget *d) { | ||
5667 | iForEach(ObjectList, i, d->media) { | ||
5668 | iMediaRequest *mr = i.object; | ||
5669 | cancel_GmRequest(mr->req); | ||
5670 | } | ||
5671 | if (d->request) { | ||
5672 | cancel_GmRequest(d->request); | ||
5673 | } | ||
5674 | } | ||
5675 | |||
5676 | void deinit_DocumentWidget(iDocumentWidget *d) { | ||
5677 | // printf("\n* * * * * * * *\nDEINIT DOCUMENT: %s\n* * * * * * * *\n\n", | ||
5678 | // cstr_String(&d->widget.id)); fflush(stdout); | ||
5679 | cancelAllRequests_DocumentWidget(d); | ||
5680 | pauseAllPlayers_Media(media_GmDocument(d->view.doc), iTrue); | ||
5681 | removeTicker_App(animate_DocumentWidget_, d); | ||
5682 | removeTicker_App(prerender_DocumentWidget_, d); | ||
5683 | remove_Periodic(periodic_App(), d); | ||
5684 | delete_Translation(d->translation); | ||
5685 | deinit_DocumentView(&d->view); | ||
5686 | delete_LinkInfo(d->linkInfo); | ||
5687 | iRelease(d->media); | ||
5688 | iRelease(d->request); | ||
5689 | delete_Gempub(d->sourceGempub); | ||
5690 | deinit_String(&d->linePrecedingLink); | ||
5691 | deinit_String(&d->pendingGotoHeading); | ||
5692 | deinit_Block(&d->sourceContent); | ||
5693 | deinit_String(&d->sourceMime); | ||
5694 | deinit_String(&d->sourceHeader); | ||
5695 | delete_Banner(d->banner); | ||
5696 | if (d->mediaTimer) { | ||
5697 | SDL_RemoveTimer(d->mediaTimer); | ||
5698 | } | ||
5699 | delete_Block(d->certFingerprint); | ||
5700 | delete_String(d->certSubject); | ||
5701 | delete_String(d->titleUser); | ||
5702 | deinit_PersistentDocumentState(&d->mod); | ||
5703 | } | ||
5704 | |||
5705 | void setSource_DocumentWidget(iDocumentWidget *d, const iString *source) { | ||
5706 | setUrl_GmDocument(d->view.doc, d->mod.url); | ||
5707 | const int docWidth = documentWidth_DocumentView_(&d->view); | ||
5708 | setSource_GmDocument(d->view.doc, | ||
5709 | source, | ||
5710 | docWidth, | ||
5711 | width_Widget(d), | ||
5712 | isFinished_GmRequest(d->request) ? final_GmDocumentUpdate | ||
5713 | : partial_GmDocumentUpdate); | ||
5714 | setWidth_Banner(d->banner, docWidth); | ||
5715 | documentWasChanged_DocumentWidget_(d); | ||
5716 | } | ||
5717 | |||
5203 | iHistory *history_DocumentWidget(iDocumentWidget *d) { | 5718 | iHistory *history_DocumentWidget(iDocumentWidget *d) { |
5204 | return d->mod.history; | 5719 | return d->mod.history; |
5205 | } | 5720 | } |
@@ -5209,7 +5724,7 @@ const iString *url_DocumentWidget(const iDocumentWidget *d) { | |||
5209 | } | 5724 | } |
5210 | 5725 | ||
5211 | const iGmDocument *document_DocumentWidget(const iDocumentWidget *d) { | 5726 | const iGmDocument *document_DocumentWidget(const iDocumentWidget *d) { |
5212 | return d->doc; | 5727 | return d->view.doc; |
5213 | } | 5728 | } |
5214 | 5729 | ||
5215 | const iBlock *sourceContent_DocumentWidget(const iDocumentWidget *d) { | 5730 | const iBlock *sourceContent_DocumentWidget(const iDocumentWidget *d) { |
@@ -5217,20 +5732,20 @@ const iBlock *sourceContent_DocumentWidget(const iDocumentWidget *d) { | |||
5217 | } | 5732 | } |
5218 | 5733 | ||
5219 | int documentWidth_DocumentWidget(const iDocumentWidget *d) { | 5734 | int documentWidth_DocumentWidget(const iDocumentWidget *d) { |
5220 | return documentWidth_DocumentWidget_(d); | 5735 | return documentWidth_DocumentView_(&d->view); |
5221 | } | 5736 | } |
5222 | 5737 | ||
5223 | const iString *feedTitle_DocumentWidget(const iDocumentWidget *d) { | 5738 | const iString *feedTitle_DocumentWidget(const iDocumentWidget *d) { |
5224 | if (!isEmpty_String(title_GmDocument(d->doc))) { | 5739 | if (!isEmpty_String(title_GmDocument(d->view.doc))) { |
5225 | return title_GmDocument(d->doc); | 5740 | return title_GmDocument(d->view.doc); |
5226 | } | 5741 | } |
5227 | return bookmarkTitle_DocumentWidget(d); | 5742 | return bookmarkTitle_DocumentWidget(d); |
5228 | } | 5743 | } |
5229 | 5744 | ||
5230 | const iString *bookmarkTitle_DocumentWidget(const iDocumentWidget *d) { | 5745 | const iString *bookmarkTitle_DocumentWidget(const iDocumentWidget *d) { |
5231 | iStringArray *title = iClob(new_StringArray()); | 5746 | iStringArray *title = iClob(new_StringArray()); |
5232 | if (!isEmpty_String(title_GmDocument(d->doc))) { | 5747 | if (!isEmpty_String(title_GmDocument(d->view.doc))) { |
5233 | pushBack_StringArray(title, title_GmDocument(d->doc)); | 5748 | pushBack_StringArray(title, title_GmDocument(d->view.doc)); |
5234 | } | 5749 | } |
5235 | if (!isEmpty_String(d->titleUser)) { | 5750 | if (!isEmpty_String(d->titleUser)) { |
5236 | pushBack_StringArray(title, d->titleUser); | 5751 | pushBack_StringArray(title, d->titleUser); |
@@ -5258,30 +5773,19 @@ void deserializeState_DocumentWidget(iDocumentWidget *d, iStream *ins) { | |||
5258 | updateFromHistory_DocumentWidget_(d); | 5773 | updateFromHistory_DocumentWidget_(d); |
5259 | } | 5774 | } |
5260 | 5775 | ||
5261 | static void setUrl_DocumentWidget_(iDocumentWidget *d, const iString *url) { | ||
5262 | url = canonicalUrl_String(url); | ||
5263 | if (!equal_String(d->mod.url, url)) { | ||
5264 | d->flags |= urlChanged_DocumentWidgetFlag; | ||
5265 | set_String(d->mod.url, url); | ||
5266 | } | ||
5267 | } | ||
5268 | |||
5269 | void setUrlFlags_DocumentWidget(iDocumentWidget *d, const iString *url, int setUrlFlags) { | 5776 | void setUrlFlags_DocumentWidget(iDocumentWidget *d, const iString *url, int setUrlFlags) { |
5270 | iChangeFlags(d->flags, openedFromSidebar_DocumentWidgetFlag, | 5777 | const iBool allowCache = (setUrlFlags & useCachedContentIfAvailable_DocumentWidgetSetUrlFlag) != 0; |
5271 | (setUrlFlags & openedFromSidebar_DocumentWidgetSetUrlFlag) != 0); | ||
5272 | const iBool isFromCache = (setUrlFlags & useCachedContentIfAvailable_DocumentWidgetSetUrlFlag) != 0; | ||
5273 | setLinkNumberMode_DocumentWidget_(d, iFalse); | 5778 | setLinkNumberMode_DocumentWidget_(d, iFalse); |
5274 | setUrl_DocumentWidget_(d, urlFragmentStripped_String(url)); | 5779 | setUrl_DocumentWidget_(d, urlFragmentStripped_String(url)); |
5275 | /* See if there a username in the URL. */ | 5780 | /* See if there a username in the URL. */ |
5276 | parseUser_DocumentWidget_(d); | 5781 | parseUser_DocumentWidget_(d); |
5277 | if (!isFromCache || !updateFromHistory_DocumentWidget_(d)) { | 5782 | if (!allowCache || !updateFromHistory_DocumentWidget_(d)) { |
5278 | fetch_DocumentWidget_(d); | 5783 | fetch_DocumentWidget_(d); |
5279 | } | 5784 | } |
5280 | } | 5785 | } |
5281 | 5786 | ||
5282 | void setUrlAndSource_DocumentWidget(iDocumentWidget *d, const iString *url, const iString *mime, | 5787 | void setUrlAndSource_DocumentWidget(iDocumentWidget *d, const iString *url, const iString *mime, |
5283 | const iBlock *source) { | 5788 | const iBlock *source) { |
5284 | d->flags &= ~openedFromSidebar_DocumentWidgetFlag; | ||
5285 | setLinkNumberMode_DocumentWidget_(d, iFalse); | 5789 | setLinkNumberMode_DocumentWidget_(d, iFalse); |
5286 | setUrl_DocumentWidget_(d, url); | 5790 | setUrl_DocumentWidget_(d, url); |
5287 | parseUser_DocumentWidget_(d); | 5791 | parseUser_DocumentWidget_(d); |
@@ -5291,18 +5795,26 @@ void setUrlAndSource_DocumentWidget(iDocumentWidget *d, const iString *url, cons | |||
5291 | set_String(&resp->meta, mime); | 5795 | set_String(&resp->meta, mime); |
5292 | set_Block(&resp->body, source); | 5796 | set_Block(&resp->body, source); |
5293 | updateFromCachedResponse_DocumentWidget_(d, 0, resp, NULL); | 5797 | updateFromCachedResponse_DocumentWidget_(d, 0, resp, NULL); |
5798 | updateBanner_DocumentWidget_(d); | ||
5294 | delete_GmResponse(resp); | 5799 | delete_GmResponse(resp); |
5295 | } | 5800 | } |
5296 | 5801 | ||
5297 | iDocumentWidget *duplicate_DocumentWidget(const iDocumentWidget *orig) { | 5802 | iDocumentWidget *duplicate_DocumentWidget(const iDocumentWidget *orig) { |
5298 | iDocumentWidget *d = new_DocumentWidget(); | 5803 | iDocumentWidget *d = new_DocumentWidget(); |
5299 | delete_History(d->mod.history); | 5804 | delete_History(d->mod.history); |
5300 | d->initNormScrollY = normScrollPos_DocumentWidget_(d); | 5805 | d->initNormScrollY = normScrollPos_DocumentView_(&d->view); |
5301 | d->mod.history = copy_History(orig->mod.history); | 5806 | d->mod.history = copy_History(orig->mod.history); |
5302 | setUrlFlags_DocumentWidget(d, orig->mod.url, useCachedContentIfAvailable_DocumentWidgetSetUrlFlag); | 5807 | setUrlFlags_DocumentWidget(d, orig->mod.url, useCachedContentIfAvailable_DocumentWidgetSetUrlFlag); |
5303 | return d; | 5808 | return d; |
5304 | } | 5809 | } |
5305 | 5810 | ||
5811 | void setOrigin_DocumentWidget(iDocumentWidget *d, const iDocumentWidget *other) { | ||
5812 | if (d != other) { | ||
5813 | /* TODO: Could remember the other's ID? */ | ||
5814 | set_String(&d->linePrecedingLink, &other->linePrecedingLink); | ||
5815 | } | ||
5816 | } | ||
5817 | |||
5306 | void setUrl_DocumentWidget(iDocumentWidget *d, const iString *url) { | 5818 | void setUrl_DocumentWidget(iDocumentWidget *d, const iString *url) { |
5307 | setUrlFlags_DocumentWidget(d, url, 0); | 5819 | setUrlFlags_DocumentWidget(d, url, 0); |
5308 | } | 5820 | } |
@@ -5315,11 +5827,6 @@ void setRedirectCount_DocumentWidget(iDocumentWidget *d, int count) { | |||
5315 | d->redirectCount = count; | 5827 | d->redirectCount = count; |
5316 | } | 5828 | } |
5317 | 5829 | ||
5318 | void setOpenedFromSidebar_DocumentWidget(iDocumentWidget *d, iBool fromSidebar) { | ||
5319 | iChangeFlags(d->flags, openedFromSidebar_DocumentWidgetFlag, fromSidebar); | ||
5320 | // setCachedDocument_History(d->mod.history, d->doc, fromSidebar); | ||
5321 | } | ||
5322 | |||
5323 | iBool isRequestOngoing_DocumentWidget(const iDocumentWidget *d) { | 5830 | iBool isRequestOngoing_DocumentWidget(const iDocumentWidget *d) { |
5324 | return d->request != NULL; | 5831 | return d->request != NULL; |
5325 | } | 5832 | } |
@@ -5327,7 +5834,7 @@ iBool isRequestOngoing_DocumentWidget(const iDocumentWidget *d) { | |||
5327 | void takeRequest_DocumentWidget(iDocumentWidget *d, iGmRequest *finishedRequest) { | 5834 | void takeRequest_DocumentWidget(iDocumentWidget *d, iGmRequest *finishedRequest) { |
5328 | cancelRequest_DocumentWidget_(d, iFalse /* don't post anything */); | 5835 | cancelRequest_DocumentWidget_(d, iFalse /* don't post anything */); |
5329 | const iString *url = url_GmRequest(finishedRequest); | 5836 | const iString *url = url_GmRequest(finishedRequest); |
5330 | 5837 | ||
5331 | add_History(d->mod.history, url); | 5838 | add_History(d->mod.history, url); |
5332 | setUrl_DocumentWidget_(d, url); | 5839 | setUrl_DocumentWidget_(d, url); |
5333 | d->state = fetching_RequestState; | 5840 | d->state = fetching_RequestState; |
@@ -5341,27 +5848,17 @@ void takeRequest_DocumentWidget(iDocumentWidget *d, iGmRequest *finishedRequest) | |||
5341 | } | 5848 | } |
5342 | 5849 | ||
5343 | void updateSize_DocumentWidget(iDocumentWidget *d) { | 5850 | void updateSize_DocumentWidget(iDocumentWidget *d) { |
5344 | updateDocumentWidthRetainingScrollPosition_DocumentWidget_(d, iFalse); | 5851 | iDocumentView *view = &d->view; |
5345 | resetWideRuns_DocumentWidget_(d); | 5852 | updateDocumentWidthRetainingScrollPosition_DocumentView_(view, iFalse); |
5346 | d->drawBufs->flags |= updateSideBuf_DrawBufsFlag; | 5853 | resetWideRuns_DocumentView_(view); |
5347 | updateVisible_DocumentWidget_(d); | 5854 | view->drawBufs->flags |= updateSideBuf_DrawBufsFlag; |
5348 | setWidth_Banner(d->banner, documentWidth_DocumentWidget(d)); | 5855 | updateVisible_DocumentView_(view); |
5856 | setWidth_Banner(d->banner, documentWidth_DocumentView_(view)); | ||
5349 | invalidate_DocumentWidget_(d); | 5857 | invalidate_DocumentWidget_(d); |
5350 | arrange_Widget(d->footerButtons); | 5858 | arrange_Widget(d->footerButtons); |
5351 | } | 5859 | } |
5352 | 5860 | ||
5353 | #if 0 | ||
5354 | static void sizeChanged_DocumentWidget_(iDocumentWidget *d) { | ||
5355 | if (current_Root()) { | ||
5356 | /* TODO: This gets called more than once during a single arrange. | ||
5357 | It could be done via some sort of callback instead. */ | ||
5358 | updateVisible_DocumentWidget_(d); | ||
5359 | } | ||
5360 | } | ||
5361 | #endif | ||
5362 | |||
5363 | iBeginDefineSubclass(DocumentWidget, Widget) | 5861 | iBeginDefineSubclass(DocumentWidget, Widget) |
5364 | .processEvent = (iAny *) processEvent_DocumentWidget_, | 5862 | .processEvent = (iAny *) processEvent_DocumentWidget_, |
5365 | .draw = (iAny *) draw_DocumentWidget_, | 5863 | .draw = (iAny *) draw_DocumentWidget_, |
5366 | // .sizeChanged = (iAny *) sizeChanged_DocumentWidget_, | ||
5367 | iEndDefineSubclass(DocumentWidget) | 5864 | iEndDefineSubclass(DocumentWidget) |
diff --git a/src/ui/documentwidget.h b/src/ui/documentwidget.h index 2df3392b..1bee8351 100644 --- a/src/ui/documentwidget.h +++ b/src/ui/documentwidget.h | |||
@@ -48,21 +48,17 @@ const iString * bookmarkTitle_DocumentWidget (const iDocumentWidget *); | |||
48 | const iString * feedTitle_DocumentWidget (const iDocumentWidget *); | 48 | const iString * feedTitle_DocumentWidget (const iDocumentWidget *); |
49 | int documentWidth_DocumentWidget (const iDocumentWidget *); | 49 | int documentWidth_DocumentWidget (const iDocumentWidget *); |
50 | 50 | ||
51 | //iBool findCachedContent_DocumentWidget(const iDocumentWidget *, const iString *url, | ||
52 | // iString *mime_out, iBlock *data_out); | ||
53 | |||
54 | enum iDocumentWidgetSetUrlFlags { | 51 | enum iDocumentWidgetSetUrlFlags { |
55 | useCachedContentIfAvailable_DocumentWidgetSetUrlFlag = iBit(1), | 52 | useCachedContentIfAvailable_DocumentWidgetSetUrlFlag = iBit(1), |
56 | openedFromSidebar_DocumentWidgetSetUrlFlag = iBit(2), | ||
57 | }; | 53 | }; |
58 | 54 | ||
55 | void setOrigin_DocumentWidget (iDocumentWidget *, const iDocumentWidget *other); | ||
59 | void setUrl_DocumentWidget (iDocumentWidget *, const iString *url); | 56 | void setUrl_DocumentWidget (iDocumentWidget *, const iString *url); |
60 | void setUrlFlags_DocumentWidget (iDocumentWidget *, const iString *url, int setUrlFlags); | 57 | void setUrlFlags_DocumentWidget (iDocumentWidget *, const iString *url, int setUrlFlags); |
61 | void setUrlAndSource_DocumentWidget (iDocumentWidget *, const iString *url, const iString *mime, const iBlock *source); | 58 | void setUrlAndSource_DocumentWidget (iDocumentWidget *, const iString *url, const iString *mime, const iBlock *source); |
62 | void setInitialScroll_DocumentWidget (iDocumentWidget *, float normScrollY); /* set after content received */ | 59 | void setInitialScroll_DocumentWidget (iDocumentWidget *, float normScrollY); /* set after content received */ |
63 | void setRedirectCount_DocumentWidget (iDocumentWidget *, int count); | 60 | void setRedirectCount_DocumentWidget (iDocumentWidget *, int count); |
64 | void setSource_DocumentWidget (iDocumentWidget *, const iString *sourceText); | 61 | void setSource_DocumentWidget (iDocumentWidget *, const iString *sourceText); |
65 | void setOpenedFromSidebar_DocumentWidget(iDocumentWidget *, iBool fromSidebar); | ||
66 | 62 | ||
67 | void takeRequest_DocumentWidget (iDocumentWidget *, iGmRequest *finishedRequest); /* ownership given */ | 63 | void takeRequest_DocumentWidget (iDocumentWidget *, iGmRequest *finishedRequest); /* ownership given */ |
68 | 64 | ||
diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index a1fb8cb5..9261da0c 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 | |||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | 20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | 21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ |
22 | 22 | ||
23 | /* InputWidget supports both fully custom and system-provided text editing. | ||
24 | The primary source of complexity is the handling of wrapped text content | ||
25 | in the custom text editor. */ | ||
26 | |||
23 | #include "inputwidget.h" | 27 | #include "inputwidget.h" |
24 | #include "command.h" | 28 | #include "command.h" |
25 | #include "paint.h" | 29 | #include "paint.h" |
@@ -40,10 +44,23 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
40 | # include "macos.h" | 44 | # include "macos.h" |
41 | #endif | 45 | #endif |
42 | 46 | ||
47 | #if defined (iPlatformAppleMobile) | ||
48 | # include "ios.h" | ||
49 | # define LAGRANGE_USE_SYSTEM_TEXT_INPUT 1 /* System-provided UI control almost handles everything. */ | ||
50 | #else | ||
51 | # define LAGRANGE_USE_SYSTEM_TEXT_INPUT 0 | ||
52 | iDeclareType(SystemTextInput) | ||
53 | #endif | ||
54 | |||
43 | static const int refreshInterval_InputWidget_ = 512; | 55 | static const int refreshInterval_InputWidget_ = 512; |
44 | static const size_t maxUndo_InputWidget_ = 64; | 56 | static const size_t maxUndo_InputWidget_ = 64; |
45 | static const int unlimitedWidth_InputWidget_ = 1000000; /* TODO: WrapText disables some functionality if maxWidth==0 */ | 57 | static const int unlimitedWidth_InputWidget_ = 1000000; /* TODO: WrapText disables some functionality if maxWidth==0 */ |
46 | 58 | ||
59 | static const iChar sensitiveChar_ = 0x25cf; /* black circle */ | ||
60 | static const char * sensitive_ = "\u25cf"; | ||
61 | |||
62 | #define minWidth_InputWidget_ (3 * gap_UI) | ||
63 | |||
47 | static void enableEditorKeysInMenus_(iBool enable) { | 64 | static void enableEditorKeysInMenus_(iBool enable) { |
48 | #if defined (iPlatformAppleDesktop) | 65 | #if defined (iPlatformAppleDesktop) |
49 | enableMenuItemsByKey_MacOS(SDLK_LEFT, KMOD_PRIMARY, enable); | 66 | enableMenuItemsByKey_MacOS(SDLK_LEFT, KMOD_PRIMARY, enable); |
@@ -57,7 +74,10 @@ static void enableEditorKeysInMenus_(iBool enable) { | |||
57 | #endif | 74 | #endif |
58 | } | 75 | } |
59 | 76 | ||
77 | static void updateMetrics_InputWidget_(iInputWidget *); | ||
78 | |||
60 | /*----------------------------------------------------------------------------------------------*/ | 79 | /*----------------------------------------------------------------------------------------------*/ |
80 | #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT | ||
61 | 81 | ||
62 | iDeclareType(InputLine) | 82 | iDeclareType(InputLine) |
63 | 83 | ||
@@ -178,6 +198,8 @@ static void deinit_InputUndo_(iInputUndo *d) { | |||
178 | deinit_String(&d->text); | 198 | deinit_String(&d->text); |
179 | } | 199 | } |
180 | 200 | ||
201 | #endif /* USE_SYSTEM_TEXT_INPUT */ | ||
202 | |||
181 | enum iInputWidgetFlag { | 203 | enum iInputWidgetFlag { |
182 | isSensitive_InputWidgetFlag = iBit(1), | 204 | isSensitive_InputWidgetFlag = iBit(1), |
183 | isUrl_InputWidgetFlag = iBit(2), /* affected by decoding preference */ | 205 | isUrl_InputWidgetFlag = iBit(2), /* affected by decoding preference */ |
@@ -203,41 +225,54 @@ enum iInputWidgetFlag { | |||
203 | struct Impl_InputWidget { | 225 | struct Impl_InputWidget { |
204 | iWidget widget; | 226 | iWidget widget; |
205 | enum iInputMode mode; | 227 | enum iInputMode mode; |
228 | int font; | ||
206 | int inFlags; | 229 | int inFlags; |
207 | size_t maxLen; /* characters */ | 230 | size_t maxLen; /* characters */ |
208 | iArray lines; /* iInputLine[] */ | ||
209 | iString oldText; /* for restoring if edits cancelled */ | ||
210 | int lastUpdateWidth; | ||
211 | iString srcHint; | 231 | iString srcHint; |
212 | iString hint; | 232 | iString hint; |
213 | int leftPadding; | 233 | int leftPadding; /* additional padding between frame and content */ |
214 | int rightPadding; | 234 | int rightPadding; |
235 | int minWrapLines, maxWrapLines; /* min/max number of visible lines allowed */ | ||
236 | iRangei visWrapLines; /* which wrap lines are current visible */ | ||
237 | iClick click; | ||
238 | int wheelAccum; | ||
239 | iTextBuf * buffered; /* pre-rendered static text */ | ||
240 | iInputWidgetValidatorFunc validator; | ||
241 | void * validatorContext; | ||
242 | iString * backupPath; | ||
243 | int backupTimer; | ||
244 | iString oldText; /* for restoring if edits cancelled */ | ||
245 | int lastUpdateWidth; | ||
246 | uint32_t lastOverflowScrollTime; /* scrolling to show focused widget */ | ||
247 | iSystemTextInput *sysCtrl; | ||
248 | #if LAGRANGE_USE_SYSTEM_TEXT_INPUT | ||
249 | iString text; | ||
250 | #else | ||
251 | iArray lines; /* iInputLine[] */ | ||
215 | iInt2 cursor; /* cursor position: x = byte offset, y = line index */ | 252 | iInt2 cursor; /* cursor position: x = byte offset, y = line index */ |
216 | iInt2 prevCursor; /* previous cursor position */ | 253 | iInt2 prevCursor; /* previous cursor position */ |
217 | iRangei visWrapLines; /* which wrap lines are current visible */ | ||
218 | int minWrapLines, maxWrapLines; /* min/max number of visible lines allowed */ | ||
219 | iRanges mark; /* TODO: would likely simplify things to use two Int2's for marking; no conversions needed */ | 254 | iRanges mark; /* TODO: would likely simplify things to use two Int2's for marking; no conversions needed */ |
220 | iRanges initialMark; | 255 | iRanges initialMark; |
221 | iArray undoStack; | 256 | iArray undoStack; |
222 | int font; | ||
223 | iClick click; | ||
224 | uint32_t tapStartTime; | 257 | uint32_t tapStartTime; |
225 | uint32_t lastTapTime; | 258 | uint32_t lastTapTime; |
226 | iInt2 lastTapPos; | 259 | iInt2 lastTapPos; |
227 | int tapCount; | 260 | int tapCount; |
228 | int wheelAccum; | ||
229 | int cursorVis; | 261 | int cursorVis; |
230 | uint32_t timer; | 262 | uint32_t timer; |
231 | iTextBuf * buffered; /* pre-rendered static text */ | 263 | #endif |
232 | iInputWidgetValidatorFunc validator; | ||
233 | void * validatorContext; | ||
234 | iString * backupPath; | ||
235 | int backupTimer; | ||
236 | }; | 264 | }; |
237 | 265 | ||
238 | iDefineObjectConstructionArgs(InputWidget, (size_t maxLen), maxLen) | 266 | iDefineObjectConstructionArgs(InputWidget, (size_t maxLen), maxLen) |
239 | 267 | ||
240 | static void updateMetrics_InputWidget_(iInputWidget *); | 268 | static int extraPaddingHeight_InputWidget_(const iInputWidget *d) { |
269 | if ((isPortraitPhone_App() || deviceType_App() == tablet_AppDeviceType) && | ||
270 | !cmp_String(id_Widget(&d->widget), "url")) { | ||
271 | /* Make the tap target more generous. */ | ||
272 | return 2.5f * gap_UI; | ||
273 | } | ||
274 | return 1.25f * gap_UI; | ||
275 | } | ||
241 | 276 | ||
242 | static void restoreBackup_InputWidget_(iInputWidget *d) { | 277 | static void restoreBackup_InputWidget_(iInputWidget *d) { |
243 | if (!d->backupPath) return; | 278 | if (!d->backupPath) return; |
@@ -252,17 +287,21 @@ static void saveBackup_InputWidget_(iInputWidget *d) { | |||
252 | if (!d->backupPath) return; | 287 | if (!d->backupPath) return; |
253 | iFile *f = new_File(d->backupPath); | 288 | iFile *f = new_File(d->backupPath); |
254 | if (open_File(f, writeOnly_FileMode | text_FileMode)) { | 289 | if (open_File(f, writeOnly_FileMode | text_FileMode)) { |
290 | #if LAGRANGE_USE_SYSTEM_TEXT_INPUT | ||
291 | write_File(f, utf8_String(&d->text)); | ||
292 | #else | ||
255 | iConstForEach(Array, i, &d->lines) { | 293 | iConstForEach(Array, i, &d->lines) { |
256 | const iInputLine *line = i.value; | 294 | const iInputLine *line = i.value; |
257 | write_File(f, utf8_String(&line->text)); | 295 | write_File(f, utf8_String(&line->text)); |
258 | } | 296 | } |
259 | d->inFlags &= ~needBackup_InputWidgetFlag; | 297 | # if !defined (NDEBUG) |
260 | #if !defined (NDEBUG) | ||
261 | iConstForEach(Array, j, &d->lines) { | 298 | iConstForEach(Array, j, &d->lines) { |
262 | iAssert(endsWith_String(&((const iInputLine *) j.value)->text, "\n") || | 299 | iAssert(endsWith_String(&((const iInputLine *) j.value)->text, "\n") || |
263 | index_ArrayConstIterator(&j) == size_Array(&d->lines) - 1); | 300 | index_ArrayConstIterator(&j) == size_Array(&d->lines) - 1); |
264 | } | 301 | } |
302 | # endif | ||
265 | #endif | 303 | #endif |
304 | d->inFlags &= ~needBackup_InputWidgetFlag; | ||
266 | } | 305 | } |
267 | iRelease(f); | 306 | iRelease(f); |
268 | } | 307 | } |
@@ -311,6 +350,12 @@ void setBackupFileName_InputWidget(iInputWidget *d, const char *fileName) { | |||
311 | restoreBackup_InputWidget_(d); | 350 | restoreBackup_InputWidget_(d); |
312 | } | 351 | } |
313 | 352 | ||
353 | iLocalDef iInt2 padding_(void) { | ||
354 | return init_I2(gap_UI / 2, gap_UI / 2); | ||
355 | } | ||
356 | |||
357 | #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT | ||
358 | |||
314 | static void clearUndo_InputWidget_(iInputWidget *d) { | 359 | static void clearUndo_InputWidget_(iInputWidget *d) { |
315 | iForEach(Array, i, &d->undoStack) { | 360 | iForEach(Array, i, &d->undoStack) { |
316 | deinit_InputUndo_(i.value); | 361 | deinit_InputUndo_(i.value); |
@@ -318,11 +363,12 @@ static void clearUndo_InputWidget_(iInputWidget *d) { | |||
318 | clear_Array(&d->undoStack); | 363 | clear_Array(&d->undoStack); |
319 | } | 364 | } |
320 | 365 | ||
321 | iLocalDef iInt2 padding_(void) { | 366 | static const iInputLine *line_InputWidget_(const iInputWidget *d, size_t index) { |
322 | return init_I2(gap_UI / 2, gap_UI / 2); | 367 | iAssert(!isEmpty_Array(&d->lines)); |
368 | return constAt_Array(&d->lines, index); | ||
323 | } | 369 | } |
324 | 370 | ||
325 | #define extraPaddingHeight_ (1.25f * gap_UI) | 371 | #endif /* !LAGRANGE_USE_SYSTEM_TEXT_INPUT */ |
326 | 372 | ||
327 | static iRect contentBounds_InputWidget_(const iInputWidget *d) { | 373 | static iRect contentBounds_InputWidget_(const iInputWidget *d) { |
328 | const iWidget *w = constAs_Widget(d); | 374 | const iWidget *w = constAs_Widget(d); |
@@ -332,11 +378,39 @@ static iRect contentBounds_InputWidget_(const iInputWidget *d) { | |||
332 | shrink_Rect(&bounds, init_I2(gap_UI * (flags_Widget(w) & tight_WidgetFlag ? 1 : 2), 0)); | 378 | shrink_Rect(&bounds, init_I2(gap_UI * (flags_Widget(w) & tight_WidgetFlag ? 1 : 2), 0)); |
333 | bounds.pos.y += padding_().y / 2; | 379 | bounds.pos.y += padding_().y / 2; |
334 | if (flags_Widget(w) & extraPadding_WidgetFlag) { | 380 | if (flags_Widget(w) & extraPadding_WidgetFlag) { |
335 | bounds.pos.y += extraPaddingHeight_ / 2; | 381 | if (d->sysCtrl && !cmp_String(id_Widget(w), "url")) { |
382 | /* TODO: This is super hacky: the native UI control would be offset incorrectly. | ||
383 | These paddings/offsets are getting a bit ridiculous, should rethink the whole thing. | ||
384 | Use the Widget paddings! */ | ||
385 | bounds.pos.y += 1.25f * gap_UI / 2; | ||
386 | } | ||
387 | else { | ||
388 | bounds.pos.y += extraPaddingHeight_InputWidget_(d) / 2; | ||
389 | } | ||
336 | } | 390 | } |
337 | return bounds; | 391 | return bounds; |
338 | } | 392 | } |
339 | 393 | ||
394 | static iWrapText wrap_InputWidget_(const iInputWidget *d, int y) { | ||
395 | #if LAGRANGE_USE_SYSTEM_TEXT_INPUT | ||
396 | iUnused(y); /* full text is wrapped always */ | ||
397 | iRangecc text = range_String(&d->text); | ||
398 | #else | ||
399 | iRangecc text = range_String(&line_InputWidget_(d, y)->text); | ||
400 | #endif | ||
401 | return (iWrapText){ | ||
402 | .text = text, | ||
403 | .maxWidth = d->maxLen == 0 ? iMaxi(minWidth_InputWidget_, | ||
404 | width_Rect(contentBounds_InputWidget_(d))) | ||
405 | : unlimitedWidth_InputWidget_, | ||
406 | .mode = | ||
407 | (d->inFlags & isUrl_InputWidgetFlag ? anyCharacter_WrapTextMode : word_WrapTextMode), | ||
408 | .overrideChar = (d->inFlags & isSensitive_InputWidgetFlag ? sensitiveChar_ : 0), | ||
409 | }; | ||
410 | } | ||
411 | |||
412 | #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT | ||
413 | |||
340 | iLocalDef iBool isLastLine_InputWidget_(const iInputWidget *d, const iInputLine *line) { | 414 | iLocalDef iBool isLastLine_InputWidget_(const iInputWidget *d, const iInputLine *line) { |
341 | return (const void *) line == constBack_Array(&d->lines); | 415 | return (const void *) line == constBack_Array(&d->lines); |
342 | } | 416 | } |
@@ -350,11 +424,6 @@ static int numWrapLines_InputWidget_(const iInputWidget *d) { | |||
350 | return lastLine_InputWidget_(d)->wrapLines.end; | 424 | return lastLine_InputWidget_(d)->wrapLines.end; |
351 | } | 425 | } |
352 | 426 | ||
353 | static const iInputLine *line_InputWidget_(const iInputWidget *d, size_t index) { | ||
354 | iAssert(!isEmpty_Array(&d->lines)); | ||
355 | return constAt_Array(&d->lines, index); | ||
356 | } | ||
357 | |||
358 | static const iString *lineString_InputWidget_(const iInputWidget *d, int y) { | 427 | static const iString *lineString_InputWidget_(const iInputWidget *d, int y) { |
359 | return &line_InputWidget_(d, y)->text; | 428 | return &line_InputWidget_(d, y)->text; |
360 | } | 429 | } |
@@ -455,20 +524,6 @@ static int visLineOffsetY_InputWidget_(const iInputWidget *d) { | |||
455 | d->wheelAccum; | 524 | d->wheelAccum; |
456 | } | 525 | } |
457 | 526 | ||
458 | static const iChar sensitiveChar_ = 0x25cf; /* black circle */ | ||
459 | static const char *sensitive_ = "\u25cf"; | ||
460 | |||
461 | static iWrapText wrap_InputWidget_(const iInputWidget *d, int y) { | ||
462 | return (iWrapText){ | ||
463 | .text = range_String(&line_InputWidget_(d, y)->text), | ||
464 | .maxWidth = d->maxLen == 0 ? width_Rect(contentBounds_InputWidget_(d)) | ||
465 | : unlimitedWidth_InputWidget_, | ||
466 | .mode = | ||
467 | (d->inFlags & isUrl_InputWidgetFlag ? anyCharacter_WrapTextMode : word_WrapTextMode), | ||
468 | .overrideChar = (d->inFlags & isSensitive_InputWidgetFlag ? sensitiveChar_ : 0), | ||
469 | }; | ||
470 | } | ||
471 | |||
472 | static iRangei visibleLineRange_InputWidget_(const iInputWidget *d) { | 527 | static iRangei visibleLineRange_InputWidget_(const iInputWidget *d) { |
473 | iRangei vis = { -1, -1 }; | 528 | iRangei vis = { -1, -1 }; |
474 | /* Determine which lines are in the potentially visible range. */ | 529 | /* Determine which lines are in the potentially visible range. */ |
@@ -518,6 +573,9 @@ static iInt2 relativeCursorCoord_InputWidget_(const iInputWidget *d) { | |||
518 | } | 573 | } |
519 | 574 | ||
520 | static void updateVisible_InputWidget_(iInputWidget *d) { | 575 | static void updateVisible_InputWidget_(iInputWidget *d) { |
576 | if (width_Widget(d) == 0) { | ||
577 | return; /* Nothing to do yet. */ | ||
578 | } | ||
521 | const int totalWraps = numWrapLines_InputWidget_(d); | 579 | const int totalWraps = numWrapLines_InputWidget_(d); |
522 | const int visWraps = iClamp(totalWraps, d->minWrapLines, d->maxWrapLines); | 580 | const int visWraps = iClamp(totalWraps, d->minWrapLines, d->maxWrapLines); |
523 | /* Resize the height of the editor. */ | 581 | /* Resize the height of the editor. */ |
@@ -535,15 +593,24 @@ static void updateVisible_InputWidget_(iInputWidget *d) { | |||
535 | else if (cursorY < d->visWrapLines.start) { | 593 | else if (cursorY < d->visWrapLines.start) { |
536 | delta = cursorY - d->visWrapLines.start; | 594 | delta = cursorY - d->visWrapLines.start; |
537 | } | 595 | } |
596 | if (d->visWrapLines.end + delta > totalWraps) { | ||
597 | /* Don't scroll past the bottom. */ | ||
598 | delta = totalWraps - d->visWrapLines.end; | ||
599 | } | ||
600 | if (d->visWrapLines.start + delta < 0) { | ||
601 | /* Don't ever scroll above the top. */ | ||
602 | delta = -d->visWrapLines.start; | ||
603 | } | ||
538 | d->visWrapLines.start += delta; | 604 | d->visWrapLines.start += delta; |
539 | d->visWrapLines.end += delta; | 605 | d->visWrapLines.end += delta; |
540 | iAssert(contains_Range(&d->visWrapLines, cursorY)); | 606 | // iAssert(contains_Range(&d->visWrapLines, cursorY)); |
541 | if (!isFocused_Widget(d) && d->maxWrapLines == 1) { | 607 | if (!isFocused_Widget(d) && d->maxWrapLines == 1) { |
542 | d->visWrapLines.start = 0; | 608 | d->visWrapLines.start = 0; |
543 | d->visWrapLines.end = 1; | 609 | d->visWrapLines.end = 1; |
544 | } | 610 | } |
545 | // printf("[InputWidget %p] total:%d viswrp:%d cur:%d vis:%d..%d\n", | 611 | // printf("[InputWidget %p] total:%d viswrp:%d cur:%d vis:%d..%d\n", |
546 | // d, totalWraps, visWraps, d->cursor.y, d->visWrapLines.start, d->visWrapLines.end); | 612 | // d, totalWraps, visWraps, d->cursor.y, d->visWrapLines.start, d->visWrapLines.end); |
613 | // fflush(stdout); | ||
547 | } | 614 | } |
548 | 615 | ||
549 | static void showCursor_InputWidget_(iInputWidget *d) { | 616 | static void showCursor_InputWidget_(iInputWidget *d) { |
@@ -551,6 +618,19 @@ static void showCursor_InputWidget_(iInputWidget *d) { | |||
551 | updateVisible_InputWidget_(d); | 618 | updateVisible_InputWidget_(d); |
552 | } | 619 | } |
553 | 620 | ||
621 | #else /* if LAGRANGE_USE_SYSTEM_TEXT_INPUT */ | ||
622 | |||
623 | static int visLineOffsetY_InputWidget_(const iInputWidget *d) { | ||
624 | return 0; /* offset for the buffered text */ | ||
625 | } | ||
626 | |||
627 | static void updateVisible_InputWidget_(iInputWidget *d) { | ||
628 | iUnused(d); | ||
629 | /* TODO: Anything to do? */ | ||
630 | } | ||
631 | |||
632 | #endif | ||
633 | |||
554 | static void invalidateBuffered_InputWidget_(iInputWidget *d) { | 634 | static void invalidateBuffered_InputWidget_(iInputWidget *d) { |
555 | if (d->buffered) { | 635 | if (d->buffered) { |
556 | delete_TextBuf(d->buffered); | 636 | delete_TextBuf(d->buffered); |
@@ -563,7 +643,7 @@ static void updateSizeForFixedLength_InputWidget_(iInputWidget *d) { | |||
563 | /* Set a fixed size based on maximum possible width of the text. */ | 643 | /* Set a fixed size based on maximum possible width of the text. */ |
564 | iBlock *content = new_Block(d->maxLen); | 644 | iBlock *content = new_Block(d->maxLen); |
565 | fill_Block(content, 'M'); | 645 | fill_Block(content, 'M'); |
566 | int extraHeight = (flags_Widget(as_Widget(d)) & extraPadding_WidgetFlag ? extraPaddingHeight_ : 0); | 646 | int extraHeight = (flags_Widget(as_Widget(d)) & extraPadding_WidgetFlag ? extraPaddingHeight_InputWidget_(d) : 0); |
567 | setFixedSize_Widget( | 647 | setFixedSize_Widget( |
568 | as_Widget(d), | 648 | as_Widget(d), |
569 | add_I2(measure_Text(d->font, cstr_Block(content)).bounds.size, | 649 | add_I2(measure_Text(d->font, cstr_Block(content)).bounds.size, |
@@ -574,11 +654,16 @@ static void updateSizeForFixedLength_InputWidget_(iInputWidget *d) { | |||
574 | } | 654 | } |
575 | 655 | ||
576 | static iString *text_InputWidget_(const iInputWidget *d) { | 656 | static iString *text_InputWidget_(const iInputWidget *d) { |
657 | #if LAGRANGE_USE_SYSTEM_TEXT_INPUT | ||
658 | return copy_String(&d->text); | ||
659 | #else | ||
577 | iString *text = new_String(); | 660 | iString *text = new_String(); |
578 | mergeLines_(&d->lines, text); | 661 | mergeLines_(&d->lines, text); |
579 | return text; | 662 | return text; |
663 | #endif | ||
580 | } | 664 | } |
581 | 665 | ||
666 | #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT | ||
582 | static size_t length_InputWidget_(const iInputWidget *d) { | 667 | static size_t length_InputWidget_(const iInputWidget *d) { |
583 | /* Note: `d->length` is kept up to date, so don't call this normally. */ | 668 | /* Note: `d->length` is kept up to date, so don't call this normally. */ |
584 | size_t len = 0; | 669 | size_t len = 0; |
@@ -589,37 +674,10 @@ static size_t length_InputWidget_(const iInputWidget *d) { | |||
589 | return len; | 674 | return len; |
590 | } | 675 | } |
591 | 676 | ||
592 | static int contentHeight_InputWidget_(const iInputWidget *d) { | ||
593 | return size_Range(&d->visWrapLines) * lineHeight_Text(d->font); | ||
594 | } | ||
595 | |||
596 | static void updateTextInputRect_InputWidget_(const iInputWidget *d) { | ||
597 | #if !defined (iPlatformAppleMobile) | ||
598 | const iRect bounds = bounds_Widget(constAs_Widget(d)); | ||
599 | SDL_SetTextInputRect(&(SDL_Rect){ bounds.pos.x, bounds.pos.y, bounds.size.x, bounds.size.y }); | ||
600 | #endif | ||
601 | } | ||
602 | |||
603 | static void updateMetrics_InputWidget_(iInputWidget *d) { | ||
604 | iWidget *w = as_Widget(d); | ||
605 | updateSizeForFixedLength_InputWidget_(d); | ||
606 | /* Caller must arrange the width, but the height is set here. */ | ||
607 | const int oldHeight = height_Rect(w->rect); | ||
608 | w->rect.size.y = contentHeight_InputWidget_(d) + 3.0f * padding_().y; /* TODO: Why 3x? */ | ||
609 | if (flags_Widget(w) & extraPadding_WidgetFlag) { | ||
610 | w->rect.size.y += extraPaddingHeight_; | ||
611 | } | ||
612 | invalidateBuffered_InputWidget_(d); | ||
613 | if (height_Rect(w->rect) != oldHeight) { | ||
614 | postCommand_Widget(d, "input.resized"); | ||
615 | updateTextInputRect_InputWidget_(d); | ||
616 | } | ||
617 | } | ||
618 | |||
619 | static void updateLine_InputWidget_(iInputWidget *d, iInputLine *line) { | 677 | static void updateLine_InputWidget_(iInputWidget *d, iInputLine *line) { |
620 | iAssert(endsWith_String(&line->text, "\n") || isLastLine_InputWidget_(d, line)); | 678 | iAssert(endsWith_String(&line->text, "\n") || isLastLine_InputWidget_(d, line)); |
621 | iWrapText wrapText = wrap_InputWidget_(d, indexOf_Array(&d->lines, line)); | 679 | iWrapText wrapText = wrap_InputWidget_(d, indexOf_Array(&d->lines, line)); |
622 | if (wrapText.maxWidth <= 0) { | 680 | if (wrapText.maxWidth <= minWidth_InputWidget_) { |
623 | line->wrapLines.end = line->wrapLines.start + 1; | 681 | line->wrapLines.end = line->wrapLines.start + 1; |
624 | return; | 682 | return; |
625 | } | 683 | } |
@@ -668,7 +726,10 @@ static uint32_t cursorTimer_(uint32_t interval, void *w) { | |||
668 | return interval; | 726 | return interval; |
669 | } | 727 | } |
670 | 728 | ||
671 | static void startOrStopCursorTimer_InputWidget_(iInputWidget *d, iBool doStart) { | 729 | static void startOrStopCursorTimer_InputWidget_(iInputWidget *d, int doStart) { |
730 | if (!prefs_App()->blinkingCursor && doStart == 1) { | ||
731 | doStart = iFalse; | ||
732 | } | ||
672 | if (doStart && !d->timer) { | 733 | if (doStart && !d->timer) { |
673 | d->timer = SDL_AddTimer(refreshInterval_InputWidget_, cursorTimer_, d); | 734 | d->timer = SDL_AddTimer(refreshInterval_InputWidget_, cursorTimer_, d); |
674 | } | 735 | } |
@@ -678,6 +739,66 @@ static void startOrStopCursorTimer_InputWidget_(iInputWidget *d, iBool doStart) | |||
678 | } | 739 | } |
679 | } | 740 | } |
680 | 741 | ||
742 | #else /* using a system-provided text control */ | ||
743 | |||
744 | static void updateAllLinesAndResizeHeight_InputWidget_(iInputWidget *d) { | ||
745 | /* Rewrap the buffered text and resize accordingly. */ | ||
746 | iWrapText wt = wrap_InputWidget_(d, 0); | ||
747 | /* TODO: Set max lines limit for WrapText. */ | ||
748 | const int height = measure_WrapText(&wt, d->font).bounds.size.y; | ||
749 | /* We use this to store the number wrapped lines for determining widget height. */ | ||
750 | d->visWrapLines.start = 0; | ||
751 | d->visWrapLines.end = iMax(d->minWrapLines, | ||
752 | iMin(d->maxWrapLines, height / lineHeight_Text(d->font))); | ||
753 | updateMetrics_InputWidget_(d); | ||
754 | } | ||
755 | |||
756 | #endif | ||
757 | |||
758 | static int contentHeight_InputWidget_(const iInputWidget *d) { | ||
759 | const int lineHeight = lineHeight_Text(d->font); | ||
760 | #if LAGRANGE_USE_SYSTEM_TEXT_INPUT | ||
761 | const int minHeight = d->minWrapLines * lineHeight; | ||
762 | const int maxHeight = d->maxWrapLines * lineHeight; | ||
763 | if (d->sysCtrl) { | ||
764 | const int preferred = (preferredHeight_SystemTextInput(d->sysCtrl) + gap_UI) / lineHeight; | ||
765 | return iClamp(preferred * lineHeight, minHeight, maxHeight); | ||
766 | } | ||
767 | if (d->buffered && ~d->inFlags & needUpdateBuffer_InputWidgetFlag) { | ||
768 | return iClamp(d->buffered->size.y, minHeight, maxHeight); | ||
769 | } | ||
770 | #endif | ||
771 | return (int) size_Range(&d->visWrapLines) * lineHeight; | ||
772 | } | ||
773 | |||
774 | static void updateTextInputRect_InputWidget_(const iInputWidget *d) { | ||
775 | #if LAGRANGE_USE_SYSTEM_TEXT_INPUT | ||
776 | if (d->sysCtrl) { | ||
777 | setRect_SystemTextInput(d->sysCtrl, contentBounds_InputWidget_(d)); | ||
778 | } | ||
779 | #endif | ||
780 | #if !defined (iPlatformAppleMobile) | ||
781 | const iRect bounds = bounds_Widget(constAs_Widget(d)); | ||
782 | SDL_SetTextInputRect(&(SDL_Rect){ bounds.pos.x, bounds.pos.y, bounds.size.x, bounds.size.y }); | ||
783 | #endif | ||
784 | } | ||
785 | |||
786 | static void updateMetrics_InputWidget_(iInputWidget *d) { | ||
787 | iWidget *w = as_Widget(d); | ||
788 | updateSizeForFixedLength_InputWidget_(d); | ||
789 | /* Caller must arrange the width, but the height is set here. */ | ||
790 | const int oldHeight = height_Rect(w->rect); | ||
791 | w->rect.size.y = contentHeight_InputWidget_(d) + 3.0f * padding_().y; /* TODO: Why 3x? */ | ||
792 | if (flags_Widget(w) & extraPadding_WidgetFlag) { | ||
793 | w->rect.size.y += extraPaddingHeight_InputWidget_(d); | ||
794 | } | ||
795 | invalidateBuffered_InputWidget_(d); | ||
796 | if (height_Rect(w->rect) != oldHeight) { | ||
797 | postCommand_Widget(d, "input.resized"); | ||
798 | updateTextInputRect_InputWidget_(d); | ||
799 | } | ||
800 | } | ||
801 | |||
681 | void init_InputWidget(iInputWidget *d, size_t maxLen) { | 802 | void init_InputWidget(iInputWidget *d, size_t maxLen) { |
682 | iWidget *w = &d->widget; | 803 | iWidget *w = &d->widget; |
683 | init_Widget(w); | 804 | init_Widget(w); |
@@ -687,40 +808,44 @@ void init_InputWidget(iInputWidget *d, size_t maxLen) { | |||
687 | #if defined (iPlatformMobile) | 808 | #if defined (iPlatformMobile) |
688 | setFlags_Widget(w, extraPadding_WidgetFlag, iTrue); | 809 | setFlags_Widget(w, extraPadding_WidgetFlag, iTrue); |
689 | #endif | 810 | #endif |
811 | #if LAGRANGE_USE_SYSTEM_TEXT_INPUT | ||
812 | init_String(&d->text); | ||
813 | #else | ||
690 | init_Array(&d->lines, sizeof(iInputLine)); | 814 | init_Array(&d->lines, sizeof(iInputLine)); |
815 | init_Array(&d->undoStack, sizeof(iInputUndo)); | ||
816 | d->cursor = zero_I2(); | ||
817 | d->prevCursor = zero_I2(); | ||
818 | d->lastTapTime = 0; | ||
819 | d->tapCount = 0; | ||
820 | d->timer = 0; | ||
821 | d->cursorVis = 0; | ||
822 | iZap(d->mark); | ||
823 | splitToLines_(&iStringLiteral(""), &d->lines); | ||
824 | #endif | ||
691 | init_String(&d->oldText); | 825 | init_String(&d->oldText); |
692 | init_Array(&d->lines, sizeof(iInputLine)); | ||
693 | init_String(&d->srcHint); | 826 | init_String(&d->srcHint); |
694 | init_String(&d->hint); | 827 | init_String(&d->hint); |
695 | init_Array(&d->undoStack, sizeof(iInputUndo)); | ||
696 | d->font = uiInput_FontId | alwaysVariableFlag_FontId; | 828 | d->font = uiInput_FontId | alwaysVariableFlag_FontId; |
697 | d->leftPadding = 0; | 829 | d->leftPadding = 0; |
698 | d->rightPadding = 0; | 830 | d->rightPadding = 0; |
699 | d->cursor = zero_I2(); | ||
700 | d->prevCursor = zero_I2(); | ||
701 | d->lastUpdateWidth = 0; | 831 | d->lastUpdateWidth = 0; |
702 | d->inFlags = eatEscape_InputWidgetFlag | enterKeyEnabled_InputWidgetFlag | | 832 | d->inFlags = eatEscape_InputWidgetFlag | enterKeyEnabled_InputWidgetFlag | |
703 | lineBreaksEnabled_InputWidgetFlag | useReturnKeyBehavior_InputWidgetFlag; | 833 | lineBreaksEnabled_InputWidgetFlag | useReturnKeyBehavior_InputWidgetFlag; |
704 | // if (deviceType_App() != desktop_AppDeviceType) { | 834 | // if (deviceType_App() != desktop_AppDeviceType) { |
705 | // d->inFlags |= enterKeyInsertsLineFeed_InputWidgetFlag; | 835 | // d->inFlags |= enterKeyInsertsLineFeed_InputWidgetFlag; |
706 | // } | 836 | // } |
707 | iZap(d->mark); | ||
708 | setMaxLen_InputWidget(d, maxLen); | 837 | setMaxLen_InputWidget(d, maxLen); |
709 | d->visWrapLines.start = 0; | 838 | d->visWrapLines.start = 0; |
710 | d->visWrapLines.end = 1; | 839 | d->visWrapLines.end = 1; |
711 | d->maxWrapLines = maxLen > 0 ? 1 : 20; /* TODO: Choose maximum dynamically? */ | 840 | d->maxWrapLines = maxLen > 0 ? 1 : 20; /* TODO: Choose maximum dynamically? */ |
712 | d->minWrapLines = 1; | 841 | d->minWrapLines = 1; |
713 | splitToLines_(&iStringLiteral(""), &d->lines); | ||
714 | setFlags_Widget(w, fixedHeight_WidgetFlag, iTrue); /* resizes its own height */ | 842 | setFlags_Widget(w, fixedHeight_WidgetFlag, iTrue); /* resizes its own height */ |
715 | init_Click(&d->click, d, SDL_BUTTON_LEFT); | 843 | init_Click(&d->click, d, SDL_BUTTON_LEFT); |
716 | d->lastTapTime = 0; | ||
717 | d->tapCount = 0; | ||
718 | d->wheelAccum = 0; | 844 | d->wheelAccum = 0; |
719 | d->timer = 0; | ||
720 | d->cursorVis = 0; | ||
721 | d->buffered = NULL; | 845 | d->buffered = NULL; |
722 | d->backupPath = NULL; | 846 | d->backupPath = NULL; |
723 | d->backupTimer = 0; | 847 | d->backupTimer = 0; |
848 | d->sysCtrl = NULL; | ||
724 | updateMetrics_InputWidget_(d); | 849 | updateMetrics_InputWidget_(d); |
725 | } | 850 | } |
726 | 851 | ||
@@ -733,26 +858,48 @@ void deinit_InputWidget(iInputWidget *d) { | |||
733 | } | 858 | } |
734 | delete_String(d->backupPath); | 859 | delete_String(d->backupPath); |
735 | d->backupPath = NULL; | 860 | d->backupPath = NULL; |
861 | delete_TextBuf(d->buffered); | ||
862 | deinit_String(&d->srcHint); | ||
863 | deinit_String(&d->hint); | ||
864 | deinit_String(&d->oldText); | ||
865 | #if LAGRANGE_USE_SYSTEM_TEXT_INPUT | ||
866 | delete_SystemTextInput(d->sysCtrl); | ||
867 | deinit_String(&d->text); | ||
868 | #else | ||
869 | startOrStopCursorTimer_InputWidget_(d, iFalse); | ||
736 | clearInputLines_(&d->lines); | 870 | clearInputLines_(&d->lines); |
737 | if (isSelected_Widget(d)) { | 871 | if (isSelected_Widget(d)) { |
738 | SDL_StopTextInput(); | 872 | SDL_StopTextInput(); |
739 | enableEditorKeysInMenus_(iTrue); | 873 | enableEditorKeysInMenus_(iTrue); |
740 | } | 874 | } |
741 | delete_TextBuf(d->buffered); | ||
742 | clearUndo_InputWidget_(d); | 875 | clearUndo_InputWidget_(d); |
743 | deinit_Array(&d->undoStack); | 876 | deinit_Array(&d->undoStack); |
744 | startOrStopCursorTimer_InputWidget_(d, iFalse); | ||
745 | deinit_String(&d->srcHint); | ||
746 | deinit_String(&d->hint); | ||
747 | deinit_String(&d->oldText); | ||
748 | deinit_Array(&d->lines); | 877 | deinit_Array(&d->lines); |
878 | #endif | ||
879 | } | ||
880 | |||
881 | static iBool isAllowedToInsertNewline_InputWidget_(const iInputWidget *d) { | ||
882 | return ~d->inFlags & isSensitive_InputWidgetFlag && | ||
883 | ~d->inFlags & isUrl_InputWidgetFlag && | ||
884 | d->inFlags & lineBreaksEnabled_InputWidgetFlag && d->maxLen == 0; | ||
749 | } | 885 | } |
750 | 886 | ||
887 | #if LAGRANGE_USE_SYSTEM_TEXT_INPUT | ||
888 | static void updateAfterVisualOffsetChange_InputWidget_(iInputWidget *d, iRoot *root) { | ||
889 | iAssert(as_Widget(d)->root == root); | ||
890 | iUnused(root); | ||
891 | if (d->sysCtrl) { | ||
892 | setRect_SystemTextInput(d->sysCtrl, contentBounds_InputWidget_(d)); | ||
893 | } | ||
894 | } | ||
895 | #endif | ||
896 | |||
751 | void setFont_InputWidget(iInputWidget *d, int fontId) { | 897 | void setFont_InputWidget(iInputWidget *d, int fontId) { |
752 | d->font = fontId; | 898 | d->font = fontId; |
753 | updateMetrics_InputWidget_(d); | 899 | updateMetrics_InputWidget_(d); |
754 | } | 900 | } |
755 | 901 | ||
902 | #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT | ||
756 | static void pushUndo_InputWidget_(iInputWidget *d) { | 903 | static void pushUndo_InputWidget_(iInputWidget *d) { |
757 | iInputUndo undo; | 904 | iInputUndo undo; |
758 | init_InputUndo_(&undo, &d->lines, d->cursor); | 905 | init_InputUndo_(&undo, &d->lines, d->cursor); |
@@ -766,7 +913,6 @@ static void pushUndo_InputWidget_(iInputWidget *d) { | |||
766 | static iBool popUndo_InputWidget_(iInputWidget *d) { | 913 | static iBool popUndo_InputWidget_(iInputWidget *d) { |
767 | if (!isEmpty_Array(&d->undoStack)) { | 914 | if (!isEmpty_Array(&d->undoStack)) { |
768 | iInputUndo *undo = back_Array(&d->undoStack); | 915 | iInputUndo *undo = back_Array(&d->undoStack); |
769 | //setCopy_Array(&d->text, &undo->text); | ||
770 | splitToLines_(&undo->text, &d->lines); | 916 | splitToLines_(&undo->text, &d->lines); |
771 | d->cursor = undo->cursor; | 917 | d->cursor = undo->cursor; |
772 | deinit_InputUndo_(undo); | 918 | deinit_InputUndo_(undo); |
@@ -778,6 +924,43 @@ static iBool popUndo_InputWidget_(iInputWidget *d) { | |||
778 | return iFalse; | 924 | return iFalse; |
779 | } | 925 | } |
780 | 926 | ||
927 | iLocalDef iInputLine *cursorLine_InputWidget_(iInputWidget *d) { | ||
928 | return at_Array(&d->lines, d->cursor.y); | ||
929 | } | ||
930 | |||
931 | iLocalDef const iInputLine *constCursorLine_InputWidget_(const iInputWidget *d) { | ||
932 | return constAt_Array(&d->lines, d->cursor.y); | ||
933 | } | ||
934 | |||
935 | iLocalDef iInt2 cursorMax_InputWidget_(const iInputWidget *d) { | ||
936 | const int yLast = size_Array(&d->lines) - 1; | ||
937 | return init_I2(endX_InputWidget_(d, yLast), yLast); | ||
938 | } | ||
939 | |||
940 | static size_t cursorToIndex_InputWidget_(const iInputWidget *d, iInt2 pos) { | ||
941 | if (pos.y < 0) { | ||
942 | return 0; | ||
943 | } | ||
944 | if (pos.y >= size_Array(&d->lines)) { | ||
945 | return lastLine_InputWidget_(d)->range.end; | ||
946 | } | ||
947 | const iInputLine *line = line_InputWidget_(d, pos.y); | ||
948 | pos.x = iClamp(pos.x, 0, endX_InputWidget_(d, pos.y)); | ||
949 | return line->range.start + pos.x; | ||
950 | } | ||
951 | |||
952 | static iInt2 indexToCursor_InputWidget_(const iInputWidget *d, size_t index) { | ||
953 | /* TODO: The lines are sorted; this could use a binary search. */ | ||
954 | iConstForEach(Array, i, &d->lines) { | ||
955 | const iInputLine *line = i.value; | ||
956 | if (contains_Range(&line->range, index)) { | ||
957 | return init_I2(index - line->range.start, index_ArrayConstIterator(&i)); | ||
958 | } | ||
959 | } | ||
960 | return cursorMax_InputWidget_(d); | ||
961 | } | ||
962 | #endif | ||
963 | |||
781 | void setMode_InputWidget(iInputWidget *d, enum iInputMode mode) { | 964 | void setMode_InputWidget(iInputWidget *d, enum iInputMode mode) { |
782 | d->mode = mode; | 965 | d->mode = mode; |
783 | } | 966 | } |
@@ -876,7 +1059,11 @@ void setContentPadding_InputWidget(iInputWidget *d, int left, int right) { | |||
876 | } | 1059 | } |
877 | 1060 | ||
878 | iLocalDef iBool isEmpty_InputWidget_(const iInputWidget *d) { | 1061 | iLocalDef iBool isEmpty_InputWidget_(const iInputWidget *d) { |
1062 | #if LAGRANGE_USE_SYSTEM_TEXT_INPUT | ||
1063 | return isEmpty_String(&d->text); | ||
1064 | #else | ||
879 | return size_Array(&d->lines) == 1 && isEmpty_String(&line_InputWidget_(d, 0)->text); | 1065 | return size_Array(&d->lines) == 1 && isEmpty_String(&line_InputWidget_(d, 0)->text); |
1066 | #endif | ||
880 | } | 1067 | } |
881 | 1068 | ||
882 | static iBool isHintVisible_InputWidget_(const iInputWidget *d) { | 1069 | static iBool isHintVisible_InputWidget_(const iInputWidget *d) { |
@@ -890,11 +1077,15 @@ static void updateBuffered_InputWidget_(iInputWidget *d) { | |||
890 | } | 1077 | } |
891 | else { | 1078 | else { |
892 | /* Draw all the potentially visible lines to a buffer. */ | 1079 | /* Draw all the potentially visible lines to a buffer. */ |
1080 | #if LAGRANGE_USE_SYSTEM_TEXT_INPUT | ||
1081 | iString *visText = copy_String(&d->text); | ||
1082 | #else | ||
893 | iString *visText = new_String(); | 1083 | iString *visText = new_String(); |
894 | const iRangei visRange = visibleLineRange_InputWidget_(d); | 1084 | const iRangei visRange = visibleLineRange_InputWidget_(d); |
895 | for (int i = visRange.start; i < visRange.end; i++) { | 1085 | for (int i = visRange.start; i < visRange.end; i++) { |
896 | append_String(visText, &line_InputWidget_(d, i)->text); | 1086 | append_String(visText, &line_InputWidget_(d, i)->text); |
897 | } | 1087 | } |
1088 | #endif | ||
898 | if (d->inFlags & isUrl_InputWidgetFlag) { | 1089 | if (d->inFlags & isUrl_InputWidgetFlag) { |
899 | /* Highlight the host name. */ | 1090 | /* Highlight the host name. */ |
900 | iUrl parts; | 1091 | iUrl parts; |
@@ -912,6 +1103,7 @@ static void updateBuffered_InputWidget_(iInputWidget *d) { | |||
912 | } | 1103 | } |
913 | } | 1104 | } |
914 | iWrapText wt = wrap_InputWidget_(d, 0); | 1105 | iWrapText wt = wrap_InputWidget_(d, 0); |
1106 | wt.maxLines = d->maxWrapLines; | ||
915 | wt.text = range_String(visText); | 1107 | wt.text = range_String(visText); |
916 | const int fg = uiInputText_ColorId; | 1108 | const int fg = uiInputText_ColorId; |
917 | d->buffered = new_TextBuf(&wt, d->font, fg); | 1109 | d->buffered = new_TextBuf(&wt, d->font, fg); |
@@ -920,25 +1112,18 @@ static void updateBuffered_InputWidget_(iInputWidget *d) { | |||
920 | d->inFlags &= ~needUpdateBuffer_InputWidgetFlag; | 1112 | d->inFlags &= ~needUpdateBuffer_InputWidgetFlag; |
921 | } | 1113 | } |
922 | 1114 | ||
923 | iLocalDef iInputLine *cursorLine_InputWidget_(iInputWidget *d) { | ||
924 | return at_Array(&d->lines, d->cursor.y); | ||
925 | } | ||
926 | |||
927 | iLocalDef const iInputLine *constCursorLine_InputWidget_(const iInputWidget *d) { | ||
928 | return constAt_Array(&d->lines, d->cursor.y); | ||
929 | } | ||
930 | |||
931 | iLocalDef iInt2 cursorMax_InputWidget_(const iInputWidget *d) { | ||
932 | const int yLast = size_Array(&d->lines) - 1; | ||
933 | return init_I2(endX_InputWidget_(d, yLast), yLast); | ||
934 | } | ||
935 | |||
936 | void setText_InputWidget(iInputWidget *d, const iString *text) { | 1115 | void setText_InputWidget(iInputWidget *d, const iString *text) { |
937 | if (!d) return; | 1116 | if (!d) return; |
938 | if (d->inFlags & isUrl_InputWidgetFlag) { | 1117 | if (d->inFlags & isUrl_InputWidgetFlag) { |
939 | /* If user wants URLs encoded, also Punycode the domain. */ | 1118 | if (prefs_App()->decodeUserVisibleURLs) { |
940 | if (!prefs_App()->decodeUserVisibleURLs) { | 1119 | iString *enc = collect_String(copy_String(text)); |
1120 | urlDecodePath_String(enc); | ||
1121 | text = enc; | ||
1122 | } | ||
1123 | else { | ||
1124 | /* The user wants URLs encoded, also Punycode the domain. */ | ||
941 | iString *enc = collect_String(copy_String(text)); | 1125 | iString *enc = collect_String(copy_String(text)); |
1126 | urlEncodePath_String(enc); | ||
942 | /* Prevent address bar spoofing (mentioned as IDN homograph attack in | 1127 | /* Prevent address bar spoofing (mentioned as IDN homograph attack in |
943 | https://github.com/skyjake/lagrange/issues/73) */ | 1128 | https://github.com/skyjake/lagrange/issues/73) */ |
944 | punyEncodeUrlHost_String(enc); | 1129 | punyEncodeUrlHost_String(enc); |
@@ -949,9 +1134,10 @@ void setText_InputWidget(iInputWidget *d, const iString *text) { | |||
949 | text = omitDefaultScheme_(collect_String(copy_String(text))); | 1134 | text = omitDefaultScheme_(collect_String(copy_String(text))); |
950 | } | 1135 | } |
951 | } | 1136 | } |
952 | clearUndo_InputWidget_(d); | ||
953 | iString *nfcText = collect_String(copy_String(text)); | 1137 | iString *nfcText = collect_String(copy_String(text)); |
954 | normalize_String(nfcText); | 1138 | normalize_String(nfcText); |
1139 | #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT | ||
1140 | clearUndo_InputWidget_(d); | ||
955 | splitToLines_(nfcText, &d->lines); | 1141 | splitToLines_(nfcText, &d->lines); |
956 | iAssert(!isEmpty_Array(&d->lines)); | 1142 | iAssert(!isEmpty_Array(&d->lines)); |
957 | iForEach(Array, i, &d->lines) { | 1143 | iForEach(Array, i, &d->lines) { |
@@ -962,12 +1148,23 @@ void setText_InputWidget(iInputWidget *d, const iString *text) { | |||
962 | if (!isFocused_Widget(d)) { | 1148 | if (!isFocused_Widget(d)) { |
963 | iZap(d->mark); | 1149 | iZap(d->mark); |
964 | } | 1150 | } |
1151 | #else | ||
1152 | set_String(&d->text, nfcText); | ||
1153 | if (d->sysCtrl) { | ||
1154 | setText_SystemTextInput(d->sysCtrl, nfcText, iTrue); | ||
1155 | } | ||
1156 | else { | ||
1157 | updateAllLinesAndResizeHeight_InputWidget_(d); /* need to know the new height */ | ||
1158 | } | ||
1159 | #endif | ||
965 | if (!isFocused_Widget(d)) { | 1160 | if (!isFocused_Widget(d)) { |
966 | d->inFlags |= needUpdateBuffer_InputWidgetFlag; | 1161 | d->inFlags |= needUpdateBuffer_InputWidgetFlag; |
967 | } | 1162 | } |
968 | updateVisible_InputWidget_(d); | 1163 | updateVisible_InputWidget_(d); |
969 | updateMetrics_InputWidget_(d); | 1164 | updateMetrics_InputWidget_(d); |
970 | refresh_Widget(as_Widget(d)); | 1165 | if (!d->sysCtrl) { |
1166 | refresh_Widget(as_Widget(d)); | ||
1167 | } | ||
971 | } | 1168 | } |
972 | 1169 | ||
973 | void setTextCStr_InputWidget(iInputWidget *d, const char *cstr) { | 1170 | void setTextCStr_InputWidget(iInputWidget *d, const char *cstr) { |
@@ -976,38 +1173,39 @@ void setTextCStr_InputWidget(iInputWidget *d, const char *cstr) { | |||
976 | delete_String(str); | 1173 | delete_String(str); |
977 | } | 1174 | } |
978 | 1175 | ||
979 | static size_t cursorToIndex_InputWidget_(const iInputWidget *d, iInt2 pos) { | ||
980 | if (pos.y < 0) { | ||
981 | return 0; | ||
982 | } | ||
983 | if (pos.y >= size_Array(&d->lines)) { | ||
984 | return lastLine_InputWidget_(d)->range.end; | ||
985 | } | ||
986 | const iInputLine *line = line_InputWidget_(d, pos.y); | ||
987 | pos.x = iClamp(pos.x, 0, endX_InputWidget_(d, pos.y)); | ||
988 | return line->range.start + pos.x; | ||
989 | } | ||
990 | |||
991 | static iInt2 indexToCursor_InputWidget_(const iInputWidget *d, size_t index) { | ||
992 | /* TODO: The lines are sorted; this could use a binary search. */ | ||
993 | iConstForEach(Array, i, &d->lines) { | ||
994 | const iInputLine *line = i.value; | ||
995 | if (contains_Range(&line->range, index)) { | ||
996 | return init_I2(index - line->range.start, index_ArrayConstIterator(&i)); | ||
997 | } | ||
998 | } | ||
999 | return cursorMax_InputWidget_(d); | ||
1000 | } | ||
1001 | |||
1002 | void selectAll_InputWidget(iInputWidget *d) { | 1176 | void selectAll_InputWidget(iInputWidget *d) { |
1177 | #if LAGRANGE_USE_SYSTEM_TEXT_INPUT | ||
1178 | if (d->sysCtrl) { | ||
1179 | selectAll_SystemTextInput(d->sysCtrl); | ||
1180 | } | ||
1181 | #else | ||
1003 | d->mark = (iRanges){ 0, lastLine_InputWidget_(d)->range.end }; | 1182 | d->mark = (iRanges){ 0, lastLine_InputWidget_(d)->range.end }; |
1004 | refresh_Widget(as_Widget(d)); | 1183 | refresh_Widget(as_Widget(d)); |
1184 | #endif | ||
1185 | } | ||
1186 | |||
1187 | void validate_InputWidget(iInputWidget *d) { | ||
1188 | if (d->validator) { | ||
1189 | d->validator(d, d->validatorContext); /* this may change the contents */ | ||
1190 | } | ||
1005 | } | 1191 | } |
1006 | 1192 | ||
1007 | iLocalDef iBool isEditing_InputWidget_(const iInputWidget *d) { | 1193 | iLocalDef iBool isEditing_InputWidget_(const iInputWidget *d) { |
1008 | return (flags_Widget(constAs_Widget(d)) & selected_WidgetFlag) != 0; | 1194 | return (flags_Widget(constAs_Widget(d)) & selected_WidgetFlag) != 0; |
1009 | } | 1195 | } |
1010 | 1196 | ||
1197 | static void contentsWereChanged_InputWidget_(iInputWidget *); | ||
1198 | |||
1199 | #if LAGRANGE_USE_SYSTEM_TEXT_INPUT | ||
1200 | void systemInputChanged_InputWidget_(iSystemTextInput *sysCtrl, void *widget) { | ||
1201 | iInputWidget *d = widget; | ||
1202 | set_String(&d->text, text_SystemTextInput(sysCtrl)); | ||
1203 | restartBackupTimer_InputWidget_(d); | ||
1204 | contentsWereChanged_InputWidget_(d); | ||
1205 | updateMetrics_InputWidget_(d); | ||
1206 | } | ||
1207 | #endif | ||
1208 | |||
1011 | void begin_InputWidget(iInputWidget *d) { | 1209 | void begin_InputWidget(iInputWidget *d) { |
1012 | iWidget *w = as_Widget(d); | 1210 | iWidget *w = as_Widget(d); |
1013 | if (isEditing_InputWidget_(d)) { | 1211 | if (isEditing_InputWidget_(d)) { |
@@ -1016,6 +1214,29 @@ void begin_InputWidget(iInputWidget *d) { | |||
1016 | } | 1214 | } |
1017 | invalidateBuffered_InputWidget_(d); | 1215 | invalidateBuffered_InputWidget_(d); |
1018 | setFlags_Widget(w, hidden_WidgetFlag | disabled_WidgetFlag, iFalse); | 1216 | setFlags_Widget(w, hidden_WidgetFlag | disabled_WidgetFlag, iFalse); |
1217 | setFlags_Widget(w, selected_WidgetFlag, iTrue); | ||
1218 | d->inFlags &= ~enterPressed_InputWidgetFlag; | ||
1219 | #if LAGRANGE_USE_SYSTEM_TEXT_INPUT | ||
1220 | set_String(&d->oldText, &d->text); | ||
1221 | d->sysCtrl = new_SystemTextInput( | ||
1222 | contentBounds_InputWidget_(d), | ||
1223 | (d->maxWrapLines > 1 ? multiLine_SystemTextInputFlags : 0) | | ||
1224 | (d->inFlags & isUrl_InputWidgetFlag ? (disableAutocorrect_SystemTextInputFlag | | ||
1225 | disableAutocapitalize_SystemTextInputFlag) | ||
1226 | : 0) | | ||
1227 | /* widget-specific tweaks (hacks) */ | ||
1228 | (!cmp_String(id_Widget(w), "url") ? returnGo_SystemTextInputFlags : 0) | | ||
1229 | (!cmp_String(id_Widget(w), "upload.text") ? extraPadding_SystemTextInputFlag : 0) | | ||
1230 | (flags_Widget(w) & alignRight_WidgetFlag ? alignRight_SystemTextInputFlag : 0) | | ||
1231 | (isAllowedToInsertNewline_InputWidget_(d) ? insertNewlines_SystemTextInputFlag : 0) | | ||
1232 | (d->inFlags & selectAllOnFocus_InputWidgetFlag ? selectAll_SystemTextInputFlags : 0)); | ||
1233 | setFont_SystemTextInput(d->sysCtrl, d->font); | ||
1234 | setText_SystemTextInput(d->sysCtrl, &d->oldText, iFalse); | ||
1235 | setTextChangedFunc_SystemTextInput(d->sysCtrl, systemInputChanged_InputWidget_, d); | ||
1236 | iConnect(Root, w->root, visualOffsetsChanged, d, updateAfterVisualOffsetChange_InputWidget_); | ||
1237 | updateTextInputRect_InputWidget_(d); | ||
1238 | updateMetrics_InputWidget_(d); | ||
1239 | #else | ||
1019 | mergeLines_(&d->lines, &d->oldText); | 1240 | mergeLines_(&d->lines, &d->oldText); |
1020 | if (d->mode == overwrite_InputMode) { | 1241 | if (d->mode == overwrite_InputMode) { |
1021 | d->cursor = zero_I2(); | 1242 | d->cursor = zero_I2(); |
@@ -1025,11 +1246,9 @@ void begin_InputWidget(iInputWidget *d) { | |||
1025 | d->cursor.x = iMin(d->cursor.x, cursorLine_InputWidget_(d)->range.end); | 1246 | d->cursor.x = iMin(d->cursor.x, cursorLine_InputWidget_(d)->range.end); |
1026 | } | 1247 | } |
1027 | SDL_StartTextInput(); | 1248 | SDL_StartTextInput(); |
1028 | setFlags_Widget(w, selected_WidgetFlag, iTrue); | ||
1029 | showCursor_InputWidget_(d); | 1249 | showCursor_InputWidget_(d); |
1030 | refresh_Widget(w); | 1250 | refresh_Widget(w); |
1031 | startOrStopCursorTimer_InputWidget_(d, iTrue); | 1251 | startOrStopCursorTimer_InputWidget_(d, iTrue); |
1032 | d->inFlags &= ~enterPressed_InputWidgetFlag; | ||
1033 | if (d->inFlags & selectAllOnFocus_InputWidgetFlag) { | 1252 | if (d->inFlags & selectAllOnFocus_InputWidgetFlag) { |
1034 | d->mark = (iRanges){ 0, lastLine_InputWidget_(d)->range.end }; | 1253 | d->mark = (iRanges){ 0, lastLine_InputWidget_(d)->range.end }; |
1035 | d->cursor = cursorMax_InputWidget_(d); | 1254 | d->cursor = cursorMax_InputWidget_(d); |
@@ -1040,6 +1259,7 @@ void begin_InputWidget(iInputWidget *d) { | |||
1040 | enableEditorKeysInMenus_(iFalse); | 1259 | enableEditorKeysInMenus_(iFalse); |
1041 | updateTextInputRect_InputWidget_(d); | 1260 | updateTextInputRect_InputWidget_(d); |
1042 | updateVisible_InputWidget_(d); | 1261 | updateVisible_InputWidget_(d); |
1262 | #endif | ||
1043 | } | 1263 | } |
1044 | 1264 | ||
1045 | void end_InputWidget(iInputWidget *d, iBool accept) { | 1265 | void end_InputWidget(iInputWidget *d, iBool accept) { |
@@ -1048,15 +1268,29 @@ void end_InputWidget(iInputWidget *d, iBool accept) { | |||
1048 | /* Was not active. */ | 1268 | /* Was not active. */ |
1049 | return; | 1269 | return; |
1050 | } | 1270 | } |
1051 | enableEditorKeysInMenus_(iTrue); | 1271 | #if LAGRANGE_USE_SYSTEM_TEXT_INPUT |
1272 | if (d->sysCtrl) { | ||
1273 | iDisconnect(Root, w->root, visualOffsetsChanged, d, updateAfterVisualOffsetChange_InputWidget_); | ||
1274 | if (accept) { | ||
1275 | set_String(&d->text, text_SystemTextInput(d->sysCtrl)); | ||
1276 | } | ||
1277 | else { | ||
1278 | set_String(&d->text, &d->oldText); | ||
1279 | } | ||
1280 | delete_SystemTextInput(d->sysCtrl); | ||
1281 | d->sysCtrl = NULL; | ||
1282 | } | ||
1283 | #else | ||
1052 | if (!accept) { | 1284 | if (!accept) { |
1053 | /* Overwrite the edited lines. */ | 1285 | /* Overwrite the edited lines. */ |
1054 | splitToLines_(&d->oldText, &d->lines); | 1286 | splitToLines_(&d->oldText, &d->lines); |
1055 | } | 1287 | } |
1056 | d->inFlags |= needUpdateBuffer_InputWidgetFlag; | 1288 | SDL_StopTextInput(); |
1289 | enableEditorKeysInMenus_(iTrue); | ||
1057 | d->inFlags &= ~isMarking_InputWidgetFlag; | 1290 | d->inFlags &= ~isMarking_InputWidgetFlag; |
1058 | startOrStopCursorTimer_InputWidget_(d, iFalse); | 1291 | startOrStopCursorTimer_InputWidget_(d, iFalse); |
1059 | SDL_StopTextInput(); | 1292 | #endif |
1293 | d->inFlags |= needUpdateBuffer_InputWidgetFlag; | ||
1060 | setFlags_Widget(w, selected_WidgetFlag | keepOnTop_WidgetFlag | touchDrag_WidgetFlag, iFalse); | 1294 | setFlags_Widget(w, selected_WidgetFlag | keepOnTop_WidgetFlag | touchDrag_WidgetFlag, iFalse); |
1061 | const char *id = cstr_String(id_Widget(as_Widget(d))); | 1295 | const char *id = cstr_String(id_Widget(as_Widget(d))); |
1062 | if (!*id) id = "_"; | 1296 | if (!*id) id = "_"; |
@@ -1068,6 +1302,7 @@ void end_InputWidget(iInputWidget *d, iBool accept) { | |||
1068 | accept ? 1 : 0); | 1302 | accept ? 1 : 0); |
1069 | } | 1303 | } |
1070 | 1304 | ||
1305 | #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT | ||
1071 | static void textOfLinesWasChanged_InputWidget_(iInputWidget *d, iRangei lineRange) { | 1306 | static void textOfLinesWasChanged_InputWidget_(iInputWidget *d, iRangei lineRange) { |
1072 | for (int i = lineRange.start; i < lineRange.end; i++) { | 1307 | for (int i = lineRange.start; i < lineRange.end; i++) { |
1073 | updateLine_InputWidget_(d, at_Array(&d->lines, i)); | 1308 | updateLine_InputWidget_(d, at_Array(&d->lines, i)); |
@@ -1201,27 +1436,6 @@ static iBool moveCursorByLine_InputWidget_(iInputWidget *d, int dir, int horiz) | |||
1201 | return iTrue; | 1436 | return iTrue; |
1202 | } | 1437 | } |
1203 | 1438 | ||
1204 | void setSensitiveContent_InputWidget(iInputWidget *d, iBool isSensitive) { | ||
1205 | iChangeFlags(d->inFlags, isSensitive_InputWidgetFlag, isSensitive); | ||
1206 | } | ||
1207 | |||
1208 | void setUrlContent_InputWidget(iInputWidget *d, iBool isUrl) { | ||
1209 | iChangeFlags(d->inFlags, isUrl_InputWidgetFlag, isUrl); | ||
1210 | d->inFlags |= needUpdateBuffer_InputWidgetFlag; | ||
1211 | } | ||
1212 | |||
1213 | void setSelectAllOnFocus_InputWidget(iInputWidget *d, iBool selectAllOnFocus) { | ||
1214 | iChangeFlags(d->inFlags, selectAllOnFocus_InputWidgetFlag, selectAllOnFocus); | ||
1215 | } | ||
1216 | |||
1217 | void setNotifyEdits_InputWidget(iInputWidget *d, iBool notifyEdits) { | ||
1218 | iChangeFlags(d->inFlags, notifyEdits_InputWidgetFlag, notifyEdits); | ||
1219 | } | ||
1220 | |||
1221 | void setEatEscape_InputWidget(iInputWidget *d, iBool eatEscape) { | ||
1222 | iChangeFlags(d->inFlags, eatEscape_InputWidgetFlag, eatEscape); | ||
1223 | } | ||
1224 | |||
1225 | static iRanges mark_InputWidget_(const iInputWidget *d) { | 1439 | static iRanges mark_InputWidget_(const iInputWidget *d) { |
1226 | iRanges m = { iMin(d->mark.start, d->mark.end), iMax(d->mark.start, d->mark.end) }; | 1440 | iRanges m = { iMin(d->mark.start, d->mark.end), iMax(d->mark.start, d->mark.end) }; |
1227 | const iInputLine *last = lastLine_InputWidget_(d); | 1441 | const iInputLine *last = lastLine_InputWidget_(d); |
@@ -1230,15 +1444,6 @@ static iRanges mark_InputWidget_(const iInputWidget *d) { | |||
1230 | return m; | 1444 | return m; |
1231 | } | 1445 | } |
1232 | 1446 | ||
1233 | static void contentsWereChanged_InputWidget_(iInputWidget *d) { | ||
1234 | if (d->validator) { | ||
1235 | d->validator(d, d->validatorContext); /* this may change the contents */ | ||
1236 | } | ||
1237 | if (d->inFlags & notifyEdits_InputWidgetFlag) { | ||
1238 | postCommand_Widget(d, "input.edited id:%s", cstr_String(id_Widget(constAs_Widget(d)))); | ||
1239 | } | ||
1240 | } | ||
1241 | |||
1242 | static void deleteIndexRange_InputWidget_(iInputWidget *d, iRanges deleted) { | 1447 | static void deleteIndexRange_InputWidget_(iInputWidget *d, iRanges deleted) { |
1243 | size_t firstModified = iInvalidPos; | 1448 | size_t firstModified = iInvalidPos; |
1244 | restartBackupTimer_InputWidget_(d); | 1449 | restartBackupTimer_InputWidget_(d); |
@@ -1364,7 +1569,7 @@ static iInt2 coordCursor_InputWidget_(const iInputWidget *d, iInt2 coord) { | |||
1364 | // return cursorMax_InputWidget_(d); | 1569 | // return cursorMax_InputWidget_(d); |
1365 | // } | 1570 | // } |
1366 | iWrapText wrapText = { | 1571 | iWrapText wrapText = { |
1367 | .maxWidth = d->maxLen == 0 ? width_Rect(bounds) : unlimitedWidth_InputWidget_, | 1572 | .maxWidth = d->maxLen == 0 ? iMaxi(minWidth_InputWidget_, width_Rect(bounds)) : unlimitedWidth_InputWidget_, |
1368 | .mode = (d->inFlags & isUrl_InputWidgetFlag ? anyCharacter_WrapTextMode : word_WrapTextMode), | 1573 | .mode = (d->inFlags & isUrl_InputWidgetFlag ? anyCharacter_WrapTextMode : word_WrapTextMode), |
1369 | .hitPoint = relCoord, | 1574 | .hitPoint = relCoord, |
1370 | .overrideChar = (d->inFlags & isSensitive_InputWidgetFlag ? sensitiveChar_ : 0), | 1575 | .overrideChar = (d->inFlags & isSensitive_InputWidgetFlag ? sensitiveChar_ : 0), |
@@ -1447,6 +1652,40 @@ static void extendRange_InputWidget_(iInputWidget *d, size_t *index, int dir) { | |||
1447 | *index = cursorToIndex_InputWidget_(d, pos); | 1652 | *index = cursorToIndex_InputWidget_(d, pos); |
1448 | } | 1653 | } |
1449 | 1654 | ||
1655 | static void lineTextWasChanged_InputWidget_(iInputWidget *d, iInputLine *line) { | ||
1656 | const int y = indexOf_Array(&d->lines, line); | ||
1657 | textOfLinesWasChanged_InputWidget_(d, (iRangei){ y, y + 1 }); | ||
1658 | } | ||
1659 | #endif | ||
1660 | |||
1661 | void setSensitiveContent_InputWidget(iInputWidget *d, iBool isSensitive) { | ||
1662 | iChangeFlags(d->inFlags, isSensitive_InputWidgetFlag, isSensitive); | ||
1663 | } | ||
1664 | |||
1665 | void setUrlContent_InputWidget(iInputWidget *d, iBool isUrl) { | ||
1666 | iChangeFlags(d->inFlags, isUrl_InputWidgetFlag, isUrl); | ||
1667 | d->inFlags |= needUpdateBuffer_InputWidgetFlag; | ||
1668 | } | ||
1669 | |||
1670 | void setSelectAllOnFocus_InputWidget(iInputWidget *d, iBool selectAllOnFocus) { | ||
1671 | iChangeFlags(d->inFlags, selectAllOnFocus_InputWidgetFlag, selectAllOnFocus); | ||
1672 | } | ||
1673 | |||
1674 | void setNotifyEdits_InputWidget(iInputWidget *d, iBool notifyEdits) { | ||
1675 | iChangeFlags(d->inFlags, notifyEdits_InputWidgetFlag, notifyEdits); | ||
1676 | } | ||
1677 | |||
1678 | void setEatEscape_InputWidget(iInputWidget *d, iBool eatEscape) { | ||
1679 | iChangeFlags(d->inFlags, eatEscape_InputWidgetFlag, eatEscape); | ||
1680 | } | ||
1681 | |||
1682 | static void contentsWereChanged_InputWidget_(iInputWidget *d) { | ||
1683 | validate_InputWidget(d); | ||
1684 | if (d->inFlags & notifyEdits_InputWidgetFlag) { | ||
1685 | postCommand_Widget(d, "input.edited id:%s", cstr_String(id_Widget(constAs_Widget(d)))); | ||
1686 | } | ||
1687 | } | ||
1688 | |||
1450 | static iRect bounds_InputWidget_(const iInputWidget *d) { | 1689 | static iRect bounds_InputWidget_(const iInputWidget *d) { |
1451 | const iWidget *w = constAs_Widget(d); | 1690 | const iWidget *w = constAs_Widget(d); |
1452 | iRect bounds = bounds_Widget(w); | 1691 | iRect bounds = bounds_Widget(w); |
@@ -1456,7 +1695,7 @@ static iRect bounds_InputWidget_(const iInputWidget *d) { | |||
1456 | /* There may be more visible lines than fits in the widget bounds. */ | 1695 | /* There may be more visible lines than fits in the widget bounds. */ |
1457 | bounds.size.y = contentHeight_InputWidget_(d) + 3 * padding_().y; | 1696 | bounds.size.y = contentHeight_InputWidget_(d) + 3 * padding_().y; |
1458 | if (w->flags & extraPadding_WidgetFlag) { | 1697 | if (w->flags & extraPadding_WidgetFlag) { |
1459 | bounds.size.y += extraPaddingHeight_; | 1698 | bounds.size.y += extraPaddingHeight_InputWidget_(d); |
1460 | } | 1699 | } |
1461 | return bounds; | 1700 | return bounds; |
1462 | } | 1701 | } |
@@ -1465,11 +1704,6 @@ static iBool contains_InputWidget_(const iInputWidget *d, iInt2 coord) { | |||
1465 | return contains_Rect(bounds_InputWidget_(d), coord); | 1704 | return contains_Rect(bounds_InputWidget_(d), coord); |
1466 | } | 1705 | } |
1467 | 1706 | ||
1468 | static void lineTextWasChanged_InputWidget_(iInputWidget *d, iInputLine *line) { | ||
1469 | const int y = indexOf_Array(&d->lines, line); | ||
1470 | textOfLinesWasChanged_InputWidget_(d, (iRangei){ y, y + 1 }); | ||
1471 | } | ||
1472 | |||
1473 | static iBool isArrowUpDownConsumed_InputWidget_(const iInputWidget *d) { | 1707 | static iBool isArrowUpDownConsumed_InputWidget_(const iInputWidget *d) { |
1474 | return d->maxWrapLines > 1; | 1708 | return d->maxWrapLines > 1; |
1475 | } | 1709 | } |
@@ -1494,6 +1728,7 @@ enum iEventResult { | |||
1494 | true_EventResult = 2, /* event was processed and should not be passed on */ | 1728 | true_EventResult = 2, /* event was processed and should not be passed on */ |
1495 | }; | 1729 | }; |
1496 | 1730 | ||
1731 | #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT | ||
1497 | static void markWordAtCursor_InputWidget_(iInputWidget *d) { | 1732 | static void markWordAtCursor_InputWidget_(iInputWidget *d) { |
1498 | d->mark.start = d->mark.end = cursorToIndex_InputWidget_(d, d->cursor); | 1733 | d->mark.start = d->mark.end = cursorToIndex_InputWidget_(d, d->cursor); |
1499 | extendRange_InputWidget_(d, &d->mark.start, -1); | 1734 | extendRange_InputWidget_(d, &d->mark.start, -1); |
@@ -1510,8 +1745,10 @@ static void showClipMenu_(iInt2 coord) { | |||
1510 | openMenuFlags_Widget(clipMenu, coord, iFalse); | 1745 | openMenuFlags_Widget(clipMenu, coord, iFalse); |
1511 | } | 1746 | } |
1512 | } | 1747 | } |
1748 | #endif | ||
1513 | 1749 | ||
1514 | static enum iEventResult processPointerEvents_InputWidget_(iInputWidget *d, const SDL_Event *ev) { | 1750 | static enum iEventResult processPointerEvents_InputWidget_(iInputWidget *d, const SDL_Event *ev) { |
1751 | #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT | ||
1515 | iWidget *w = as_Widget(d); | 1752 | iWidget *w = as_Widget(d); |
1516 | if (ev->type == SDL_MOUSEMOTION && (isHover_Widget(d) || flags_Widget(w) & keepOnTop_WidgetFlag)) { | 1753 | if (ev->type == SDL_MOUSEMOTION && (isHover_Widget(d) || flags_Widget(w) & keepOnTop_WidgetFlag)) { |
1517 | const iInt2 coord = init_I2(ev->motion.x, ev->motion.y); | 1754 | const iInt2 coord = init_I2(ev->motion.x, ev->motion.y); |
@@ -1585,9 +1822,11 @@ static enum iEventResult processPointerEvents_InputWidget_(iInputWidget *d, cons | |||
1585 | return true_EventResult; | 1822 | return true_EventResult; |
1586 | } | 1823 | } |
1587 | } | 1824 | } |
1825 | #endif | ||
1588 | return ignored_EventResult; | 1826 | return ignored_EventResult; |
1589 | } | 1827 | } |
1590 | 1828 | ||
1829 | #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT | ||
1591 | static iInt2 touchCoordCursor_InputWidget_(const iInputWidget *d, iInt2 coord) { | 1830 | static iInt2 touchCoordCursor_InputWidget_(const iInputWidget *d, iInt2 coord) { |
1592 | /* Clamp to the bounds so the cursor doesn't wrap at the ends. */ | 1831 | /* Clamp to the bounds so the cursor doesn't wrap at the ends. */ |
1593 | iRect bounds = shrunk_Rect(contentBounds_InputWidget_(d), one_I2()); | 1832 | iRect bounds = shrunk_Rect(contentBounds_InputWidget_(d), one_I2()); |
@@ -1609,9 +1848,11 @@ static int distanceToPos_InputWidget_(const iInputWidget *d, iInt2 uiCoord, iInt | |||
1609 | } | 1848 | } |
1610 | return dist_I2(addY_I2(winCoord, lineHeight_Text(d->font) / 2), uiCoord); | 1849 | return dist_I2(addY_I2(winCoord, lineHeight_Text(d->font) / 2), uiCoord); |
1611 | } | 1850 | } |
1851 | #endif | ||
1612 | 1852 | ||
1613 | static enum iEventResult processTouchEvents_InputWidget_(iInputWidget *d, const SDL_Event *ev) { | 1853 | static enum iEventResult processTouchEvents_InputWidget_(iInputWidget *d, const SDL_Event *ev) { |
1614 | iWidget *w = as_Widget(d); | 1854 | iWidget *w = as_Widget(d); |
1855 | #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT | ||
1615 | /* | 1856 | /* |
1616 | + first tap to focus & select all/place cursor | 1857 | + first tap to focus & select all/place cursor |
1617 | + focused tap to place cursor | 1858 | + focused tap to place cursor |
@@ -1853,9 +2094,22 @@ static enum iEventResult processTouchEvents_InputWidget_(iInputWidget *d, const | |||
1853 | // /* Eat all mouse clicks on the widget. */ | 2094 | // /* Eat all mouse clicks on the widget. */ |
1854 | // return true_EventResult; | 2095 | // return true_EventResult; |
1855 | // } | 2096 | // } |
2097 | #else | ||
2098 | /* Just a tap to activate the system-provided text input control. */ | ||
2099 | switch (processEvent_Click(&d->click, ev)) { | ||
2100 | case none_ClickResult: | ||
2101 | break; | ||
2102 | case started_ClickResult: | ||
2103 | setFocus_Widget(w); | ||
2104 | return true_EventResult; | ||
2105 | default: | ||
2106 | return true_EventResult; | ||
2107 | } | ||
2108 | #endif | ||
1856 | return ignored_EventResult; | 2109 | return ignored_EventResult; |
1857 | } | 2110 | } |
1858 | 2111 | ||
2112 | #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT | ||
1859 | static void clampWheelAccum_InputWidget_(iInputWidget *d, int wheel) { | 2113 | static void clampWheelAccum_InputWidget_(iInputWidget *d, int wheel) { |
1860 | if (wheel > 0 && d->visWrapLines.start == 0) { | 2114 | if (wheel > 0 && d->visWrapLines.start == 0) { |
1861 | d->wheelAccum = 0; | 2115 | d->wheelAccum = 0; |
@@ -1866,12 +2120,41 @@ static void clampWheelAccum_InputWidget_(iInputWidget *d, int wheel) { | |||
1866 | refresh_Widget(d); | 2120 | refresh_Widget(d); |
1867 | } | 2121 | } |
1868 | } | 2122 | } |
2123 | #endif | ||
2124 | |||
2125 | static void overflowScrollToKeepVisible_InputWidget_(iAny *widget) { | ||
2126 | iInputWidget *d = widget; | ||
2127 | iWidget *w = as_Widget(d); | ||
2128 | if (!isFocused_Widget(w) || isAffectedByVisualOffset_Widget(w)) { | ||
2129 | return; | ||
2130 | } | ||
2131 | iRect rect = boundsWithoutVisualOffset_Widget(w); | ||
2132 | iRect visible = visibleRect_Root(w->root); | ||
2133 | const uint32_t nowTime = SDL_GetTicks(); | ||
2134 | const double elapsed = (nowTime - d->lastOverflowScrollTime) / 1000.0; | ||
2135 | int dist = bottom_Rect(rect) + gap_UI - bottom_Rect(visible); | ||
2136 | const int step = iRound(10 * dist * elapsed); | ||
2137 | if (step > 0) { | ||
2138 | iWidget *scrollable = findOverflowScrollable_Widget(w); | ||
2139 | if (scrollable) { | ||
2140 | scrollOverflow_Widget(scrollable, -iClamp(step, 1, dist)); | ||
2141 | d->lastOverflowScrollTime = nowTime; | ||
2142 | } | ||
2143 | } | ||
2144 | if (dist > 0) { | ||
2145 | addTicker_App(overflowScrollToKeepVisible_InputWidget_, widget); | ||
2146 | } | ||
2147 | } | ||
1869 | 2148 | ||
1870 | static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { | 2149 | static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { |
1871 | iWidget *w = as_Widget(d); | 2150 | iWidget *w = as_Widget(d); |
1872 | /* Resize according to width immediately. */ | 2151 | /* Resize according to width immediately. */ |
1873 | if (d->lastUpdateWidth != w->rect.size.x) { | 2152 | if (d->lastUpdateWidth != w->rect.size.x) { |
1874 | d->inFlags |= needUpdateBuffer_InputWidgetFlag; | 2153 | d->inFlags |= needUpdateBuffer_InputWidgetFlag; |
2154 | if (contentBounds_InputWidget_(d).size.x < minWidth_InputWidget_) { | ||
2155 | setFocus_Widget(NULL); | ||
2156 | return iFalse; | ||
2157 | } | ||
1875 | if (d->inFlags & isUrl_InputWidgetFlag) { | 2158 | if (d->inFlags & isUrl_InputWidgetFlag) { |
1876 | /* Restore/omit the default scheme if necessary. */ | 2159 | /* Restore/omit the default scheme if necessary. */ |
1877 | setText_InputWidget(d, text_InputWidget(d)); | 2160 | setText_InputWidget(d, text_InputWidget(d)); |
@@ -1879,15 +2162,24 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { | |||
1879 | updateAllLinesAndResizeHeight_InputWidget_(d); | 2162 | updateAllLinesAndResizeHeight_InputWidget_(d); |
1880 | d->lastUpdateWidth = w->rect.size.x; | 2163 | d->lastUpdateWidth = w->rect.size.x; |
1881 | } | 2164 | } |
1882 | if (isCommand_Widget(w, ev, "focus.gained")) { | 2165 | #if LAGRANGE_USE_SYSTEM_TEXT_INPUT |
1883 | begin_InputWidget(d); | 2166 | if (isResize_UserEvent(ev)) { |
2167 | if (d->sysCtrl) { | ||
2168 | updateAfterVisualOffsetChange_InputWidget_(d, w->root); | ||
2169 | } | ||
2170 | } | ||
2171 | #endif | ||
2172 | if (deviceType_App() != desktop_AppDeviceType && isCommand_UserEvent(ev, "menu.opened")) { | ||
2173 | setFocus_Widget(NULL); | ||
1884 | return iFalse; | 2174 | return iFalse; |
1885 | } | 2175 | } |
1886 | else if (isEditing_InputWidget_(d) && (isCommand_UserEvent(ev, "window.focus.lost") || | 2176 | if (isCommand_Widget(w, ev, "focus.gained")) { |
1887 | isCommand_UserEvent(ev, "window.focus.gained"))) { | 2177 | if (contentBounds_InputWidget_(d).size.x < minWidth_InputWidget_) { |
1888 | startOrStopCursorTimer_InputWidget_(d, isCommand_UserEvent(ev, "window.focus.gained")); | 2178 | setFocus_Widget(NULL); |
1889 | d->cursorVis = 1; | 2179 | } |
1890 | refresh_Widget(d); | 2180 | else { |
2181 | begin_InputWidget(d); | ||
2182 | } | ||
1891 | return iFalse; | 2183 | return iFalse; |
1892 | } | 2184 | } |
1893 | else if (isCommand_UserEvent(ev, "keyroot.changed")) { | 2185 | else if (isCommand_UserEvent(ev, "keyroot.changed")) { |
@@ -1902,11 +2194,29 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { | |||
1902 | end_InputWidget(d, iTrue); | 2194 | end_InputWidget(d, iTrue); |
1903 | return iFalse; | 2195 | return iFalse; |
1904 | } | 2196 | } |
2197 | #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT | ||
2198 | else if (isCommand_UserEvent(ev, "prefs.blink.changed")) { | ||
2199 | if (isEditing_InputWidget_(d) && arg_Command(command_UserEvent(ev))) { | ||
2200 | startOrStopCursorTimer_InputWidget_(d, 2); | ||
2201 | } | ||
2202 | return iFalse; | ||
2203 | } | ||
2204 | else if (isEditing_InputWidget_(d) && (isCommand_UserEvent(ev, "window.focus.lost") || | ||
2205 | isCommand_UserEvent(ev, "window.focus.gained"))) { | ||
2206 | startOrStopCursorTimer_InputWidget_(d, isCommand_UserEvent(ev, "window.focus.gained")); | ||
2207 | d->cursorVis = 1; | ||
2208 | refresh_Widget(d); | ||
2209 | return iFalse; | ||
2210 | } | ||
1905 | else if ((isCommand_UserEvent(ev, "copy") || isCommand_UserEvent(ev, "input.copy")) && | 2211 | else if ((isCommand_UserEvent(ev, "copy") || isCommand_UserEvent(ev, "input.copy")) && |
1906 | isEditing_InputWidget_(d)) { | 2212 | isEditing_InputWidget_(d)) { |
1907 | copy_InputWidget_(d, argLabel_Command(command_UserEvent(ev), "cut")); | 2213 | copy_InputWidget_(d, argLabel_Command(command_UserEvent(ev), "cut")); |
1908 | return iTrue; | 2214 | return iTrue; |
1909 | } | 2215 | } |
2216 | // else if (isFocused_Widget(d) && isCommand_UserEvent(ev, "copy")) { | ||
2217 | // copy_InputWidget_(d, iFalse); | ||
2218 | // return iTrue; | ||
2219 | // } | ||
1910 | else if (isCommand_UserEvent(ev, "input.paste") && isEditing_InputWidget_(d)) { | 2220 | else if (isCommand_UserEvent(ev, "input.paste") && isEditing_InputWidget_(d)) { |
1911 | paste_InputWidget_(d); | 2221 | paste_InputWidget_(d); |
1912 | return iTrue; | 2222 | return iTrue; |
@@ -1918,6 +2228,14 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { | |||
1918 | } | 2228 | } |
1919 | return iTrue; | 2229 | return iTrue; |
1920 | } | 2230 | } |
2231 | else if (isCommand_UserEvent(ev, "text.insert")) { | ||
2232 | pushUndo_InputWidget_(d); | ||
2233 | deleteMarked_InputWidget_(d); | ||
2234 | insertChar_InputWidget_(d, arg_Command(command_UserEvent(ev))); | ||
2235 | contentsWereChanged_InputWidget_(d); | ||
2236 | return iTrue; | ||
2237 | } | ||
2238 | #endif | ||
1921 | else if (isCommand_UserEvent(ev, "input.selectall") && isEditing_InputWidget_(d)) { | 2239 | else if (isCommand_UserEvent(ev, "input.selectall") && isEditing_InputWidget_(d)) { |
1922 | selectAll_InputWidget(d); | 2240 | selectAll_InputWidget(d); |
1923 | return iTrue; | 2241 | return iTrue; |
@@ -1928,24 +2246,19 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { | |||
1928 | } | 2246 | } |
1929 | return iFalse; | 2247 | return iFalse; |
1930 | } | 2248 | } |
1931 | /* TODO: Scroll to keep widget visible when keyboard appears. */ | 2249 | else if (isCommand_UserEvent(ev, "keyboard.changed")) { |
1932 | // else if (isCommand_UserEvent(ev, "keyboard.changed")) { | 2250 | const iBool isKeyboardVisible = (arg_Command(command_UserEvent(ev)) != 0); |
1933 | // if (isFocused_Widget(d) && arg_Command(command_UserEvent(ev))) { | 2251 | /* Scroll to keep widget visible when keyboard appears. */ |
1934 | // iRect rect = bounds_Widget(w); | 2252 | if (isFocused_Widget(d)) { |
1935 | // rect.pos.y -= value_Anim(&get_Window()->rootOffset); | 2253 | if (isKeyboardVisible) { |
1936 | // const iInt2 visRoot = visibleSize_Root(w->root); | 2254 | d->lastOverflowScrollTime = SDL_GetTicks(); |
1937 | // if (bottom_Rect(rect) > visRoot.y) { | 2255 | overflowScrollToKeepVisible_InputWidget_(d); |
1938 | // setValue_Anim(&get_Window()->rootOffset, -(bottom_Rect(rect) - visRoot.y), 250); | 2256 | } |
1939 | // } | 2257 | else { |
1940 | // } | 2258 | setFocus_Widget(NULL); /* stop editing */ |
1941 | // return iFalse; | 2259 | } |
1942 | // } | 2260 | } |
1943 | else if (isCommand_UserEvent(ev, "text.insert")) { | 2261 | return iFalse; |
1944 | pushUndo_InputWidget_(d); | ||
1945 | deleteMarked_InputWidget_(d); | ||
1946 | insertChar_InputWidget_(d, arg_Command(command_UserEvent(ev))); | ||
1947 | contentsWereChanged_InputWidget_(d); | ||
1948 | return iTrue; | ||
1949 | } | 2262 | } |
1950 | else if (isCommand_Widget(w, ev, "input.backup")) { | 2263 | else if (isCommand_Widget(w, ev, "input.backup")) { |
1951 | if (d->inFlags & needBackup_InputWidgetFlag) { | 2264 | if (d->inFlags & needBackup_InputWidgetFlag) { |
@@ -1957,10 +2270,7 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { | |||
1957 | updateMetrics_InputWidget_(d); | 2270 | updateMetrics_InputWidget_(d); |
1958 | // updateLinesAndResize_InputWidget_(d); | 2271 | // updateLinesAndResize_InputWidget_(d); |
1959 | } | 2272 | } |
1960 | else if (isFocused_Widget(d) && isCommand_UserEvent(ev, "copy")) { | 2273 | #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT |
1961 | copy_InputWidget_(d, iFalse); | ||
1962 | return iTrue; | ||
1963 | } | ||
1964 | if (ev->type == SDL_MOUSEWHEEL && contains_Widget(w, coord_MouseWheelEvent(&ev->wheel))) { | 2274 | if (ev->type == SDL_MOUSEWHEEL && contains_Widget(w, coord_MouseWheelEvent(&ev->wheel))) { |
1965 | if (numWrapLines_InputWidget_(d) <= size_Range(&d->visWrapLines)) { | 2275 | if (numWrapLines_InputWidget_(d) <= size_Range(&d->visWrapLines)) { |
1966 | return ignored_EventResult; | 2276 | return ignored_EventResult; |
@@ -1994,6 +2304,17 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { | |||
1994 | } | 2304 | } |
1995 | return false_EventResult; | 2305 | return false_EventResult; |
1996 | } | 2306 | } |
2307 | if (ev->type == SDL_TEXTINPUT && isFocused_Widget(w)) { | ||
2308 | pushUndo_InputWidget_(d); | ||
2309 | deleteMarked_InputWidget_(d); | ||
2310 | insertRange_InputWidget_(d, range_CStr(ev->text.text)); | ||
2311 | contentsWereChanged_InputWidget_(d); | ||
2312 | return iTrue; | ||
2313 | } | ||
2314 | const iInt2 curMax = cursorMax_InputWidget_(d); | ||
2315 | const iInt2 lineFirst = init_I2(0, d->cursor.y); | ||
2316 | const iInt2 lineLast = init_I2(endX_InputWidget_(d, d->cursor.y), d->cursor.y); | ||
2317 | #endif | ||
1997 | /* Click behavior depends on device type. */ { | 2318 | /* Click behavior depends on device type. */ { |
1998 | const int mbResult = (deviceType_App() == desktop_AppDeviceType | 2319 | const int mbResult = (deviceType_App() == desktop_AppDeviceType |
1999 | ? processPointerEvents_InputWidget_(d, ev) | 2320 | ? processPointerEvents_InputWidget_(d, ev) |
@@ -2005,12 +2326,10 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { | |||
2005 | if (ev->type == SDL_KEYUP && isFocused_Widget(w)) { | 2326 | if (ev->type == SDL_KEYUP && isFocused_Widget(w)) { |
2006 | return iTrue; | 2327 | return iTrue; |
2007 | } | 2328 | } |
2008 | const iInt2 curMax = cursorMax_InputWidget_(d); | ||
2009 | const iInt2 lineFirst = init_I2(0, d->cursor.y); | ||
2010 | const iInt2 lineLast = init_I2(endX_InputWidget_(d, d->cursor.y), d->cursor.y); | ||
2011 | if (ev->type == SDL_KEYDOWN && isFocused_Widget(w)) { | 2329 | if (ev->type == SDL_KEYDOWN && isFocused_Widget(w)) { |
2012 | const int key = ev->key.keysym.sym; | 2330 | const int key = ev->key.keysym.sym; |
2013 | const int mods = keyMods_Sym(ev->key.keysym.mod); | 2331 | const int mods = keyMods_Sym(ev->key.keysym.mod); |
2332 | #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT | ||
2014 | if (mods == KMOD_PRIMARY) { | 2333 | if (mods == KMOD_PRIMARY) { |
2015 | switch (key) { | 2334 | switch (key) { |
2016 | case 'c': | 2335 | case 'c': |
@@ -2028,7 +2347,7 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { | |||
2028 | return iTrue; | 2347 | return iTrue; |
2029 | } | 2348 | } |
2030 | } | 2349 | } |
2031 | #if defined (iPlatformApple) | 2350 | # if defined (iPlatformApple) |
2032 | if (mods == KMOD_PRIMARY || mods == (KMOD_PRIMARY | KMOD_SHIFT)) { | 2351 | if (mods == KMOD_PRIMARY || mods == (KMOD_PRIMARY | KMOD_SHIFT)) { |
2033 | switch (key) { | 2352 | switch (key) { |
2034 | case SDLK_UP: | 2353 | case SDLK_UP: |
@@ -2038,19 +2357,14 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { | |||
2038 | return iTrue; | 2357 | return iTrue; |
2039 | } | 2358 | } |
2040 | } | 2359 | } |
2041 | #endif | 2360 | # endif |
2042 | d->prevCursor = d->cursor; | 2361 | d->prevCursor = d->cursor; |
2362 | #endif | ||
2043 | switch (key) { | 2363 | switch (key) { |
2044 | case SDLK_INSERT: | ||
2045 | if (mods == KMOD_SHIFT) { | ||
2046 | paste_InputWidget_(d); | ||
2047 | } | ||
2048 | return iTrue; | ||
2049 | case SDLK_RETURN: | 2364 | case SDLK_RETURN: |
2050 | case SDLK_KP_ENTER: | 2365 | case SDLK_KP_ENTER: |
2051 | if (~d->inFlags & isSensitive_InputWidgetFlag && | 2366 | #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT |
2052 | ~d->inFlags & isUrl_InputWidgetFlag && | 2367 | if (isAllowedToInsertNewline_InputWidget_(d)) { |
2053 | d->inFlags & lineBreaksEnabled_InputWidgetFlag && d->maxLen == 0) { | ||
2054 | if (checkLineBreakMods_InputWidget_(d, mods)) { | 2368 | if (checkLineBreakMods_InputWidget_(d, mods)) { |
2055 | pushUndo_InputWidget_(d); | 2369 | pushUndo_InputWidget_(d); |
2056 | deleteMarked_InputWidget_(d); | 2370 | deleteMarked_InputWidget_(d); |
@@ -2059,6 +2373,7 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { | |||
2059 | return iTrue; | 2373 | return iTrue; |
2060 | } | 2374 | } |
2061 | } | 2375 | } |
2376 | #endif | ||
2062 | if (d->inFlags & enterKeyEnabled_InputWidgetFlag && | 2377 | if (d->inFlags & enterKeyEnabled_InputWidgetFlag && |
2063 | (checkAcceptMods_InputWidget_(d, mods) || | 2378 | (checkAcceptMods_InputWidget_(d, mods) || |
2064 | (~d->inFlags & lineBreaksEnabled_InputWidgetFlag))) { | 2379 | (~d->inFlags & lineBreaksEnabled_InputWidgetFlag))) { |
@@ -2071,6 +2386,12 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { | |||
2071 | end_InputWidget(d, iTrue); | 2386 | end_InputWidget(d, iTrue); |
2072 | setFocus_Widget(NULL); | 2387 | setFocus_Widget(NULL); |
2073 | return (d->inFlags & eatEscape_InputWidgetFlag) != 0; | 2388 | return (d->inFlags & eatEscape_InputWidgetFlag) != 0; |
2389 | #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT | ||
2390 | case SDLK_INSERT: | ||
2391 | if (mods == KMOD_SHIFT) { | ||
2392 | paste_InputWidget_(d); | ||
2393 | } | ||
2394 | return iTrue; | ||
2074 | case SDLK_BACKSPACE: | 2395 | case SDLK_BACKSPACE: |
2075 | if (!isEmpty_Range(&d->mark)) { | 2396 | if (!isEmpty_Range(&d->mark)) { |
2076 | pushUndo_InputWidget_(d); | 2397 | pushUndo_InputWidget_(d); |
@@ -2170,7 +2491,7 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { | |||
2170 | refresh_Widget(w); | 2491 | refresh_Widget(w); |
2171 | return iTrue; | 2492 | return iTrue; |
2172 | } | 2493 | } |
2173 | #if defined (iPlatformApple) | 2494 | # if defined (iPlatformApple) |
2174 | /* fall through for Emacs-style Home/End */ | 2495 | /* fall through for Emacs-style Home/End */ |
2175 | case SDLK_e: | 2496 | case SDLK_e: |
2176 | if (mods == KMOD_CTRL || mods == (KMOD_CTRL | KMOD_SHIFT)) { | 2497 | if (mods == KMOD_CTRL || mods == (KMOD_CTRL | KMOD_SHIFT)) { |
@@ -2178,7 +2499,7 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { | |||
2178 | refresh_Widget(w); | 2499 | refresh_Widget(w); |
2179 | return iTrue; | 2500 | return iTrue; |
2180 | } | 2501 | } |
2181 | #endif | 2502 | # endif |
2182 | break; | 2503 | break; |
2183 | case SDLK_LEFT: | 2504 | case SDLK_LEFT: |
2184 | case SDLK_RIGHT: { | 2505 | case SDLK_RIGHT: { |
@@ -2228,22 +2549,17 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { | |||
2228 | } | 2549 | } |
2229 | refresh_Widget(d); | 2550 | refresh_Widget(d); |
2230 | return iTrue; | 2551 | return iTrue; |
2552 | #endif | ||
2231 | } | 2553 | } |
2232 | if (mods & (KMOD_PRIMARY | KMOD_SECONDARY)) { | 2554 | if (mods & (KMOD_PRIMARY | KMOD_SECONDARY)) { |
2233 | return iFalse; | 2555 | return iFalse; |
2234 | } | 2556 | } |
2235 | return iTrue; | 2557 | return iTrue; |
2236 | } | 2558 | } |
2237 | else if (ev->type == SDL_TEXTINPUT && isFocused_Widget(w)) { | ||
2238 | pushUndo_InputWidget_(d); | ||
2239 | deleteMarked_InputWidget_(d); | ||
2240 | insertRange_InputWidget_(d, range_CStr(ev->text.text)); | ||
2241 | contentsWereChanged_InputWidget_(d); | ||
2242 | return iTrue; | ||
2243 | } | ||
2244 | return processEvent_Widget(w, ev); | 2559 | return processEvent_Widget(w, ev); |
2245 | } | 2560 | } |
2246 | 2561 | ||
2562 | #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT | ||
2247 | iDeclareType(MarkPainter) | 2563 | iDeclareType(MarkPainter) |
2248 | 2564 | ||
2249 | struct Impl_MarkPainter { | 2565 | struct Impl_MarkPainter { |
@@ -2302,6 +2618,7 @@ static iBool draw_MarkPainter_(iWrapText *wrapText, iRangecc wrappedText, iTextA | |||
2302 | } | 2618 | } |
2303 | return iTrue; | 2619 | return iTrue; |
2304 | } | 2620 | } |
2621 | #endif | ||
2305 | 2622 | ||
2306 | static void draw_InputWidget_(const iInputWidget *d) { | 2623 | static void draw_InputWidget_(const iInputWidget *d) { |
2307 | const iWidget *w = constAs_Widget(d); | 2624 | const iWidget *w = constAs_Widget(d); |
@@ -2324,13 +2641,22 @@ static void draw_InputWidget_(const iInputWidget *d) { | |||
2324 | isFocused ? gap_UI / 4 : 1, | 2641 | isFocused ? gap_UI / 4 : 1, |
2325 | isFocused ? uiInputFrameFocused_ColorId | 2642 | isFocused ? uiInputFrameFocused_ColorId |
2326 | : isHover ? uiInputFrameHover_ColorId : uiInputFrame_ColorId); | 2643 | : isHover ? uiInputFrameHover_ColorId : uiInputFrame_ColorId); |
2327 | setClip_Paint(&p, adjusted_Rect(bounds, init_I2(d->leftPadding, 0), | 2644 | if (d->sysCtrl) { |
2328 | init_I2(-d->rightPadding, w->flags & extraPadding_WidgetFlag ? -gap_UI / 2 : 0))); | 2645 | /* The system-provided control is drawing the text. */ |
2646 | drawChildren_Widget(w); | ||
2647 | return; | ||
2648 | } | ||
2329 | const iRect contentBounds = contentBounds_InputWidget_(d); | 2649 | const iRect contentBounds = contentBounds_InputWidget_(d); |
2330 | iInt2 drawPos = topLeft_Rect(contentBounds); | 2650 | iInt2 drawPos = topLeft_Rect(contentBounds); |
2331 | const int fg = isHint ? uiAnnotation_ColorId | 2651 | const int fg = isHint ? uiAnnotation_ColorId |
2332 | : isFocused /*&& !isEmpty_Array(&d->lines)*/ ? uiInputTextFocused_ColorId | 2652 | : isFocused ? uiInputTextFocused_ColorId |
2333 | : uiInputText_ColorId; | 2653 | : uiInputText_ColorId; |
2654 | #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT | ||
2655 | setClip_Paint(&p, | ||
2656 | adjusted_Rect(bounds, | ||
2657 | init_I2(d->leftPadding, 0), | ||
2658 | init_I2(-d->rightPadding, | ||
2659 | w->flags & extraPadding_WidgetFlag ? -gap_UI / 2 : 0))); | ||
2334 | iWrapText wrapText = { | 2660 | iWrapText wrapText = { |
2335 | .maxWidth = d->maxLen == 0 ? width_Rect(contentBounds) : unlimitedWidth_InputWidget_, | 2661 | .maxWidth = d->maxLen == 0 ? width_Rect(contentBounds) : unlimitedWidth_InputWidget_, |
2336 | .mode = (d->inFlags & isUrl_InputWidgetFlag ? anyCharacter_WrapTextMode | 2662 | .mode = (d->inFlags & isUrl_InputWidgetFlag ? anyCharacter_WrapTextMode |
@@ -2338,16 +2664,37 @@ static void draw_InputWidget_(const iInputWidget *d) { | |||
2338 | .overrideChar = (d->inFlags & isSensitive_InputWidgetFlag ? sensitiveChar_ : 0), | 2664 | .overrideChar = (d->inFlags & isSensitive_InputWidgetFlag ? sensitiveChar_ : 0), |
2339 | }; | 2665 | }; |
2340 | const iRangei visLines = visibleLineRange_InputWidget_(d); | 2666 | const iRangei visLines = visibleLineRange_InputWidget_(d); |
2341 | const int visLineOffsetY = visLineOffsetY_InputWidget_(d); | ||
2342 | iRect markerRects[2] = { zero_Rect(), zero_Rect() }; | 2667 | iRect markerRects[2] = { zero_Rect(), zero_Rect() }; |
2668 | #endif | ||
2669 | const int visLineOffsetY = visLineOffsetY_InputWidget_(d); | ||
2343 | /* If buffered, just draw the buffered copy. */ | 2670 | /* If buffered, just draw the buffered copy. */ |
2344 | if (d->buffered && !isFocused) { | 2671 | if (d->buffered && !isFocused) { |
2345 | /* Most input widgets will use this, since only one is focused at a time. */ | 2672 | /* Most input widgets will use this, since only one is focused at a time. */ |
2346 | draw_TextBuf(d->buffered, addY_I2(drawPos, visLineOffsetY), white_ColorId); | 2673 | if (flags_Widget(w) & alignRight_WidgetFlag) { |
2674 | draw_TextBuf( | ||
2675 | d->buffered, | ||
2676 | addY_I2(init_I2(right_Rect(contentBounds) - d->buffered->size.x, drawPos.y), | ||
2677 | visLineOffsetY), | ||
2678 | white_ColorId); | ||
2679 | } | ||
2680 | else { | ||
2681 | draw_TextBuf(d->buffered, addY_I2(drawPos, visLineOffsetY), white_ColorId); | ||
2682 | } | ||
2347 | } | 2683 | } |
2348 | else if (isHint) { | 2684 | else if (isHint) { |
2349 | drawRange_Text(d->font, drawPos, uiAnnotation_ColorId, range_String(&d->hint)); | 2685 | if (flags_Widget(w) & alignRight_WidgetFlag) { |
2686 | drawAlign_Text(d->font, | ||
2687 | init_I2(right_Rect(contentBounds), drawPos.y), | ||
2688 | uiAnnotation_ColorId, | ||
2689 | right_Alignment, | ||
2690 | "%s", | ||
2691 | cstr_String(&d->hint)); | ||
2692 | } | ||
2693 | else { | ||
2694 | drawRange_Text(d->font, drawPos, uiAnnotation_ColorId, range_String(&d->hint)); | ||
2695 | } | ||
2350 | } | 2696 | } |
2697 | #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT | ||
2351 | else { | 2698 | else { |
2352 | iAssert(~d->inFlags & isSensitive_InputWidgetFlag || size_Range(&visLines) == 1); | 2699 | iAssert(~d->inFlags & isSensitive_InputWidgetFlag || size_Range(&visLines) == 1); |
2353 | drawPos.y += visLineOffsetY; | 2700 | drawPos.y += visLineOffsetY; |
@@ -2372,7 +2719,8 @@ static void draw_InputWidget_(const iInputWidget *d) { | |||
2372 | wrapText.context = NULL; | 2719 | wrapText.context = NULL; |
2373 | } | 2720 | } |
2374 | /* Draw the insertion point. */ | 2721 | /* Draw the insertion point. */ |
2375 | if (isFocused && d->cursorVis && contains_Range(&visLines, d->cursor.y) && | 2722 | if (isFocused && (d->cursorVis || !prefs_App()->blinkingCursor) && |
2723 | contains_Range(&visLines, d->cursor.y) && | ||
2376 | (deviceType_App() == desktop_AppDeviceType || isEmpty_Range(&d->mark))) { | 2724 | (deviceType_App() == desktop_AppDeviceType || isEmpty_Range(&d->mark))) { |
2377 | iInt2 curSize; | 2725 | iInt2 curSize; |
2378 | iRangecc cursorChar = iNullRange; | 2726 | iRangecc cursorChar = iNullRange; |
@@ -2426,6 +2774,7 @@ static void draw_InputWidget_(const iInputWidget *d) { | |||
2426 | drawPin_Paint(&p, markerRects[i], i, uiTextCaution_ColorId); | 2774 | drawPin_Paint(&p, markerRects[i], i, uiTextCaution_ColorId); |
2427 | } | 2775 | } |
2428 | } | 2776 | } |
2777 | #endif | ||
2429 | drawChildren_Widget(w); | 2778 | drawChildren_Widget(w); |
2430 | } | 2779 | } |
2431 | 2780 | ||
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); | |||
57 | void begin_InputWidget (iInputWidget *); | 57 | void begin_InputWidget (iInputWidget *); |
58 | void end_InputWidget (iInputWidget *, iBool accept); | 58 | void end_InputWidget (iInputWidget *, iBool accept); |
59 | void selectAll_InputWidget (iInputWidget *); | 59 | void selectAll_InputWidget (iInputWidget *); |
60 | void validate_InputWidget (iInputWidget *); | ||
60 | 61 | ||
61 | void setSelectAllOnFocus_InputWidget (iInputWidget *, iBool selectAllOnFocus); | 62 | void setSelectAllOnFocus_InputWidget (iInputWidget *, iBool selectAllOnFocus); |
62 | void setSensitiveContent_InputWidget (iInputWidget *, iBool isSensitive); | 63 | void setSensitiveContent_InputWidget (iInputWidget *, iBool isSensitive); |
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_[] = | |||
240 | { 100,{ "${keys.hoverurl}", '/', KMOD_PRIMARY, "prefs.hoverlink.toggle" }, 0 }, | 240 | { 100,{ "${keys.hoverurl}", '/', KMOD_PRIMARY, "prefs.hoverlink.toggle" }, 0 }, |
241 | { 110,{ "${menu.save.downloads}", SDLK_s, KMOD_PRIMARY, "document.save" }, 0 }, | 241 | { 110,{ "${menu.save.downloads}", SDLK_s, KMOD_PRIMARY, "document.save" }, 0 }, |
242 | { 120,{ "${keys.upload}", SDLK_u, KMOD_PRIMARY, "document.upload" }, 0 }, | 242 | { 120,{ "${keys.upload}", SDLK_u, KMOD_PRIMARY, "document.upload" }, 0 }, |
243 | { 121,{ "${keys.upload.edit}", SDLK_e, KMOD_PRIMARY, "document.upload copy:1" }, 0 }, | ||
243 | /* The following cannot currently be changed (built-in duplicates). */ | 244 | /* The following cannot currently be changed (built-in duplicates). */ |
244 | #if defined (iPlatformApple) | 245 | #if defined (iPlatformApple) |
245 | { 1002, { NULL, SDLK_LEFTBRACKET, KMOD_PRIMARY, "navigate.back" }, 0 }, | 246 | { 1002, { NULL, SDLK_LEFTBRACKET, KMOD_PRIMARY, "navigate.back" }, 0 }, |
diff --git a/src/ui/labelwidget.c b/src/ui/labelwidget.c index 4dd66a28..3454014a 100644 --- a/src/ui/labelwidget.c +++ b/src/ui/labelwidget.c | |||
@@ -36,6 +36,7 @@ struct Impl_LabelWidget { | |||
36 | iWidget widget; | 36 | iWidget widget; |
37 | iString srcLabel; | 37 | iString srcLabel; |
38 | iString label; | 38 | iString label; |
39 | iInt2 labelOffset; | ||
39 | int font; | 40 | int font; |
40 | int key; | 41 | int key; |
41 | int kmods; | 42 | int kmods; |
@@ -44,14 +45,15 @@ struct Impl_LabelWidget { | |||
44 | iString command; | 45 | iString command; |
45 | iClick click; | 46 | iClick click; |
46 | struct { | 47 | struct { |
47 | uint8_t alignVisual : 1; /* align according to visible bounds, not font metrics */ | 48 | uint16_t alignVisual : 1; /* align according to visible bounds, not font metrics */ |
48 | uint8_t noAutoMinHeight : 1; /* minimum height is not set automatically */ | 49 | uint16_t noAutoMinHeight : 1; /* minimum height is not set automatically */ |
49 | uint8_t drawAsOutline : 1; /* draw as outline, filled with background color */ | 50 | uint16_t drawAsOutline : 1; /* draw as outline, filled with background color */ |
50 | uint8_t noTopFrame : 1; | 51 | uint16_t noTopFrame : 1; |
51 | uint8_t wrap : 1; | 52 | uint16_t wrap : 1; |
52 | uint8_t allCaps : 1; | 53 | uint16_t allCaps : 1; |
53 | uint8_t removeTrailingColon : 1; | 54 | uint16_t removeTrailingColon : 1; |
54 | uint8_t chevron : 1; | 55 | uint16_t chevron : 1; |
56 | uint16_t checkMark : 1; | ||
55 | } flags; | 57 | } flags; |
56 | }; | 58 | }; |
57 | 59 | ||
@@ -132,6 +134,10 @@ static iBool processEvent_LabelWidget_(iLabelWidget *d, const SDL_Event *ev) { | |||
132 | refresh_Widget(d); | 134 | refresh_Widget(d); |
133 | return iFalse; | 135 | return iFalse; |
134 | } | 136 | } |
137 | else if (isCommand_Widget(w, ev, "trigger")) { | ||
138 | trigger_LabelWidget_(d); | ||
139 | return iTrue; | ||
140 | } | ||
135 | if (!isEmpty_String(&d->command)) { | 141 | if (!isEmpty_String(&d->command)) { |
136 | #if 0 && defined (iPlatformAppleMobile) | 142 | #if 0 && defined (iPlatformAppleMobile) |
137 | /* Touch allows activating any button on release. */ | 143 | /* Touch allows activating any button on release. */ |
@@ -193,6 +199,7 @@ static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int | |||
193 | int *icon, int *meta) { | 199 | int *icon, int *meta) { |
194 | const iWidget *w = constAs_Widget(d); | 200 | const iWidget *w = constAs_Widget(d); |
195 | const int64_t flags = flags_Widget(w); | 201 | const int64_t flags = flags_Widget(w); |
202 | const iBool isHover = isHover_LabelWidget_(d); | ||
196 | const iBool isFocus = (flags & focusable_WidgetFlag && isFocused_Widget(d)); | 203 | const iBool isFocus = (flags & focusable_WidgetFlag && isFocused_Widget(d)); |
197 | const iBool isPress = (flags & pressed_WidgetFlag) != 0; | 204 | const iBool isPress = (flags & pressed_WidgetFlag) != 0; |
198 | const iBool isSel = (flags & selected_WidgetFlag) != 0; | 205 | const iBool isSel = (flags & selected_WidgetFlag) != 0; |
@@ -205,6 +212,9 @@ static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int | |||
205 | *bg = isButton && ~flags & noBackground_WidgetFlag ? (d->widget.bgColor != none_ColorId ? | 212 | *bg = isButton && ~flags & noBackground_WidgetFlag ? (d->widget.bgColor != none_ColorId ? |
206 | d->widget.bgColor : uiBackground_ColorId) | 213 | d->widget.bgColor : uiBackground_ColorId) |
207 | : none_ColorId; | 214 | : none_ColorId; |
215 | if (d->flags.checkMark) { | ||
216 | *bg = none_ColorId; | ||
217 | } | ||
208 | *fg = uiText_ColorId; | 218 | *fg = uiText_ColorId; |
209 | *frame1 = isButton ? uiEmboss1_ColorId : d->widget.frameColor; | 219 | *frame1 = isButton ? uiEmboss1_ColorId : d->widget.frameColor; |
210 | *frame2 = isButton ? uiEmboss2_ColorId : *frame1; | 220 | *frame2 = isButton ? uiEmboss2_ColorId : *frame1; |
@@ -216,18 +226,17 @@ static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int | |||
216 | *meta = uiTextDisabled_ColorId; | 226 | *meta = uiTextDisabled_ColorId; |
217 | } | 227 | } |
218 | if (isSel) { | 228 | if (isSel) { |
219 | if (isMenuItem) { | 229 | if (!d->flags.checkMark) { |
220 | *bg = uiBackgroundUnfocusedSelection_ColorId; | 230 | if (isMenuItem) { |
221 | } | 231 | *bg = uiBackgroundUnfocusedSelection_ColorId; |
222 | else { | 232 | } |
223 | *bg = uiBackgroundSelected_ColorId; | 233 | else { |
224 | } | 234 | *bg = uiBackgroundSelected_ColorId; |
225 | // if (!isKeyRoot) { | 235 | } |
226 | // *bg = uiEmbossSelected1_ColorId; //uiBackgroundUnfocusedSelection_ColorId; | 236 | if (!isKeyRoot) { |
227 | // } | 237 | *bg = isDark_ColorTheme(colorTheme_App()) ? uiBackgroundUnfocusedSelection_ColorId |
228 | if (!isKeyRoot) { | 238 | : uiMarked_ColorId; |
229 | *bg = isDark_ColorTheme(colorTheme_App()) ? uiBackgroundUnfocusedSelection_ColorId | 239 | } |
230 | : uiMarked_ColorId ; | ||
231 | } | 240 | } |
232 | *fg = uiTextSelected_ColorId; | 241 | *fg = uiTextSelected_ColorId; |
233 | if (isButton) { | 242 | if (isButton) { |
@@ -248,7 +257,7 @@ static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int | |||
248 | if (colorEscape == uiTextCaution_ColorId) { | 257 | if (colorEscape == uiTextCaution_ColorId) { |
249 | *icon = *meta = colorEscape; | 258 | *icon = *meta = colorEscape; |
250 | } | 259 | } |
251 | if (isHover_LabelWidget_(d)) { | 260 | if (isHover) { |
252 | if (isFrameless) { | 261 | if (isFrameless) { |
253 | *bg = uiBackgroundFramelessHover_ColorId; | 262 | *bg = uiBackgroundFramelessHover_ColorId; |
254 | *fg = uiTextFramelessHover_ColorId; | 263 | *fg = uiTextFramelessHover_ColorId; |
@@ -274,7 +283,7 @@ static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int | |||
274 | } | 283 | } |
275 | } | 284 | } |
276 | if (d->forceFg >= 0) { | 285 | if (d->forceFg >= 0) { |
277 | *fg = /* *icon = */ *meta = d->forceFg; | 286 | *fg = *meta = d->forceFg; |
278 | } | 287 | } |
279 | if (isPress) { | 288 | if (isPress) { |
280 | if (colorEscape == uiTextAction_ColorId || colorEscape == uiTextCaution_ColorId) { | 289 | if (colorEscape == uiTextAction_ColorId || colorEscape == uiTextCaution_ColorId) { |
@@ -289,13 +298,12 @@ static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int | |||
289 | *frame1 = uiEmbossPressed1_ColorId; | 298 | *frame1 = uiEmbossPressed1_ColorId; |
290 | *frame2 = colorEscape != none_ColorId ? colorEscape : uiEmbossPressed2_ColorId; | 299 | *frame2 = colorEscape != none_ColorId ? colorEscape : uiEmbossPressed2_ColorId; |
291 | } | 300 | } |
292 | //if (colorEscape == none_ColorId || colorEscape == uiTextAction_ColorId) { | ||
293 | *fg = *icon = *meta = uiTextPressed_ColorId | permanent_ColorId; | 301 | *fg = *icon = *meta = uiTextPressed_ColorId | permanent_ColorId; |
294 | // } | ||
295 | // else { | ||
296 | // *fg = (isDark_ColorTheme(colorTheme_App()) ? white_ColorId : black_ColorId) | permanent_ColorId; | ||
297 | // } | ||
298 | } | 302 | } |
303 | } | ||
304 | if (((isSel || isHover) && isFrameless) || isPress) { | ||
305 | /* Ensure that the full label text remains readable. */ | ||
306 | *fg |= permanent_ColorId; | ||
299 | } | 307 | } |
300 | } | 308 | } |
301 | 309 | ||
@@ -328,6 +336,7 @@ static void draw_LabelWidget_(const iLabelWidget *d) { | |||
328 | init_Paint(&p); | 336 | init_Paint(&p); |
329 | int bg, fg, frame, frame2, iconColor, metaColor; | 337 | int bg, fg, frame, frame2, iconColor, metaColor; |
330 | getColors_LabelWidget_(d, &bg, &fg, &frame, &frame2, &iconColor, &metaColor); | 338 | getColors_LabelWidget_(d, &bg, &fg, &frame, &frame2, &iconColor, &metaColor); |
339 | setBaseAttributes_Text(d->font, fg); | ||
331 | const enum iColorId colorEscape = parseEscape_Color(cstr_String(&d->label), NULL); | 340 | const enum iColorId colorEscape = parseEscape_Color(cstr_String(&d->label), NULL); |
332 | const iBool isCaution = (colorEscape == uiTextCaution_ColorId); | 341 | const iBool isCaution = (colorEscape == uiTextCaution_ColorId); |
333 | if (bg >= 0) { | 342 | if (bg >= 0) { |
@@ -347,7 +356,7 @@ static void draw_LabelWidget_(const iLabelWidget *d) { | |||
347 | bottomRight_Rect(frameRect), | 356 | bottomRight_Rect(frameRect), |
348 | bottomLeft_Rect(frameRect) | 357 | bottomLeft_Rect(frameRect) |
349 | }; | 358 | }; |
350 | #if SDL_VERSION_ATLEAST(2, 0, 16) | 359 | #if SDL_COMPILEDVERSION == SDL_VERSIONNUM(2, 0, 16) |
351 | if (isOpenGLRenderer_Window()) { | 360 | if (isOpenGLRenderer_Window()) { |
352 | /* A very curious regression in SDL 2.0.16. */ | 361 | /* A very curious regression in SDL 2.0.16. */ |
353 | points[3].x--; | 362 | points[3].x--; |
@@ -362,10 +371,6 @@ static void draw_LabelWidget_(const iLabelWidget *d) { | |||
362 | } | 371 | } |
363 | setClip_Paint(&p, rect); | 372 | setClip_Paint(&p, rect); |
364 | const int iconPad = iconPadding_LabelWidget_(d); | 373 | const int iconPad = iconPadding_LabelWidget_(d); |
365 | // const int iconColor = isCaution ? uiTextCaution_ColorId | ||
366 | // : flags & (disabled_WidgetFlag | pressed_WidgetFlag) ? fg | ||
367 | // : isHover ? uiIconHover_ColorId | ||
368 | // : uiIcon_ColorId; | ||
369 | if (d->icon && d->icon != 0x20) { /* no need to draw an empty icon */ | 374 | if (d->icon && d->icon != 0x20) { /* no need to draw an empty icon */ |
370 | iString str; | 375 | iString str; |
371 | initUnicodeN_String(&str, &d->icon, 1); | 376 | initUnicodeN_String(&str, &d->icon, 1); |
@@ -427,22 +432,35 @@ static void draw_LabelWidget_(const iLabelWidget *d) { | |||
427 | else { | 432 | else { |
428 | drawCenteredOutline_Text( | 433 | drawCenteredOutline_Text( |
429 | d->font, | 434 | d->font, |
430 | adjusted_Rect(bounds, init_I2(iconPad * (flags & tight_WidgetFlag ? 1.0f : 1.5f), 0), | 435 | moved_Rect( |
436 | adjusted_Rect(bounds, | ||
437 | init_I2(iconPad * (flags & tight_WidgetFlag ? 1.0f : 1.5f), 0), | ||
431 | init_I2(-iconPad * (flags & tight_WidgetFlag ? 0.5f : 1.0f), 0)), | 438 | init_I2(-iconPad * (flags & tight_WidgetFlag ? 0.5f : 1.0f), 0)), |
439 | d->labelOffset), | ||
432 | d->flags.alignVisual, | 440 | d->flags.alignVisual, |
433 | d->flags.drawAsOutline ? fg : none_ColorId, | 441 | d->flags.drawAsOutline ? fg : none_ColorId, |
434 | d->flags.drawAsOutline ? d->widget.bgColor : fg, | 442 | d->flags.drawAsOutline ? d->widget.bgColor : fg, |
435 | "%s", | 443 | "%s", |
436 | cstr_String(&d->label)); | 444 | cstr_String(&d->label)); |
437 | } | 445 | } |
438 | if (d->flags.chevron) { | 446 | if (d->flags.chevron || (flags & selected_WidgetFlag && d->flags.checkMark)) { |
439 | const iRect chRect = rect; | 447 | const iRect chRect = rect; |
440 | const int chSize = lineHeight_Text(d->font); | 448 | const int chSize = lineHeight_Text(d->font); |
449 | int offset = 0; | ||
450 | if (d->flags.chevron) { | ||
451 | offset = -iconPad; | ||
452 | } | ||
453 | else { | ||
454 | offset = -10 * gap_UI; | ||
455 | } | ||
441 | drawCentered_Text(d->font, | 456 | drawCentered_Text(d->font, |
442 | (iRect){ addX_I2(topRight_Rect(chRect), -iconPad), | 457 | (iRect){ addX_I2(topRight_Rect(chRect), offset), |
443 | init_I2(chSize, height_Rect(chRect)) }, | 458 | init_I2(chSize, height_Rect(chRect)) }, |
444 | iTrue, iconColor, rightAngle_Icon); | 459 | iTrue, |
460 | iconColor, | ||
461 | d->flags.chevron ? rightAngle_Icon : check_Icon); | ||
445 | } | 462 | } |
463 | setBaseAttributes_Text(-1, -1); | ||
446 | unsetClip_Paint(&p); | 464 | unsetClip_Paint(&p); |
447 | drawChildren_Widget(w); | 465 | drawChildren_Widget(w); |
448 | } | 466 | } |
@@ -482,9 +500,10 @@ int font_LabelWidget(const iLabelWidget *d) { | |||
482 | } | 500 | } |
483 | 501 | ||
484 | void updateSize_LabelWidget(iLabelWidget *d) { | 502 | void updateSize_LabelWidget(iLabelWidget *d) { |
485 | iWidget *w = as_Widget(d); | 503 | if (!d) return; |
504 | iWidget *w = as_Widget(d); | ||
486 | const int64_t flags = flags_Widget(w); | 505 | const int64_t flags = flags_Widget(w); |
487 | const iInt2 size = defaultSize_LabelWidget(d); | 506 | const iInt2 size = defaultSize_LabelWidget(d); |
488 | if (!d->flags.noAutoMinHeight) { | 507 | if (!d->flags.noAutoMinHeight) { |
489 | w->minSize.y = size.y; /* vertically text must remain visible */ | 508 | w->minSize.y = size.y; /* vertically text must remain visible */ |
490 | } | 509 | } |
@@ -514,6 +533,7 @@ void init_LabelWidget(iLabelWidget *d, const char *label, const char *cmd) { | |||
514 | d->font = uiLabel_FontId; | 533 | d->font = uiLabel_FontId; |
515 | d->forceFg = none_ColorId; | 534 | d->forceFg = none_ColorId; |
516 | d->icon = 0; | 535 | d->icon = 0; |
536 | d->labelOffset = zero_I2(); | ||
517 | initCStr_String(&d->srcLabel, label); | 537 | initCStr_String(&d->srcLabel, label); |
518 | initCopy_String(&d->label, &d->srcLabel); | 538 | initCopy_String(&d->label, &d->srcLabel); |
519 | replaceVariables_LabelWidget_(d); | 539 | replaceVariables_LabelWidget_(d); |
@@ -551,18 +571,22 @@ void setTextColor_LabelWidget(iLabelWidget *d, int color) { | |||
551 | } | 571 | } |
552 | 572 | ||
553 | void setText_LabelWidget(iLabelWidget *d, const iString *text) { | 573 | void setText_LabelWidget(iLabelWidget *d, const iString *text) { |
574 | if (d) { | ||
554 | updateText_LabelWidget(d, text); | 575 | updateText_LabelWidget(d, text); |
555 | updateSize_LabelWidget(d); | 576 | updateSize_LabelWidget(d); |
556 | if (isWrapped_LabelWidget(d)) { | 577 | if (isWrapped_LabelWidget(d)) { |
557 | sizeChanged_LabelWidget_(d); | 578 | sizeChanged_LabelWidget_(d); |
579 | } | ||
558 | } | 580 | } |
559 | } | 581 | } |
560 | 582 | ||
561 | void setTextCStr_LabelWidget(iLabelWidget *d, const char *text) { | 583 | void setTextCStr_LabelWidget(iLabelWidget *d, const char *text) { |
584 | if (d) { | ||
562 | updateTextCStr_LabelWidget(d, text); | 585 | updateTextCStr_LabelWidget(d, text); |
563 | updateSize_LabelWidget(d); | 586 | updateSize_LabelWidget(d); |
564 | if (isWrapped_LabelWidget(d)) { | 587 | if (isWrapped_LabelWidget(d)) { |
565 | sizeChanged_LabelWidget_(d); | 588 | sizeChanged_LabelWidget_(d); |
589 | } | ||
566 | } | 590 | } |
567 | } | 591 | } |
568 | 592 | ||
@@ -586,6 +610,10 @@ void setChevron_LabelWidget(iLabelWidget *d, iBool chevron) { | |||
586 | d->flags.chevron = chevron; | 610 | d->flags.chevron = chevron; |
587 | } | 611 | } |
588 | 612 | ||
613 | void setCheckMark_LabelWidget(iLabelWidget *d, iBool checkMark) { | ||
614 | d->flags.checkMark = checkMark; | ||
615 | } | ||
616 | |||
589 | void setWrap_LabelWidget(iLabelWidget *d, iBool wrap) { | 617 | void setWrap_LabelWidget(iLabelWidget *d, iBool wrap) { |
590 | d->flags.wrap = wrap; | 618 | d->flags.wrap = wrap; |
591 | } | 619 | } |
@@ -610,6 +638,10 @@ void setRemoveTrailingColon_LabelWidget(iLabelWidget *d, iBool removeTrailingCol | |||
610 | } | 638 | } |
611 | } | 639 | } |
612 | 640 | ||
641 | void setTextOffset_LabelWidget(iLabelWidget *d, iInt2 offset) { | ||
642 | d->labelOffset = offset; | ||
643 | } | ||
644 | |||
613 | void updateText_LabelWidget(iLabelWidget *d, const iString *text) { | 645 | void updateText_LabelWidget(iLabelWidget *d, const iString *text) { |
614 | set_String(&d->label, text); | 646 | set_String(&d->label, text); |
615 | set_String(&d->srcLabel, text); | 647 | set_String(&d->srcLabel, text); |
diff --git a/src/ui/labelwidget.h b/src/ui/labelwidget.h index 6542ae12..4f605d6b 100644 --- a/src/ui/labelwidget.h +++ b/src/ui/labelwidget.h | |||
@@ -33,10 +33,12 @@ void setAlignVisually_LabelWidget(iLabelWidget *, iBool alignVisual); | |||
33 | void setNoAutoMinHeight_LabelWidget (iLabelWidget *, iBool noAutoMinHeight); | 33 | void setNoAutoMinHeight_LabelWidget (iLabelWidget *, iBool noAutoMinHeight); |
34 | void setNoTopFrame_LabelWidget (iLabelWidget *, iBool noTopFrame); | 34 | void setNoTopFrame_LabelWidget (iLabelWidget *, iBool noTopFrame); |
35 | void setChevron_LabelWidget (iLabelWidget *, iBool chevron); | 35 | void setChevron_LabelWidget (iLabelWidget *, iBool chevron); |
36 | void setCheckMark_LabelWidget (iLabelWidget *, iBool checkMark); | ||
36 | void setWrap_LabelWidget (iLabelWidget *, iBool wrap); | 37 | void setWrap_LabelWidget (iLabelWidget *, iBool wrap); |
37 | void setOutline_LabelWidget (iLabelWidget *, iBool drawAsOutline); | 38 | void setOutline_LabelWidget (iLabelWidget *, iBool drawAsOutline); |
38 | void setAllCaps_LabelWidget (iLabelWidget *, iBool allCaps); | 39 | void setAllCaps_LabelWidget (iLabelWidget *, iBool allCaps); |
39 | void setRemoveTrailingColon_LabelWidget (iLabelWidget *, iBool removeTrailingColon); | 40 | void setRemoveTrailingColon_LabelWidget (iLabelWidget *, iBool removeTrailingColon); |
41 | void setTextOffset_LabelWidget (iLabelWidget *, iInt2 offset); | ||
40 | void setFont_LabelWidget (iLabelWidget *, int fontId); | 42 | void setFont_LabelWidget (iLabelWidget *, int fontId); |
41 | void setTextColor_LabelWidget (iLabelWidget *, int color); | 43 | void setTextColor_LabelWidget (iLabelWidget *, int color); |
42 | void setText_LabelWidget (iLabelWidget *, const iString *text); /* resizes widget */ | 44 | void setText_LabelWidget (iLabelWidget *, const iString *text); /* resizes widget */ |
diff --git a/src/ui/linkinfo.c b/src/ui/linkinfo.c new file mode 100644 index 00000000..5102f9b3 --- /dev/null +++ b/src/ui/linkinfo.c | |||
@@ -0,0 +1,176 @@ | |||
1 | /* Copyright 2021 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
23 | #include "linkinfo.h" | ||
24 | #include "metrics.h" | ||
25 | #include "paint.h" | ||
26 | #include "../gmcerts.h" | ||
27 | #include "../app.h" | ||
28 | |||
29 | #include <SDL_render.h> | ||
30 | |||
31 | iDefineTypeConstruction(LinkInfo) | ||
32 | |||
33 | #define minWidth_LinkInfo_ (40 * gap_UI) | ||
34 | #define hPad_LinkInfo_ (2 * gap_UI) | ||
35 | #define vPad_LinkInfo_ (1 * gap_UI) | ||
36 | |||
37 | void init_LinkInfo(iLinkInfo *d) { | ||
38 | d->buf = NULL; | ||
39 | init_Anim(&d->opacity, 0.0f); | ||
40 | d->isAltPos = iFalse; | ||
41 | } | ||
42 | |||
43 | void deinit_LinkInfo(iLinkInfo *d) { | ||
44 | delete_TextBuf(d->buf); | ||
45 | } | ||
46 | |||
47 | iInt2 size_LinkInfo(const iLinkInfo *d) { | ||
48 | if (!d->buf) { | ||
49 | return zero_I2(); | ||
50 | } | ||
51 | return add_I2(d->buf->size, init_I2(2 * hPad_LinkInfo_, 2 * vPad_LinkInfo_)); | ||
52 | } | ||
53 | |||
54 | void infoText_LinkInfo(const iGmDocument *doc, iGmLinkId linkId, iString *text_out) { | ||
55 | const iString *url = linkUrl_GmDocument(doc, linkId); | ||
56 | iUrl parts; | ||
57 | init_Url(&parts, url); | ||
58 | const int flags = linkFlags_GmDocument(doc, linkId); | ||
59 | const enum iGmLinkScheme scheme = scheme_GmLinkFlag(flags); | ||
60 | const iBool isImage = (flags & imageFileExtension_GmLinkFlag) != 0; | ||
61 | const iBool isAudio = (flags & audioFileExtension_GmLinkFlag) != 0; | ||
62 | /* Most important info first: the identity that will be used. */ | ||
63 | const iGmIdentity *ident = identityForUrl_GmCerts(certs_App(), url); | ||
64 | if (ident) { | ||
65 | appendFormat_String(text_out, person_Icon " %s", | ||
66 | //escape_Color(tmBannerItemTitle_ColorId), | ||
67 | cstr_String(name_GmIdentity(ident))); | ||
68 | } | ||
69 | /* Possibly inlined content. */ | ||
70 | if (isImage || isAudio) { | ||
71 | if (!isEmpty_String(text_out)) { | ||
72 | appendCStr_String(text_out, "\n"); | ||
73 | } | ||
74 | appendCStr_String( | ||
75 | text_out, | ||
76 | format_CStr(isImage ? photo_Icon " %s " : "\U0001f3b5 %s", | ||
77 | cstr_Lang(isImage ? "link.hint.image" : "link.hint.audio"))); | ||
78 | } | ||
79 | if (!isEmpty_String(text_out)) { | ||
80 | appendCStr_String(text_out, " \u2014 "); | ||
81 | } | ||
82 | /* Indicate non-Gemini schemes. */ | ||
83 | if (scheme == mailto_GmLinkScheme) { | ||
84 | appendCStr_String(text_out, envelope_Icon " "); | ||
85 | append_String(text_out, url); | ||
86 | } | ||
87 | else if (scheme != gemini_GmLinkScheme && !isEmpty_Range(&parts.host)) { | ||
88 | appendCStr_String(text_out, globe_Icon " \x1b[1m"); | ||
89 | appendRange_String(text_out, (iRangecc){ constBegin_String(url), | ||
90 | parts.host.end }); | ||
91 | appendCStr_String(text_out, "\x1b[0m"); | ||
92 | appendRange_String(text_out, (iRangecc){ parts.path.start, constEnd_String(url) }); | ||
93 | } | ||
94 | else if (scheme != gemini_GmLinkScheme) { | ||
95 | appendCStr_String(text_out, scheme == file_GmLinkScheme ? "" : globe_Icon " "); | ||
96 | append_String(text_out, url); | ||
97 | } | ||
98 | else { | ||
99 | appendCStr_String(text_out, "\x1b[1m"); | ||
100 | appendRange_String(text_out, parts.host); | ||
101 | if (!isEmpty_Range(&parts.port)) { | ||
102 | appendCStr_String(text_out, ":"); | ||
103 | appendRange_String(text_out, parts.port); | ||
104 | } | ||
105 | appendCStr_String(text_out, "\x1b[0m"); | ||
106 | appendRange_String(text_out, (iRangecc){ parts.path.start, constEnd_String(url) }); | ||
107 | } | ||
108 | /* Date of last visit. */ | ||
109 | if (flags & visited_GmLinkFlag) { | ||
110 | iDate date; | ||
111 | init_Date(&date, linkTime_GmDocument(doc, linkId)); | ||
112 | if (!isEmpty_String(text_out)) { | ||
113 | appendCStr_String(text_out, " \u2014 "); | ||
114 | } | ||
115 | iString *dateStr = format_Date(&date, "%b %d"); | ||
116 | append_String(text_out, dateStr); | ||
117 | delete_String(dateStr); | ||
118 | } | ||
119 | } | ||
120 | |||
121 | iBool update_LinkInfo(iLinkInfo *d, const iGmDocument *doc, iGmLinkId linkId, int maxWidth) { | ||
122 | if (!d) { | ||
123 | return iFalse; | ||
124 | } | ||
125 | const iBool isAnimated = prefs_App()->uiAnimations; | ||
126 | if (d->linkId != linkId || d->maxWidth != maxWidth) { | ||
127 | d->linkId = linkId; | ||
128 | d->maxWidth = maxWidth; | ||
129 | invalidate_LinkInfo(d); | ||
130 | if (linkId) { | ||
131 | iString str; | ||
132 | init_String(&str); | ||
133 | infoText_LinkInfo(doc, linkId, &str); | ||
134 | if (targetValue_Anim(&d->opacity) < 1) { | ||
135 | setValue_Anim(&d->opacity, 1, isAnimated ? 75 : 0); | ||
136 | } | ||
137 | /* Draw to a buffer, wrapped. */ | ||
138 | const int avail = iMax(minWidth_LinkInfo_, maxWidth) - 2 * hPad_LinkInfo_; | ||
139 | iWrapText wt = { .text = range_String(&str), .maxWidth = avail, .mode = word_WrapTextMode }; | ||
140 | d->buf = new_TextBuf(&wt, uiLabel_FontId, tmQuote_ColorId); | ||
141 | deinit_String(&str); | ||
142 | } | ||
143 | else { | ||
144 | if (targetValue_Anim(&d->opacity) > 0) { | ||
145 | setValue_Anim(&d->opacity, 0, isAnimated ? 150 : 0); | ||
146 | } | ||
147 | } | ||
148 | return iTrue; | ||
149 | } | ||
150 | return iFalse; | ||
151 | } | ||
152 | |||
153 | void invalidate_LinkInfo(iLinkInfo *d) { | ||
154 | if (targetValue_Anim(&d->opacity) > 0) { | ||
155 | setValue_Anim(&d->opacity, 0, prefs_App()->uiAnimations ? 150 : 0); | ||
156 | } | ||
157 | } | ||
158 | |||
159 | void draw_LinkInfo(const iLinkInfo *d, iInt2 topLeft) { | ||
160 | const float opacity = value_Anim(&d->opacity); | ||
161 | if (!d->buf || opacity <= 0.01f) { | ||
162 | return; | ||
163 | } | ||
164 | iPaint p; | ||
165 | init_Paint(&p); | ||
166 | iInt2 size = size_LinkInfo(d); | ||
167 | iRect rect = { topLeft, size }; | ||
168 | SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND); | ||
169 | p.alpha = 255 * opacity; | ||
170 | fillRect_Paint(&p, rect, tmBackgroundAltText_ColorId); | ||
171 | drawRect_Paint(&p, rect, tmFrameAltText_ColorId); | ||
172 | SDL_SetTextureAlphaMod(d->buf->texture, p.alpha); | ||
173 | draw_TextBuf(d->buf, add_I2(topLeft, init_I2(hPad_LinkInfo_, vPad_LinkInfo_)), | ||
174 | white_ColorId); | ||
175 | SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE); | ||
176 | } | ||
diff --git a/src/ui/linkinfo.h b/src/ui/linkinfo.h new file mode 100644 index 00000000..38b90b87 --- /dev/null +++ b/src/ui/linkinfo.h | |||
@@ -0,0 +1,47 @@ | |||
1 | /* Copyright 2021 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
23 | #pragma once | ||
24 | |||
25 | #include "text.h" | ||
26 | #include "util.h" | ||
27 | #include "../gmdocument.h" | ||
28 | |||
29 | iDeclareType(LinkInfo) | ||
30 | iDeclareTypeConstruction(LinkInfo) | ||
31 | |||
32 | struct Impl_LinkInfo { | ||
33 | iGmLinkId linkId; | ||
34 | int maxWidth; | ||
35 | iTextBuf *buf; | ||
36 | iAnim opacity; | ||
37 | iBool isAltPos; | ||
38 | }; | ||
39 | |||
40 | iBool update_LinkInfo (iLinkInfo *, const iGmDocument *doc, iGmLinkId linkId, | ||
41 | int maxWidth); /* returns true if changed */ | ||
42 | void invalidate_LinkInfo (iLinkInfo *); | ||
43 | |||
44 | void infoText_LinkInfo (const iGmDocument *doc, iGmLinkId linkId, iString *text_out); | ||
45 | |||
46 | iInt2 size_LinkInfo (const iLinkInfo *); | ||
47 | void draw_LinkInfo (const iLinkInfo *, iInt2 topLeft); | ||
diff --git a/src/ui/listwidget.c b/src/ui/listwidget.c index c2ba5581..1d0f1729 100644 --- a/src/ui/listwidget.c +++ b/src/ui/listwidget.c | |||
@@ -49,21 +49,6 @@ iDefineClass(ListItem) | |||
49 | 49 | ||
50 | iDefineObjectConstruction(ListWidget) | 50 | iDefineObjectConstruction(ListWidget) |
51 | 51 | ||
52 | struct Impl_ListWidget { | ||
53 | iWidget widget; | ||
54 | iScrollWidget *scroll; | ||
55 | iSmoothScroll scrollY; | ||
56 | int itemHeight; | ||
57 | iPtrArray items; | ||
58 | size_t hoverItem; | ||
59 | size_t dragItem; | ||
60 | iInt2 dragOrigin; /* offset from mouse to drag item's top-left corner */ | ||
61 | iClick click; | ||
62 | iIntSet invalidItems; | ||
63 | iVisBuf *visBuf; | ||
64 | iBool noHoverWhileScrolling; | ||
65 | }; | ||
66 | |||
67 | static void refreshWhileScrolling_ListWidget_(iAnyObject *any) { | 52 | static void refreshWhileScrolling_ListWidget_(iAnyObject *any) { |
68 | iListWidget *d = any; | 53 | iListWidget *d = any; |
69 | updateVisible_ListWidget(d); | 54 | updateVisible_ListWidget(d); |
@@ -96,11 +81,13 @@ void init_ListWidget(iListWidget *d) { | |||
96 | setThumb_ScrollWidget(d->scroll, 0, 0); | 81 | setThumb_ScrollWidget(d->scroll, 0, 0); |
97 | init_SmoothScroll(&d->scrollY, w, scrollBegan_ListWidget_); | 82 | init_SmoothScroll(&d->scrollY, w, scrollBegan_ListWidget_); |
98 | d->itemHeight = 0; | 83 | d->itemHeight = 0; |
84 | d->scrollMode = normal_ScrollMode; | ||
99 | d->noHoverWhileScrolling = iFalse; | 85 | d->noHoverWhileScrolling = iFalse; |
100 | init_PtrArray(&d->items); | 86 | init_PtrArray(&d->items); |
101 | d->hoverItem = iInvalidPos; | 87 | d->hoverItem = iInvalidPos; |
102 | d->dragItem = iInvalidPos; | 88 | d->dragItem = iInvalidPos; |
103 | d->dragOrigin = zero_I2(); | 89 | d->dragOrigin = zero_I2(); |
90 | d->dragHandleWidth = 0; | ||
104 | init_Click(&d->click, d, SDL_BUTTON_LEFT); | 91 | init_Click(&d->click, d, SDL_BUTTON_LEFT); |
105 | init_IntSet(&d->invalidItems); | 92 | init_IntSet(&d->invalidItems); |
106 | d->visBuf = new_VisBuf(); | 93 | d->visBuf = new_VisBuf(); |
@@ -202,6 +189,17 @@ void setScrollPos_ListWidget(iListWidget *d, int pos) { | |||
202 | refresh_Widget(as_Widget(d)); | 189 | refresh_Widget(as_Widget(d)); |
203 | } | 190 | } |
204 | 191 | ||
192 | void setScrollMode_ListWidget(iListWidget *d, enum iScrollMode mode) { | ||
193 | d->scrollMode = mode; | ||
194 | } | ||
195 | |||
196 | void setDragHandleWidth_ListWidget(iListWidget *d, int dragHandleWidth) { | ||
197 | d->dragHandleWidth = dragHandleWidth; | ||
198 | if (dragHandleWidth == 0) { | ||
199 | setFlags_Widget(as_Widget(d), touchDrag_WidgetFlag, iFalse); /* mobile drag handles */ | ||
200 | } | ||
201 | } | ||
202 | |||
205 | void scrollOffset_ListWidget(iListWidget *d, int offset) { | 203 | void scrollOffset_ListWidget(iListWidget *d, int offset) { |
206 | moveSpan_SmoothScroll(&d->scrollY, offset, 0); | 204 | moveSpan_SmoothScroll(&d->scrollY, offset, 0); |
207 | } | 205 | } |
@@ -363,6 +361,7 @@ static iBool endDrag_ListWidget_(iListWidget *d, iInt2 endPos) { | |||
363 | if (d->dragItem == iInvalidPos) { | 361 | if (d->dragItem == iInvalidPos) { |
364 | return iFalse; | 362 | return iFalse; |
365 | } | 363 | } |
364 | setFlags_Widget(as_Widget(d), touchDrag_WidgetFlag, iFalse); /* mobile drag handles */ | ||
366 | stop_Anim(&d->scrollY.pos); | 365 | stop_Anim(&d->scrollY.pos); |
367 | enum iDragDestination dstKind; | 366 | enum iDragDestination dstKind; |
368 | const size_t index = resolveDragDestination_ListWidget_(d, endPos, &dstKind); | 367 | const size_t index = resolveDragDestination_ListWidget_(d, endPos, &dstKind); |
@@ -381,12 +380,31 @@ static iBool endDrag_ListWidget_(iListWidget *d, iInt2 endPos) { | |||
381 | return iTrue; | 380 | return iTrue; |
382 | } | 381 | } |
383 | 382 | ||
383 | static iBool isScrollDisabled_ListWidget_(const iListWidget *d, const SDL_Event *ev) { | ||
384 | int dir = 0; | ||
385 | if (ev->type == SDL_MOUSEWHEEL) { | ||
386 | dir = iSign(ev->wheel.y); | ||
387 | } | ||
388 | switch (d->scrollMode) { | ||
389 | case disabled_ScrollMode: | ||
390 | return iTrue; | ||
391 | case disabledAtTopBothDirections_ScrollMode: | ||
392 | return scrollPos_ListWidget(d) <= 0; | ||
393 | case disabledAtTopUpwards_ScrollMode: | ||
394 | return scrollPos_ListWidget(d) <= 0 && dir > 0; | ||
395 | default: | ||
396 | break; | ||
397 | } | ||
398 | return iFalse; | ||
399 | } | ||
400 | |||
384 | static iBool processEvent_ListWidget_(iListWidget *d, const SDL_Event *ev) { | 401 | static iBool processEvent_ListWidget_(iListWidget *d, const SDL_Event *ev) { |
385 | iWidget *w = as_Widget(d); | 402 | iWidget *w = as_Widget(d); |
386 | if (isMetricsChange_UserEvent(ev)) { | 403 | if (isMetricsChange_UserEvent(ev)) { |
387 | invalidate_ListWidget(d); | 404 | invalidate_ListWidget(d); |
388 | } | 405 | } |
389 | else if (processEvent_SmoothScroll(&d->scrollY, ev)) { | 406 | else if (!isScrollDisabled_ListWidget_(d, ev) && |
407 | processEvent_SmoothScroll(&d->scrollY, ev)) { | ||
390 | return iTrue; | 408 | return iTrue; |
391 | } | 409 | } |
392 | else if (isCommand_SDLEvent(ev)) { | 410 | else if (isCommand_SDLEvent(ev)) { |
@@ -435,6 +453,29 @@ static iBool processEvent_ListWidget_(iListWidget *d, const SDL_Event *ev) { | |||
435 | } | 453 | } |
436 | } | 454 | } |
437 | if (ev->type == SDL_MOUSEWHEEL && isHover_Widget(w)) { | 455 | if (ev->type == SDL_MOUSEWHEEL && isHover_Widget(w)) { |
456 | if (d->dragHandleWidth) { | ||
457 | if (d->dragItem == iInvalidPos) { | ||
458 | const iInt2 wpos = coord_MouseWheelEvent(&ev->wheel); | ||
459 | if (contains_Widget(w, wpos) && | ||
460 | wpos.x >= right_Rect(boundsWithoutVisualOffset_Widget(w)) - d->dragHandleWidth) { | ||
461 | setFlags_Widget(w, touchDrag_WidgetFlag, iTrue); | ||
462 | printf("[%p] touch drag started\n", d); | ||
463 | return iTrue; | ||
464 | } | ||
465 | } | ||
466 | } | ||
467 | if (isScrollDisabled_ListWidget_(d, ev)) { | ||
468 | if (ev->wheel.which == SDL_TOUCH_MOUSEID) { | ||
469 | /* TODO: Could generalize this selection of the scrollable parent. */ | ||
470 | extern iWidgetClass Class_SidebarWidget; | ||
471 | iWidget *sidebar = findParentClass_Widget(w, &Class_SidebarWidget); | ||
472 | if (sidebar) { | ||
473 | transferAffinity_Touch(w, sidebar); | ||
474 | d->noHoverWhileScrolling = iTrue; | ||
475 | } | ||
476 | } | ||
477 | return iFalse; | ||
478 | } | ||
438 | int amount = -ev->wheel.y; | 479 | int amount = -ev->wheel.y; |
439 | if (isPerPixel_MouseWheelEvent(&ev->wheel)) { | 480 | if (isPerPixel_MouseWheelEvent(&ev->wheel)) { |
440 | stop_Anim(&d->scrollY.pos); | 481 | stop_Anim(&d->scrollY.pos); |
@@ -480,7 +521,9 @@ static iBool processEvent_ListWidget_(iListWidget *d, const SDL_Event *ev) { | |||
480 | return iTrue; | 521 | return iTrue; |
481 | } | 522 | } |
482 | redrawHoverItem_ListWidget_(d); | 523 | redrawHoverItem_ListWidget_(d); |
483 | if (contains_Rect(itemRect_ListWidget(d, d->hoverItem), pos_Click(&d->click)) && | 524 | if (contains_Rect(adjusted_Rect(itemRect_ListWidget(d, d->hoverItem), |
525 | zero_I2(), init_I2(-d->dragHandleWidth, 0)), | ||
526 | pos_Click(&d->click)) && | ||
484 | d->hoverItem != iInvalidPos) { | 527 | d->hoverItem != iInvalidPos) { |
485 | postCommand_Widget(w, "list.clicked arg:%zu item:%p", | 528 | postCommand_Widget(w, "list.clicked arg:%zu item:%p", |
486 | d->hoverItem, constHoverItem_ListWidget(d)); | 529 | d->hoverItem, constHoverItem_ListWidget(d)); |
@@ -580,8 +623,9 @@ static void draw_ListWidget_(const iListWidget *d) { | |||
580 | } | 623 | } |
581 | setClip_Paint(&p, bounds_Widget(w)); | 624 | setClip_Paint(&p, bounds_Widget(w)); |
582 | draw_VisBuf(d->visBuf, addY_I2(topLeft_Rect(bounds), -scrollY), ySpan_Rect(bounds)); | 625 | draw_VisBuf(d->visBuf, addY_I2(topLeft_Rect(bounds), -scrollY), ySpan_Rect(bounds)); |
583 | const iInt2 mousePos = mouseCoord_Window(get_Window(), 0); | 626 | const iBool isMobile = (deviceType_App() != desktop_AppDeviceType); |
584 | if (d->dragItem != iInvalidPos && contains_Rect(bounds, mousePos)) { | 627 | const iInt2 mousePos = mouseCoord_Window(get_Window(), isMobile ? SDL_TOUCH_MOUSEID : 0); |
628 | if (d->dragItem != iInvalidPos && (isMobile || contains_Rect(bounds, mousePos))) { | ||
585 | iInt2 pos = add_I2(mousePos, d->dragOrigin); | 629 | iInt2 pos = add_I2(mousePos, d->dragOrigin); |
586 | const iListItem *item = constAt_PtrArray(&d->items, d->dragItem); | 630 | const iListItem *item = constAt_PtrArray(&d->items, d->dragItem); |
587 | const iRect itemRect = { init_I2(left_Rect(bounds), pos.y), | 631 | const iRect itemRect = { init_I2(left_Rect(bounds), pos.y), |
diff --git a/src/ui/listwidget.h b/src/ui/listwidget.h index 8adf6ac3..da215b19 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. */ | |||
25 | #include "scrollwidget.h" | 25 | #include "scrollwidget.h" |
26 | #include "paint.h" | 26 | #include "paint.h" |
27 | 27 | ||
28 | #include <the_Foundation/intset.h> | ||
28 | #include <the_Foundation/ptrarray.h> | 29 | #include <the_Foundation/ptrarray.h> |
29 | 30 | ||
30 | iDeclareType(ListWidget) | 31 | iDeclareType(ListWidget) |
@@ -48,6 +49,34 @@ iDeclareObjectConstruction(ListItem) | |||
48 | iDeclareWidgetClass(ListWidget) | 49 | iDeclareWidgetClass(ListWidget) |
49 | iDeclareObjectConstruction(ListWidget) | 50 | iDeclareObjectConstruction(ListWidget) |
50 | 51 | ||
52 | iDeclareType(VisBuf) | ||
53 | |||
54 | enum iScrollMode { | ||
55 | normal_ScrollMode, | ||
56 | disabledAtTopBothDirections_ScrollMode, | ||
57 | disabledAtTopUpwards_ScrollMode, | ||
58 | disabled_ScrollMode, | ||
59 | }; | ||
60 | |||
61 | struct Impl_ListWidget { | ||
62 | iWidget widget; | ||
63 | iScrollWidget *scroll; | ||
64 | iSmoothScroll scrollY; | ||
65 | int itemHeight; | ||
66 | iPtrArray items; | ||
67 | size_t hoverItem; | ||
68 | size_t dragItem; | ||
69 | iInt2 dragOrigin; /* offset from mouse to drag item's top-left corner */ | ||
70 | int dragHandleWidth; | ||
71 | iClick click; | ||
72 | iIntSet invalidItems; | ||
73 | iVisBuf *visBuf; | ||
74 | enum iScrollMode scrollMode; | ||
75 | iBool noHoverWhileScrolling; | ||
76 | }; | ||
77 | |||
78 | void init_ListWidget (iListWidget *); | ||
79 | |||
51 | void setItemHeight_ListWidget (iListWidget *, int itemHeight); | 80 | void setItemHeight_ListWidget (iListWidget *, int itemHeight); |
52 | 81 | ||
53 | void invalidate_ListWidget (iListWidget *); | 82 | void invalidate_ListWidget (iListWidget *); |
@@ -62,6 +91,8 @@ int itemHeight_ListWidget (const iListWidget *); | |||
62 | int scrollPos_ListWidget (const iListWidget *); | 91 | int scrollPos_ListWidget (const iListWidget *); |
63 | 92 | ||
64 | void setScrollPos_ListWidget (iListWidget *, int pos); | 93 | void setScrollPos_ListWidget (iListWidget *, int pos); |
94 | void setScrollMode_ListWidget (iListWidget *, enum iScrollMode mode); | ||
95 | void setDragHandleWidth_ListWidget(iListWidget *, int dragHandleWidth); | ||
65 | void scrollToItem_ListWidget (iListWidget *, size_t index, uint32_t span); | 96 | void scrollToItem_ListWidget (iListWidget *, size_t index, uint32_t span); |
66 | void scrollOffset_ListWidget (iListWidget *, int offset); | 97 | void scrollOffset_ListWidget (iListWidget *, int offset); |
67 | void scrollOffsetSpan_ListWidget (iListWidget *, int offset, uint32_t span); | 98 | void scrollOffsetSpan_ListWidget (iListWidget *, int offset, uint32_t span); |
diff --git a/src/ui/lookupwidget.c b/src/ui/lookupwidget.c index da0113ce..f14170ad 100644 --- a/src/ui/lookupwidget.c +++ b/src/ui/lookupwidget.c | |||
@@ -658,23 +658,37 @@ static iBool processEvent_LookupWidget_(iLookupWidget *d, const SDL_Event *ev) { | |||
658 | (equal_Command(cmd, "layout.changed") && | 658 | (equal_Command(cmd, "layout.changed") && |
659 | equal_Rangecc(range_Command(cmd, "id"), "navbar"))) { | 659 | equal_Rangecc(range_Command(cmd, "id"), "navbar"))) { |
660 | /* Position the lookup popup under the URL bar. */ { | 660 | /* Position the lookup popup under the URL bar. */ { |
661 | iRoot *root = w->root; | 661 | iRoot *root = w->root; |
662 | iWidget *url = findChild_Widget(root->widget, "url"); | ||
663 | const int minWidth = iMin(120 * gap_UI, width_Rect(safeRect_Root(root))); | ||
664 | const int urlWidth = width_Widget(url); | ||
665 | int extraWidth = 0; | ||
666 | if (urlWidth < minWidth) { | ||
667 | extraWidth = minWidth - urlWidth; | ||
668 | } | ||
662 | const iRect navBarBounds = bounds_Widget(findChild_Widget(root->widget, "navbar")); | 669 | const iRect navBarBounds = bounds_Widget(findChild_Widget(root->widget, "navbar")); |
663 | iWidget *url = findChild_Widget(root->widget, "url"); | 670 | setFixedSize_Widget( |
664 | setFixedSize_Widget(w, init_I2(width_Widget(url), | 671 | w, |
665 | (bottom_Rect(rect_Root(root)) - bottom_Rect(navBarBounds)) / 2)); | 672 | init_I2(width_Widget(url) + extraWidth, |
666 | setPos_Widget(w, windowToLocal_Widget(w, bottomLeft_Rect(bounds_Widget(url)))); | 673 | (bottom_Rect(rect_Root(root)) - bottom_Rect(navBarBounds)) / 2)); |
667 | #if defined (iPlatformAppleMobile) | 674 | setPos_Widget(w, |
675 | windowToLocal_Widget(w, | ||
676 | max_I2(zero_I2(), | ||
677 | addX_I2(bottomLeft_Rect(bounds_Widget(url)), | ||
678 | -extraWidth / 2)))); | ||
679 | #if defined(iPlatformMobile) | ||
668 | /* TODO: Check this again. */ | 680 | /* TODO: Check this again. */ |
669 | /* Adjust height based on keyboard size. */ { | 681 | /* Adjust height based on keyboard size. */ { |
670 | w->rect.size.y = bottom_Rect(visibleRect_Root(root)) - top_Rect(bounds_Widget(w)); | 682 | w->rect.size.y = bottom_Rect(visibleRect_Root(root)) - top_Rect(bounds_Widget(w)); |
683 | # if defined (iPlatformAppleMobile) | ||
671 | if (deviceType_App() == phone_AppDeviceType) { | 684 | if (deviceType_App() == phone_AppDeviceType) { |
672 | float l, r; | 685 | float l = 0.0f, r = 0.0f; |
673 | safeAreaInsets_iOS(&l, NULL, &r, NULL); | 686 | safeAreaInsets_iOS(&l, NULL, &r, NULL); |
674 | w->rect.size.x = size_Root(root).x - l - r; | 687 | w->rect.size.x = size_Root(root).x - l - r; |
675 | w->rect.pos.x = l; | 688 | w->rect.pos.x = l; |
676 | /* TODO: Need to use windowToLocal_Widget? */ | 689 | /* TODO: Need to use windowToLocal_Widget? */ |
677 | } | 690 | } |
691 | # endif | ||
678 | } | 692 | } |
679 | #endif | 693 | #endif |
680 | arrange_Widget(w); | 694 | arrange_Widget(w); |
diff --git a/src/ui/mediaui.c b/src/ui/mediaui.c index 4f2499b0..f0070688 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) { | |||
104 | if (align == right_Alignment) { | 104 | if (align == right_Alignment) { |
105 | pos.x -= size.x; | 105 | pos.x -= size.x; |
106 | } | 106 | } |
107 | drawRange_Text(font, addY_I2(pos, -gap_UI / 8), color, range_String(&num)); | 107 | drawRange_Text(font, addY_I2(pos, gap_UI / 2), color, range_String(&num)); |
108 | deinit_String(&num); | 108 | deinit_String(&num); |
109 | return size.x; | 109 | return size.x; |
110 | } | 110 | } |
@@ -238,6 +238,56 @@ void init_DownloadUI(iDownloadUI *d, const iMedia *media, uint16_t mediaId, iRec | |||
238 | /*----------------------------------------------------------------------------------------------*/ | 238 | /*----------------------------------------------------------------------------------------------*/ |
239 | 239 | ||
240 | iBool processEvent_DownloadUI(iDownloadUI *d, const SDL_Event *ev) { | 240 | iBool processEvent_DownloadUI(iDownloadUI *d, const SDL_Event *ev) { |
241 | if (ev->type == SDL_MOUSEBUTTONDOWN || ev->type == SDL_MOUSEBUTTONUP) { | ||
242 | const iInt2 mouse = init_I2(ev->button.x, ev->button.y); | ||
243 | if (!contains_Rect(d->bounds, mouse)) { | ||
244 | return iFalse; | ||
245 | } | ||
246 | float bytesPerSecond; | ||
247 | const iString *path; | ||
248 | iBool isFinished; | ||
249 | downloadStats_Media(d->media, (iMediaId){ download_MediaType, d->mediaId }, | ||
250 | &path, &bytesPerSecond, &isFinished); | ||
251 | if (isFinished) { | ||
252 | if (ev->button.button == SDL_BUTTON_RIGHT && ev->type == SDL_MOUSEBUTTONDOWN) { | ||
253 | const iMenuItem items[] = { | ||
254 | /* Items related to the file */ | ||
255 | { openTab_Icon " ${menu.opentab}", | ||
256 | 0, | ||
257 | 0, | ||
258 | format_CStr("!open newtab:1 url:%s", | ||
259 | cstrCollect_String(makeFileUrl_String(path))) }, | ||
260 | #if defined (iPlatformAppleDesktop) | ||
261 | { "${menu.reveal.macos}", | ||
262 | 0, | ||
263 | 0, | ||
264 | format_CStr("!reveal path:%s", cstr_String(path)) }, | ||
265 | #endif | ||
266 | #if defined (iPlatformAppleMobile) | ||
267 | { export_Icon " ${menu.share}", | ||
268 | 0, | ||
269 | 0, | ||
270 | format_CStr("!reveal path:%s", cstr_String(path)) }, | ||
271 | #endif | ||
272 | #if defined (iPlatformLinux) | ||
273 | { "${menu.reveal.filemgr}", | ||
274 | 0, | ||
275 | 0, | ||
276 | format_CStr("!reveal path:%s", cstr_String(path)) }, | ||
277 | #endif | ||
278 | { "---" }, | ||
279 | /* Generic items */ | ||
280 | { "${menu.downloads}", 0, 0, "downloads.open newtab:1" }, | ||
281 | }; | ||
282 | openMenu_Widget(makeMenu_Widget(get_Root()->widget, items, iElemCount(items)), | ||
283 | mouse); | ||
284 | return iTrue; | ||
285 | } | ||
286 | else if (ev->button.button == SDL_BUTTON_LEFT && ev->type == SDL_MOUSEBUTTONUP) { | ||
287 | postCommandf_App("open default:1 url:%s", cstrCollect_String(makeFileUrl_String(path))); | ||
288 | } | ||
289 | } | ||
290 | } | ||
241 | return iFalse; | 291 | return iFalse; |
242 | } | 292 | } |
243 | 293 | ||
diff --git a/src/ui/mobile.c b/src/ui/mobile.c index abc91218..cf955423 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. */ | |||
23 | #include "mobile.h" | 23 | #include "mobile.h" |
24 | 24 | ||
25 | #include "app.h" | 25 | #include "app.h" |
26 | #include "certlistwidget.h" | ||
26 | #include "command.h" | 27 | #include "command.h" |
27 | #include "defs.h" | 28 | #include "defs.h" |
28 | #include "inputwidget.h" | 29 | #include "inputwidget.h" |
@@ -36,14 +37,39 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
36 | # include "ios.h" | 37 | # include "ios.h" |
37 | #endif | 38 | #endif |
38 | 39 | ||
40 | const iToolbarActionSpec toolbarActions_Mobile[max_ToolbarAction] = { | ||
41 | { backArrow_Icon, "${menu.back}", "navigate.back" }, | ||
42 | { forwardArrow_Icon, "${menu.forward}", "navigate.forward" }, | ||
43 | { home_Icon, "${menu.home}", "navigate.home" }, | ||
44 | { upArrow_Icon, "${menu.parent}", "navigate.parent" }, | ||
45 | { reload_Icon, "${menu.reload}", "navigate.reload" }, | ||
46 | { openTab_Icon, "${menu.newtab}", "tabs.new" }, | ||
47 | { close_Icon, "${menu.closetab}", "tabs.close" }, | ||
48 | { bookmark_Icon, "${menu.page.bookmark}", "bookmark.add" }, | ||
49 | { globe_Icon, "${menu.page.translate}", "document.translate" }, | ||
50 | { upload_Icon, "${menu.page.upload}", "document.upload" }, | ||
51 | { edit_Icon, "${menu.page.upload.edit}", "document.upload copy:1" }, | ||
52 | { magnifyingGlass_Icon, "${menu.find}", "focus.set id:find.input" }, | ||
53 | { gear_Icon, "${menu.settings}", "preferences" }, | ||
54 | { leftHalf_Icon, "${menu.sidebar.left}", "sidebar.toggle" }, | ||
55 | }; | ||
56 | |||
39 | iBool isUsingPanelLayout_Mobile(void) { | 57 | iBool isUsingPanelLayout_Mobile(void) { |
40 | return deviceType_App() != desktop_AppDeviceType; | 58 | return deviceType_App() != desktop_AppDeviceType; |
41 | } | 59 | } |
42 | 60 | ||
61 | #define topPanelMinWidth_Mobile (80 * gap_UI) | ||
62 | |||
43 | static iBool isSideBySideLayout_(void) { | 63 | static iBool isSideBySideLayout_(void) { |
64 | /* Minimum is an even split. */ | ||
65 | const int safeWidth = safeRect_Root(get_Root()).size.x; | ||
66 | if (safeWidth / 2 < topPanelMinWidth_Mobile) { | ||
67 | return iFalse; | ||
68 | } | ||
44 | if (deviceType_App() == phone_AppDeviceType) { | 69 | if (deviceType_App() == phone_AppDeviceType) { |
45 | return isLandscape_App(); | 70 | return isLandscape_App(); |
46 | } | 71 | } |
72 | /* Tablet may still be too narrow. */ | ||
47 | return numRoots_Window(get_Window()) == 1; | 73 | return numRoots_Window(get_Window()) == 1; |
48 | } | 74 | } |
49 | 75 | ||
@@ -99,6 +125,16 @@ static iWidget *findTitleLabel_(iWidget *panel) { | |||
99 | return NULL; | 125 | return NULL; |
100 | } | 126 | } |
101 | 127 | ||
128 | static void updateCertListHeight_(iWidget *detailStack) { | ||
129 | iWidget *certList = findChild_Widget(detailStack, "certlist"); | ||
130 | if (certList) { | ||
131 | setFixedSize_Widget(certList, | ||
132 | init_I2(-1, | ||
133 | -1 * gap_UI + bottom_Rect(safeRect_Root(certList->root)) - | ||
134 | top_Rect(boundsWithoutVisualOffset_Widget(certList)))); | ||
135 | } | ||
136 | } | ||
137 | |||
102 | static iBool mainDetailSplitHandler_(iWidget *mainDetailSplit, const char *cmd) { | 138 | static iBool mainDetailSplitHandler_(iWidget *mainDetailSplit, const char *cmd) { |
103 | if (equal_Command(cmd, "window.resized")) { | 139 | if (equal_Command(cmd, "window.resized")) { |
104 | const iBool isPortrait = (deviceType_App() == phone_AppDeviceType && isPortrait_App()); | 140 | const iBool isPortrait = (deviceType_App() == phone_AppDeviceType && isPortrait_App()); |
@@ -117,11 +153,13 @@ static iBool mainDetailSplitHandler_(iWidget *mainDetailSplit, const char *cmd) | |||
117 | const int pad = isPortrait ? 0 : 3 * gap_UI; | 153 | const int pad = isPortrait ? 0 : 3 * gap_UI; |
118 | if (isSideBySide) { | 154 | if (isSideBySide) { |
119 | iAssert(topPanel); | 155 | iAssert(topPanel); |
120 | topPanel->rect.size.x = (deviceType_App() == phone_AppDeviceType ? | 156 | topPanel->rect.size.x = iMax(topPanelMinWidth_Mobile, |
121 | safeRoot.size.x * 2 / 5 : (safeRoot.size.x / 3)); | 157 | (deviceType_App() == phone_AppDeviceType ? |
122 | } | 158 | safeRoot.size.x * 2 / 5 : safeRoot.size.x / 3)); |
159 | } | ||
123 | if (deviceType_App() == tablet_AppDeviceType) { | 160 | if (deviceType_App() == tablet_AppDeviceType) { |
124 | setPadding_Widget(topPanel, pad, 0, pad, pad); | 161 | setPadding_Widget(topPanel, pad, 0, pad, pad); |
162 | #if 0 | ||
125 | if (numPanels == 0) { | 163 | if (numPanels == 0) { |
126 | setFlags_Widget(sheet, centerHorizontal_WidgetFlag, iTrue); | 164 | setFlags_Widget(sheet, centerHorizontal_WidgetFlag, iTrue); |
127 | const int sheetWidth = iMin(safeRoot.size.x, safeRoot.size.y); | 165 | const int sheetWidth = iMin(safeRoot.size.x, safeRoot.size.y); |
@@ -129,6 +167,7 @@ static iBool mainDetailSplitHandler_(iWidget *mainDetailSplit, const char *cmd) | |||
129 | setFixedSize_Widget(sheet, init_I2(sheetWidth, -1)); | 167 | setFixedSize_Widget(sheet, init_I2(sheetWidth, -1)); |
130 | setFixedSize_Widget(navi, init_I2(sheetWidth, -1)); | 168 | setFixedSize_Widget(navi, init_I2(sheetWidth, -1)); |
131 | } | 169 | } |
170 | #endif | ||
132 | } | 171 | } |
133 | iWidget *detailTitle = findChild_Widget(navi, "detailtitle"); { | 172 | iWidget *detailTitle = findChild_Widget(navi, "detailtitle"); { |
134 | setPos_Widget(detailTitle, init_I2(width_Widget(topPanel), 0)); | 173 | setPos_Widget(detailTitle, init_I2(width_Widget(topPanel), 0)); |
@@ -143,9 +182,10 @@ static iBool mainDetailSplitHandler_(iWidget *mainDetailSplit, const char *cmd) | |||
143 | if (isSideBySide) { | 182 | if (isSideBySide) { |
144 | setVisualOffset_Widget(panel, 0, 0, 0); | 183 | setVisualOffset_Widget(panel, 0, 0, 0); |
145 | } | 184 | } |
146 | setPadding_Widget(panel, pad, 0, pad, pad); | 185 | setPadding_Widget(panel, pad, 0, pad, pad + bottomSafeInset_Mobile()); |
147 | } | 186 | } |
148 | arrange_Widget(mainDetailSplit); | 187 | arrange_Widget(mainDetailSplit); |
188 | updateCertListHeight_(detailStack); | ||
149 | } | 189 | } |
150 | else if (equal_Command(cmd, "mouse.clicked") && arg_Command(cmd)) { | 190 | else if (equal_Command(cmd, "mouse.clicked") && arg_Command(cmd)) { |
151 | if (focus_Widget() && class_Widget(focus_Widget()) == &Class_InputWidget) { | 191 | if (focus_Widget() && class_Widget(focus_Widget()) == &Class_InputWidget) { |
@@ -168,13 +208,16 @@ size_t currentPanelIndex_Mobile(const iWidget *panels) { | |||
168 | return iInvalidPos; | 208 | return iInvalidPos; |
169 | } | 209 | } |
170 | 210 | ||
211 | iWidget *panel_Mobile(const iWidget *panels, size_t index) { | ||
212 | return child_Widget(findChild_Widget(panels, "detailstack"), index); | ||
213 | } | ||
214 | |||
171 | static iBool topPanelHandler_(iWidget *topPanel, const char *cmd) { | 215 | static iBool topPanelHandler_(iWidget *topPanel, const char *cmd) { |
172 | const iBool isPortrait = !isSideBySideLayout_(); | 216 | const iBool isPortrait = !isSideBySideLayout_(); |
173 | if (equal_Command(cmd, "panel.open")) { | 217 | if (equal_Command(cmd, "panel.open")) { |
174 | iWidget *button = pointer_Command(cmd); | 218 | /* This command is sent by the button that opens the panel. */ |
219 | iWidget *button = pointer_Command(cmd); | ||
175 | iWidget *panel = userData_Object(button); | 220 | iWidget *panel = userData_Object(button); |
176 | // openMenu_Widget(panel, innerToWindow_Widget(panel, zero_I2())); | ||
177 | // setFlags_Widget(panel, hidden_WidgetFlag, iFalse); | ||
178 | unselectAllPanelButtons_(topPanel); | 221 | unselectAllPanelButtons_(topPanel); |
179 | int panelIndex = -1; | 222 | int panelIndex = -1; |
180 | size_t childIndex = 0; | 223 | size_t childIndex = 0; |
@@ -184,7 +227,7 @@ static iBool topPanelHandler_(iWidget *topPanel, const char *cmd) { | |||
184 | /* Animate the current panel in. */ | 227 | /* Animate the current panel in. */ |
185 | if (child == panel && isPortrait) { | 228 | if (child == panel && isPortrait) { |
186 | setupSheetTransition_Mobile(panel, iTrue); | 229 | setupSheetTransition_Mobile(panel, iTrue); |
187 | panelIndex = childIndex; | 230 | panelIndex = (int) childIndex; |
188 | } | 231 | } |
189 | childIndex++; | 232 | childIndex++; |
190 | } | 233 | } |
@@ -196,6 +239,7 @@ static iBool topPanelHandler_(iWidget *topPanel, const char *cmd) { | |||
196 | setText_LabelWidget(detailTitle, text_LabelWidget((iLabelWidget *) findTitleLabel_(panel))); | 239 | setText_LabelWidget(detailTitle, text_LabelWidget((iLabelWidget *) findTitleLabel_(panel))); |
197 | setFlags_Widget(button, selected_WidgetFlag, iTrue); | 240 | setFlags_Widget(button, selected_WidgetFlag, iTrue); |
198 | postCommand_Widget(topPanel, "panel.changed arg:%d", panelIndex); | 241 | postCommand_Widget(topPanel, "panel.changed arg:%d", panelIndex); |
242 | updateCertListHeight_(findDetailStack_(topPanel)); | ||
199 | return iTrue; | 243 | return iTrue; |
200 | } | 244 | } |
201 | if (equal_Command(cmd, "swipe.back")) { | 245 | if (equal_Command(cmd, "swipe.back")) { |
@@ -227,6 +271,10 @@ static iBool topPanelHandler_(iWidget *topPanel, const char *cmd) { | |||
227 | else if (findWidget_App("upload")) { | 271 | else if (findWidget_App("upload")) { |
228 | postCommand_App("upload.cancel"); | 272 | postCommand_App("upload.cancel"); |
229 | } | 273 | } |
274 | else if (findWidget_App("bmed.sidebar") || findWidget_App("bmed.create") || | ||
275 | findWidget_App("bmed")) { | ||
276 | postCommand_App("bmed.cancel"); | ||
277 | } | ||
230 | else if (findWidget_App("ident")) { | 278 | else if (findWidget_App("ident")) { |
231 | postCommand_Widget(topPanel, "ident.cancel"); | 279 | postCommand_Widget(topPanel, "ident.cancel"); |
232 | } | 280 | } |
@@ -468,7 +516,7 @@ void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) { | |||
468 | iLabelWidget *heading = NULL; | 516 | iLabelWidget *heading = NULL; |
469 | iWidget * value = NULL; | 517 | iWidget * value = NULL; |
470 | const char * spec = item->label; | 518 | const char * spec = item->label; |
471 | const char * id = cstr_Rangecc(range_Command(spec, "id")); | 519 | const char * id = cstr_Command(spec, "id"); |
472 | const char * label = hasLabel_Command(spec, "text") | 520 | const char * label = hasLabel_Command(spec, "text") |
473 | ? suffixPtr_Command(spec, "text") | 521 | ? suffixPtr_Command(spec, "text") |
474 | : format_CStr("${%s}", id); | 522 | : format_CStr("${%s}", id); |
@@ -482,7 +530,7 @@ void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) { | |||
482 | collapse_WidgetFlag); | 530 | collapse_WidgetFlag); |
483 | setFont_LabelWidget(title, uiLabelLargeBold_FontId); | 531 | setFont_LabelWidget(title, uiLabelLargeBold_FontId); |
484 | setTextColor_LabelWidget(title, uiHeading_ColorId); | 532 | setTextColor_LabelWidget(title, uiHeading_ColorId); |
485 | setAllCaps_LabelWidget(title, iTrue); | 533 | // setAllCaps_LabelWidget(title, iTrue); |
486 | setId_Widget(as_Widget(title), id); | 534 | setId_Widget(as_Widget(title), id); |
487 | } | 535 | } |
488 | else if (equal_Command(spec, "heading")) { | 536 | else if (equal_Command(spec, "heading")) { |
@@ -511,10 +559,12 @@ void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) { | |||
511 | setId_Widget(as_Widget(drop), id); | 559 | setId_Widget(as_Widget(drop), id); |
512 | widget = makeValuePaddingWithHeading_(heading = makeHeading_Widget(label), as_Widget(drop)); | 560 | widget = makeValuePaddingWithHeading_(heading = makeHeading_Widget(label), as_Widget(drop)); |
513 | setCommandHandler_Widget(widget, dropdownHeadingHandler_); | 561 | setCommandHandler_Widget(widget, dropdownHeadingHandler_); |
562 | widget->padding[2] = gap_UI; | ||
514 | setUserData_Object(widget, drop); | 563 | setUserData_Object(widget, drop); |
515 | } | 564 | } |
516 | else if (equal_Command(spec, "radio") || equal_Command(spec, "buttons")) { | 565 | else if (equal_Command(spec, "radio") || equal_Command(spec, "buttons")) { |
517 | const iBool isRadio = equal_Command(spec, "radio"); | 566 | const iBool isRadio = equal_Command(spec, "radio"); |
567 | const iBool isHorizontal = argLabel_Command(spec, "horizontal"); | ||
518 | addChild_Widget(panel, iClob(makePadding_Widget(lineHeight_Text(labelFont_())))); | 568 | addChild_Widget(panel, iClob(makePadding_Widget(lineHeight_Text(labelFont_())))); |
519 | iLabelWidget *head = makeHeading_Widget(label); | 569 | iLabelWidget *head = makeHeading_Widget(label); |
520 | setAllCaps_LabelWidget(head, iTrue); | 570 | setAllCaps_LabelWidget(head, iTrue); |
@@ -522,25 +572,42 @@ void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) { | |||
522 | addChild_Widget(panel, iClob(head)); | 572 | addChild_Widget(panel, iClob(head)); |
523 | widget = new_Widget(); | 573 | widget = new_Widget(); |
524 | setBackgroundColor_Widget(widget, uiBackgroundSidebar_ColorId); | 574 | setBackgroundColor_Widget(widget, uiBackgroundSidebar_ColorId); |
525 | setPadding_Widget(widget, 4 * gap_UI, 2 * gap_UI, 4 * gap_UI, 2 * gap_UI); | 575 | const int hPad = (isHorizontal ? 0 : 1); |
576 | setPadding_Widget(widget, hPad * gap_UI, 2 * gap_UI, hPad * gap_UI, 2 * gap_UI); | ||
526 | setFlags_Widget(widget, | 577 | setFlags_Widget(widget, |
527 | borderTop_WidgetFlag | | 578 | borderTop_WidgetFlag | |
528 | borderBottom_WidgetFlag | | 579 | borderBottom_WidgetFlag | |
529 | arrangeHorizontal_WidgetFlag | | 580 | (isHorizontal ? arrangeHorizontal_WidgetFlag : arrangeVertical_WidgetFlag) | |
530 | arrangeHeight_WidgetFlag | | 581 | arrangeHeight_WidgetFlag | |
531 | resizeToParentWidth_WidgetFlag | | 582 | resizeToParentWidth_WidgetFlag | |
532 | resizeWidthOfChildren_WidgetFlag, | 583 | resizeWidthOfChildren_WidgetFlag, |
533 | iTrue); | 584 | iTrue); |
534 | setId_Widget(widget, id); | 585 | setId_Widget(widget, id); |
586 | iBool isFirst = iTrue; | ||
535 | for (const iMenuItem *radioItem = item->data; radioItem->label; radioItem++) { | 587 | for (const iMenuItem *radioItem = item->data; radioItem->label; radioItem++) { |
536 | const char * radId = cstr_Rangecc(range_Command(radioItem->label, "id")); | 588 | if (!isHorizontal && !isFirst) { |
537 | int64_t flags = noBackground_WidgetFlag; | 589 | /* The separator is padded from the left so we need two. */ |
590 | iWidget *sep = new_Widget(); | ||
591 | iWidget *sep2 = new_Widget(); | ||
592 | addChildFlags_Widget(sep, iClob(sep2), 0); | ||
593 | setFlags_Widget(sep, arrangeHeight_WidgetFlag | resizeWidthOfChildren_WidgetFlag, iTrue); | ||
594 | setBackgroundColor_Widget(sep2, uiSeparator_ColorId); | ||
595 | setFixedSize_Widget(sep2, init_I2(-1, gap_UI / 4)); | ||
596 | setPadding_Widget(sep, 5 * gap_UI, 0, 0, 0); | ||
597 | addChildFlags_Widget(widget, iClob(sep), 0); | ||
598 | } | ||
599 | isFirst = iFalse; | ||
600 | const char * radId = cstr_Command(radioItem->label, "id"); | ||
601 | int64_t flags = noBackground_WidgetFlag | frameless_WidgetFlag; | ||
602 | if (!isHorizontal) { | ||
603 | flags |= alignLeft_WidgetFlag; | ||
604 | } | ||
538 | iLabelWidget *button; | 605 | iLabelWidget *button; |
539 | if (isRadio) { | 606 | if (isRadio) { |
540 | const char *radLabel = | 607 | const char *radLabel = |
541 | hasLabel_Command(radioItem->label, "label") | 608 | hasLabel_Command(radioItem->label, "label") |
542 | ? format_CStr("${%s}", | 609 | ? format_CStr("${%s}", |
543 | cstr_Rangecc(range_Command(radioItem->label, "label"))) | 610 | cstr_Command(radioItem->label, "label")) |
544 | : suffixPtr_Command(radioItem->label, "text"); | 611 | : suffixPtr_Command(radioItem->label, "text"); |
545 | button = new_LabelWidget(radLabel, radioItem->command); | 612 | button = new_LabelWidget(radLabel, radioItem->command); |
546 | flags |= radio_WidgetFlag; | 613 | flags |= radio_WidgetFlag; |
@@ -549,17 +616,21 @@ void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) { | |||
549 | button = (iLabelWidget *) makeToggle_Widget(radId); | 616 | button = (iLabelWidget *) makeToggle_Widget(radId); |
550 | setTextCStr_LabelWidget(button, format_CStr("${%s}", radId)); | 617 | setTextCStr_LabelWidget(button, format_CStr("${%s}", radId)); |
551 | setFlags_Widget(as_Widget(button), fixedWidth_WidgetFlag, iFalse); | 618 | setFlags_Widget(as_Widget(button), fixedWidth_WidgetFlag, iFalse); |
552 | updateSize_LabelWidget(button); | ||
553 | } | 619 | } |
554 | setId_Widget(as_Widget(button), radId); | 620 | setId_Widget(as_Widget(button), radId); |
555 | setFont_LabelWidget(button, uiLabelMedium_FontId); | 621 | setFont_LabelWidget(button, deviceType_App() == phone_AppDeviceType ? |
622 | (isHorizontal ? uiLabelMedium_FontId : uiLabelBig_FontId) : labelFont_()); | ||
623 | setCheckMark_LabelWidget(button, !isHorizontal); | ||
624 | setPadding_Widget(as_Widget(button), gap_UI, 1 * gap_UI, 0, 1 * gap_UI); | ||
625 | updateSize_LabelWidget(button); | ||
626 | setPadding_Widget(widget, 0, 0, 0, 0); | ||
556 | addChildFlags_Widget(widget, iClob(button), flags); | 627 | addChildFlags_Widget(widget, iClob(button), flags); |
557 | } | 628 | } |
558 | } | 629 | } |
559 | else if (equal_Command(spec, "input")) { | 630 | else if (equal_Command(spec, "input")) { |
560 | iInputWidget *input = new_InputWidget(argU32Label_Command(spec, "maxlen")); | 631 | iInputWidget *input = new_InputWidget(argU32Label_Command(spec, "maxlen")); |
561 | if (hasLabel_Command(spec, "hint")) { | 632 | if (hasLabel_Command(spec, "hint")) { |
562 | setHint_InputWidget(input, cstr_Lang(cstr_Rangecc(range_Command(spec, "hint")))); | 633 | setHint_InputWidget(input, cstr_Lang(cstr_Command(spec, "hint"))); |
563 | } | 634 | } |
564 | setId_Widget(as_Widget(input), id); | 635 | setId_Widget(as_Widget(input), id); |
565 | setUrlContent_InputWidget(input, argLabel_Command(spec, "url")); | 636 | setUrlContent_InputWidget(input, argLabel_Command(spec, "url")); |
@@ -570,12 +641,13 @@ void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) { | |||
570 | setFlags_Widget(widget, expand_WidgetFlag, iTrue); | 641 | setFlags_Widget(widget, expand_WidgetFlag, iTrue); |
571 | } | 642 | } |
572 | else { | 643 | else { |
644 | setFlags_Widget(as_Widget(input), alignRight_WidgetFlag, iTrue); | ||
573 | setContentPadding_InputWidget(input, 3 * gap_UI, 0); | 645 | setContentPadding_InputWidget(input, 3 * gap_UI, 0); |
574 | if (hasLabel_Command(spec, "unit")) { | 646 | if (hasLabel_Command(spec, "unit")) { |
575 | iWidget *unit = addChildFlags_Widget( | 647 | iWidget *unit = addChildFlags_Widget( |
576 | as_Widget(input), | 648 | as_Widget(input), |
577 | iClob(new_LabelWidget( | 649 | iClob(new_LabelWidget( |
578 | format_CStr("${%s}", cstr_Rangecc(range_Command(spec, "unit"))), NULL)), | 650 | format_CStr("${%s}", cstr_Command(spec, "unit")), NULL)), |
579 | frameless_WidgetFlag | moveToParentRightEdge_WidgetFlag | | 651 | frameless_WidgetFlag | moveToParentRightEdge_WidgetFlag | |
580 | resizeToParentHeight_WidgetFlag); | 652 | resizeToParentHeight_WidgetFlag); |
581 | setContentPadding_InputWidget(input, -1, width_Widget(unit) - 4 * gap_UI); | 653 | setContentPadding_InputWidget(input, -1, width_Widget(unit) - 4 * gap_UI); |
@@ -586,6 +658,14 @@ void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) { | |||
586 | setUserData_Object(widget, input); | 658 | setUserData_Object(widget, input); |
587 | } | 659 | } |
588 | } | 660 | } |
661 | else if (equal_Command(spec, "certlist")) { | ||
662 | iCertListWidget *certList = new_CertListWidget(); | ||
663 | iListWidget *list = (iListWidget *) certList; | ||
664 | setBackgroundColor_Widget(as_Widget(list), uiBackgroundSidebar_ColorId); | ||
665 | widget = as_Widget(certList); | ||
666 | updateItems_CertListWidget(certList); | ||
667 | invalidate_ListWidget(list); | ||
668 | } | ||
589 | else if (equal_Command(spec, "button")) { | 669 | else if (equal_Command(spec, "button")) { |
590 | widget = as_Widget(heading = makePanelButton_(label, item->command)); | 670 | widget = as_Widget(heading = makePanelButton_(label, item->command)); |
591 | setFlags_Widget(widget, selected_WidgetFlag, argLabel_Command(spec, "selected") != 0); | 671 | setFlags_Widget(widget, selected_WidgetFlag, argLabel_Command(spec, "selected") != 0); |
@@ -599,6 +679,9 @@ void makePanelItem_Mobile(iWidget *panel, const iMenuItem *item) { | |||
599 | fixedHeight_WidgetFlag | | 679 | fixedHeight_WidgetFlag | |
600 | (!argLabel_Command(spec, "frame") ? frameless_WidgetFlag : 0), | 680 | (!argLabel_Command(spec, "frame") ? frameless_WidgetFlag : 0), |
601 | iTrue); | 681 | iTrue); |
682 | if (argLabel_Command(spec, "font")) { | ||
683 | setFont_LabelWidget(lab, argLabel_Command(spec, "font")); | ||
684 | } | ||
602 | } | 685 | } |
603 | else if (equal_Command(spec, "padding")) { | 686 | else if (equal_Command(spec, "padding")) { |
604 | float height = 1.5f; | 687 | float height = 1.5f; |
@@ -670,8 +753,10 @@ void initPanels_Mobile(iWidget *panels, iWidget *parentWidget, | |||
670 | setFlags_Widget(panels, | 753 | setFlags_Widget(panels, |
671 | resizeToParentWidth_WidgetFlag | resizeToParentHeight_WidgetFlag | | 754 | resizeToParentWidth_WidgetFlag | resizeToParentHeight_WidgetFlag | |
672 | frameless_WidgetFlag | focusRoot_WidgetFlag | commandOnClick_WidgetFlag | | 755 | frameless_WidgetFlag | focusRoot_WidgetFlag | commandOnClick_WidgetFlag | |
756 | horizontalOffset_WidgetFlag | | ||
673 | /*overflowScrollable_WidgetFlag |*/ leftEdgeDraggable_WidgetFlag, | 757 | /*overflowScrollable_WidgetFlag |*/ leftEdgeDraggable_WidgetFlag, |
674 | iTrue); | 758 | iTrue); |
759 | panels->flags2 |= fadeBackground_WidgetFlag2; | ||
675 | setFlags_Widget(panels, overflowScrollable_WidgetFlag, iFalse); | 760 | setFlags_Widget(panels, overflowScrollable_WidgetFlag, iFalse); |
676 | /* The top-level split between main and detail panels. */ | 761 | /* The top-level split between main and detail panels. */ |
677 | iWidget *mainDetailSplit = makeHDiv_Widget(); { | 762 | iWidget *mainDetailSplit = makeHDiv_Widget(); { |
@@ -733,7 +818,7 @@ void initPanels_Mobile(iWidget *panels, iWidget *parentWidget, | |||
733 | const iMenuItem *item = &itemsNullTerminated[i]; | 818 | const iMenuItem *item = &itemsNullTerminated[i]; |
734 | if (equal_Command(item->label, "panel")) { | 819 | if (equal_Command(item->label, "panel")) { |
735 | haveDetailPanels = iTrue; | 820 | haveDetailPanels = iTrue; |
736 | const char *id = cstr_Rangecc(range_Command(item->label, "id")); | 821 | const char *id = cstr_Command(item->label, "id"); |
737 | const iString *label = hasLabel_Command(item->label, "text") | 822 | const iString *label = hasLabel_Command(item->label, "text") |
738 | ? collect_String(suffix_Command(item->label, "text")) | 823 | ? collect_String(suffix_Command(item->label, "text")) |
739 | : collectNewFormat_String("${%s}", id); | 824 | : collectNewFormat_String("${%s}", id); |
@@ -849,18 +934,21 @@ void setupMenuTransition_Mobile(iWidget *sheet, iBool isIncoming) { | |||
849 | if (!isUsingPanelLayout_Mobile()) { | 934 | if (!isUsingPanelLayout_Mobile()) { |
850 | return; | 935 | return; |
851 | } | 936 | } |
852 | const iBool isSlidePanel = (flags_Widget(sheet) & horizontalOffset_WidgetFlag) != 0; | 937 | const iBool isHorizPanel = (flags_Widget(sheet) & horizontalOffset_WidgetFlag) != 0; |
853 | if (isSlidePanel && isLandscape_App()) { | 938 | if (isHorizPanel && isLandscape_App()) { |
854 | return; | 939 | return; |
855 | } | 940 | } |
941 | const int maxOffset = isHorizPanel ? width_Widget(sheet) | ||
942 | : isPortraitPhone_App() ? height_Widget(sheet) | ||
943 | : (12 * gap_UI); | ||
856 | if (isIncoming) { | 944 | if (isIncoming) { |
857 | setVisualOffset_Widget(sheet, isSlidePanel ? width_Widget(sheet) : height_Widget(sheet), 0, 0); | 945 | setVisualOffset_Widget(sheet, maxOffset, 0, 0); |
858 | setVisualOffset_Widget(sheet, 0, 330, easeOut_AnimFlag | softer_AnimFlag); | 946 | setVisualOffset_Widget(sheet, 0, 330, easeOut_AnimFlag | softer_AnimFlag); |
859 | } | 947 | } |
860 | else { | 948 | else { |
861 | const iBool wasDragged = iAbs(value_Anim(&sheet->visualOffset) - 0) > 1; | 949 | const iBool wasDragged = iAbs(value_Anim(&sheet->visualOffset) - 0) > 1; |
862 | setVisualOffset_Widget(sheet, | 950 | setVisualOffset_Widget(sheet, |
863 | isSlidePanel ? width_Widget(sheet) : height_Widget(sheet), | 951 | maxOffset, |
864 | wasDragged ? 100 : 200, | 952 | wasDragged ? 100 : 200, |
865 | wasDragged ? 0 : easeIn_AnimFlag | softer_AnimFlag); | 953 | wasDragged ? 0 : easeIn_AnimFlag | softer_AnimFlag); |
866 | } | 954 | } |
@@ -930,3 +1018,13 @@ void setupSheetTransition_Mobile(iWidget *sheet, int flags) { | |||
930 | } | 1018 | } |
931 | } | 1019 | } |
932 | } | 1020 | } |
1021 | |||
1022 | int bottomSafeInset_Mobile(void) { | ||
1023 | #if defined (iPlatformAppleMobile) | ||
1024 | float bot; | ||
1025 | safeAreaInsets_iOS(NULL, NULL, NULL, &bot); | ||
1026 | return iRound(bot); | ||
1027 | #else | ||
1028 | return 0; | ||
1029 | #endif | ||
1030 | } | ||
diff --git a/src/ui/mobile.h b/src/ui/mobile.h index 9d7ac8e4..a719f20b 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. */ | |||
22 | 22 | ||
23 | #pragma once | 23 | #pragma once |
24 | 24 | ||
25 | #include "defs.h" | ||
25 | #include <the_Foundation/rect.h> | 26 | #include <the_Foundation/rect.h> |
26 | 27 | ||
28 | iDeclareType(ToolbarActionSpec) | ||
29 | |||
30 | struct Impl_ToolbarActionSpec { | ||
31 | const char *icon; | ||
32 | const char *label; | ||
33 | const char *command; | ||
34 | }; | ||
35 | |||
36 | extern const iToolbarActionSpec toolbarActions_Mobile[max_ToolbarAction]; | ||
37 | |||
27 | iDeclareType(Widget) | 38 | iDeclareType(Widget) |
28 | iDeclareType(MenuItem) | 39 | iDeclareType(MenuItem) |
29 | 40 | ||
@@ -39,6 +50,7 @@ void initPanels_Mobile (iWidget *panels, iWidget *parentWidget, | |||
39 | const iMenuItem *itemsNullTerminated, | 50 | const iMenuItem *itemsNullTerminated, |
40 | const iMenuItem *actions, size_t numActions); | 51 | const iMenuItem *actions, size_t numActions); |
41 | 52 | ||
53 | iWidget * panel_Mobile (const iWidget *panels, size_t index); | ||
42 | size_t currentPanelIndex_Mobile (const iWidget *panels); | 54 | size_t currentPanelIndex_Mobile (const iWidget *panels); |
43 | 55 | ||
44 | enum iTransitionFlags { | 56 | enum iTransitionFlags { |
@@ -55,3 +67,5 @@ enum iTransitionDir { | |||
55 | 67 | ||
56 | void setupMenuTransition_Mobile (iWidget *menu, iBool isIncoming); | 68 | void setupMenuTransition_Mobile (iWidget *menu, iBool isIncoming); |
57 | void setupSheetTransition_Mobile (iWidget *sheet, int flags); | 69 | void setupSheetTransition_Mobile (iWidget *sheet, int flags); |
70 | |||
71 | int bottomSafeInset_Mobile (void); | ||
diff --git a/src/ui/paint.c b/src/ui/paint.c index b92be27e..5e66f521 100644 --- a/src/ui/paint.c +++ b/src/ui/paint.c | |||
@@ -41,6 +41,7 @@ void init_Paint(iPaint *d) { | |||
41 | d->dst = get_Window(); | 41 | d->dst = get_Window(); |
42 | d->setTarget = NULL; | 42 | d->setTarget = NULL; |
43 | d->oldTarget = NULL; | 43 | d->oldTarget = NULL; |
44 | d->oldOrigin = zero_I2(); | ||
44 | d->alpha = 255; | 45 | d->alpha = 255; |
45 | } | 46 | } |
46 | 47 | ||
@@ -48,6 +49,8 @@ void beginTarget_Paint(iPaint *d, SDL_Texture *target) { | |||
48 | SDL_Renderer *rend = renderer_Paint_(d); | 49 | SDL_Renderer *rend = renderer_Paint_(d); |
49 | if (!d->setTarget) { | 50 | if (!d->setTarget) { |
50 | d->oldTarget = SDL_GetRenderTarget(rend); | 51 | d->oldTarget = SDL_GetRenderTarget(rend); |
52 | d->oldOrigin = origin_Paint; | ||
53 | origin_Paint = zero_I2(); | ||
51 | SDL_SetRenderTarget(rend, target); | 54 | SDL_SetRenderTarget(rend, target); |
52 | d->setTarget = target; | 55 | d->setTarget = target; |
53 | } | 56 | } |
@@ -59,8 +62,10 @@ void beginTarget_Paint(iPaint *d, SDL_Texture *target) { | |||
59 | void endTarget_Paint(iPaint *d) { | 62 | void endTarget_Paint(iPaint *d) { |
60 | if (d->setTarget) { | 63 | if (d->setTarget) { |
61 | SDL_SetRenderTarget(renderer_Paint_(d), d->oldTarget); | 64 | SDL_SetRenderTarget(renderer_Paint_(d), d->oldTarget); |
65 | origin_Paint = d->oldOrigin; | ||
66 | d->oldOrigin = zero_I2(); | ||
62 | d->oldTarget = NULL; | 67 | d->oldTarget = NULL; |
63 | d->setTarget = NULL; | 68 | d->setTarget = NULL; |
64 | } | 69 | } |
65 | } | 70 | } |
66 | 71 | ||
@@ -108,7 +113,7 @@ void drawRect_Paint(const iPaint *d, iRect rect, int color) { | |||
108 | { left_Rect(rect), br.y }, | 113 | { left_Rect(rect), br.y }, |
109 | { left_Rect(rect), top_Rect(rect) } | 114 | { left_Rect(rect), top_Rect(rect) } |
110 | }; | 115 | }; |
111 | #if SDL_VERSION_ATLEAST(2, 0, 16) | 116 | #if SDL_COMPILEDVERSION == SDL_VERSIONNUM(2, 0, 16) |
112 | if (isOpenGLRenderer_Window()) { | 117 | if (isOpenGLRenderer_Window()) { |
113 | /* A very curious regression in SDL 2.0.16. */ | 118 | /* A very curious regression in SDL 2.0.16. */ |
114 | edges[3].y--; | 119 | edges[3].y--; |
@@ -170,7 +175,7 @@ void drawLines_Paint(const iPaint *d, const iInt2 *points, size_t n, int color) | |||
170 | for (size_t i = 0; i < n; i++) { | 175 | for (size_t i = 0; i < n; i++) { |
171 | offsetPoints[i] = add_I2(points[i], origin_Paint); | 176 | offsetPoints[i] = add_I2(points[i], origin_Paint); |
172 | } | 177 | } |
173 | SDL_RenderDrawLines(renderer_Paint_(d), (const SDL_Point *) offsetPoints, n); | 178 | SDL_RenderDrawLines(renderer_Paint_(d), (const SDL_Point *) offsetPoints, (int) n); |
174 | free(offsetPoints); | 179 | free(offsetPoints); |
175 | } | 180 | } |
176 | 181 | ||
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 { | |||
33 | iWindow * dst; | 33 | iWindow * dst; |
34 | SDL_Texture *setTarget; | 34 | SDL_Texture *setTarget; |
35 | SDL_Texture *oldTarget; | 35 | SDL_Texture *oldTarget; |
36 | iInt2 oldOrigin; | ||
36 | uint8_t alpha; | 37 | uint8_t alpha; |
37 | }; | 38 | }; |
38 | 39 | ||
diff --git a/src/ui/root.c b/src/ui/root.c index cf13169d..5c4296cf 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. */ | |||
38 | #include "../history.h" | 38 | #include "../history.h" |
39 | #include "../gmcerts.h" | 39 | #include "../gmcerts.h" |
40 | #include "../gmutil.h" | 40 | #include "../gmutil.h" |
41 | #include "../sitespec.h" | ||
41 | #include "../visited.h" | 42 | #include "../visited.h" |
42 | 43 | ||
43 | #if defined (iPlatformMsys) | 44 | #if defined (iPlatformMsys) |
@@ -94,16 +95,16 @@ static const iMenuItem tabletNavMenuItems_[] = { | |||
94 | { close_Icon " ${menu.closetab}", 'w', KMOD_PRIMARY, "tabs.close" }, | 95 | { close_Icon " ${menu.closetab}", 'w', KMOD_PRIMARY, "tabs.close" }, |
95 | { "---" }, | 96 | { "---" }, |
96 | { magnifyingGlass_Icon " ${menu.find}", 0, 0, "focus.set id:find.input" }, | 97 | { magnifyingGlass_Icon " ${menu.find}", 0, 0, "focus.set id:find.input" }, |
97 | { leftHalf_Icon " ${menu.sidebar.left}", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" }, | 98 | // { leftHalf_Icon " ${menu.sidebar.left}", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" }, |
98 | { rightHalf_Icon " ${menu.sidebar.right}", SDLK_p, KMOD_PRIMARY | KMOD_SHIFT, "sidebar2.toggle" }, | 99 | { rightHalf_Icon " ${menu.sidebar.right}", SDLK_p, KMOD_PRIMARY | KMOD_SHIFT, "sidebar2.toggle" }, |
99 | { "${menu.view.split}", SDLK_j, KMOD_PRIMARY, "splitmenu.open" }, | 100 | { "${menu.view.split}", SDLK_j, KMOD_PRIMARY, "splitmenu.open" }, |
100 | { "---" }, | 101 | { "---" }, |
101 | { book_Icon " ${menu.bookmarks.list}", 0, 0, "!open url:about:bookmarks" }, | 102 | { book_Icon " ${menu.bookmarks.list}", 0, 0, "!open url:about:bookmarks" }, |
102 | { "${menu.bookmarks.bytag}", 0, 0, "!open url:about:bookmarks?tags" }, | 103 | { "${menu.bookmarks.bytag}", 0, 0, "!open url:about:bookmarks?tags" }, |
103 | { "${menu.feeds.entrylist}", 0, 0, "!open url:about:feeds" }, | 104 | { "${menu.feeds.entrylist}", 0, 0, "!open url:about:feeds" }, |
104 | { "${menu.downloads}", 0, 0, "downloads.open" }, | 105 | //{ "${menu.downloads}", 0, 0, "downloads.open" }, |
105 | { "---" }, | 106 | { "---" }, |
106 | { gear_Icon " ${menu.preferences}", SDLK_COMMA, KMOD_PRIMARY, "preferences" }, | 107 | { gear_Icon " ${menu.settings}", SDLK_COMMA, KMOD_PRIMARY, "preferences" }, |
107 | { "${menu.help}", SDLK_F1, 0, "!open url:about:help" }, | 108 | { "${menu.help}", SDLK_F1, 0, "!open url:about:help" }, |
108 | { "${menu.releasenotes}", 0, 0, "!open url:about:version" }, | 109 | { "${menu.releasenotes}", 0, 0, "!open url:about:version" }, |
109 | }; | 110 | }; |
@@ -115,10 +116,10 @@ static const iMenuItem phoneNavMenuItems_[] = { | |||
115 | { close_Icon " ${menu.closetab}", 'w', KMOD_PRIMARY, "tabs.close" }, | 116 | { close_Icon " ${menu.closetab}", 'w', KMOD_PRIMARY, "tabs.close" }, |
116 | { "---" }, | 117 | { "---" }, |
117 | { magnifyingGlass_Icon " ${menu.find}", 0, 0, "focus.set id:find.input" }, | 118 | { magnifyingGlass_Icon " ${menu.find}", 0, 0, "focus.set id:find.input" }, |
118 | { leftHalf_Icon " ${menu.sidebar}", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" }, | 119 | // { leftHalf_Icon " ${menu.sidebar}", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" }, |
119 | { "---" }, | 120 | { "---" }, |
120 | { book_Icon " ${menu.bookmarks.list}", 0, 0, "!open url:about:bookmarks" }, | 121 | { book_Icon " ${menu.bookmarks.list}", 0, 0, "!open url:about:bookmarks" }, |
121 | { "${menu.downloads}", 0, 0, "downloads.open" }, | 122 | //{ "${menu.downloads}", 0, 0, "downloads.open" }, |
122 | { "${menu.feeds.entrylist}", 0, 0, "!open url:about:feeds" }, | 123 | { "${menu.feeds.entrylist}", 0, 0, "!open url:about:feeds" }, |
123 | { "---" }, | 124 | { "---" }, |
124 | { gear_Icon " ${menu.settings}", SDLK_COMMA, KMOD_PRIMARY, "preferences" }, | 125 | { gear_Icon " ${menu.settings}", SDLK_COMMA, KMOD_PRIMARY, "preferences" }, |
@@ -240,6 +241,7 @@ static int loadAnimIndex_ = 0; | |||
240 | static iRoot * activeRoot_ = NULL; | 241 | static iRoot * activeRoot_ = NULL; |
241 | 242 | ||
242 | iDefineTypeConstruction(Root) | 243 | iDefineTypeConstruction(Root) |
244 | iDefineAudienceGetter(Root, visualOffsetsChanged) | ||
243 | 245 | ||
244 | void init_Root(iRoot *d) { | 246 | void init_Root(iRoot *d) { |
245 | iZap(*d); | 247 | iZap(*d); |
@@ -249,6 +251,7 @@ void deinit_Root(iRoot *d) { | |||
249 | iReleasePtr(&d->widget); | 251 | iReleasePtr(&d->widget); |
250 | delete_PtrArray(d->onTop); | 252 | delete_PtrArray(d->onTop); |
251 | delete_PtrSet(d->pendingDestruction); | 253 | delete_PtrSet(d->pendingDestruction); |
254 | delete_Audience(d->visualOffsetsChanged); | ||
252 | } | 255 | } |
253 | 256 | ||
254 | void setCurrent_Root(iRoot *root) { | 257 | void setCurrent_Root(iRoot *root) { |
@@ -315,9 +318,12 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) { | |||
315 | if (equal_Command(cmd, "menu.open")) { | 318 | if (equal_Command(cmd, "menu.open")) { |
316 | iWidget *button = pointer_Command(cmd); | 319 | iWidget *button = pointer_Command(cmd); |
317 | iWidget *menu = findChild_Widget(button, "menu"); | 320 | iWidget *menu = findChild_Widget(button, "menu"); |
321 | const iBool isPlacedUnder = argLabel_Command(cmd, "under"); | ||
318 | iAssert(menu); | 322 | iAssert(menu); |
319 | if (!isVisible_Widget(menu)) { | 323 | if (!isVisible_Widget(menu)) { |
320 | openMenu_Widget(menu, topLeft_Rect(bounds_Widget(button))); | 324 | openMenu_Widget(menu, |
325 | isPlacedUnder ? bottomLeft_Rect(bounds_Widget(button)) | ||
326 | : topLeft_Rect(bounds_Widget(button))); | ||
321 | } | 327 | } |
322 | else { | 328 | else { |
323 | closeMenu_Widget(menu); | 329 | closeMenu_Widget(menu); |
@@ -330,6 +336,93 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) { | |||
330 | openMenuFlags_Widget(menu, zero_I2(), postCommands_MenuOpenFlags | center_MenuOpenFlags); | 336 | openMenuFlags_Widget(menu, zero_I2(), postCommands_MenuOpenFlags | center_MenuOpenFlags); |
331 | return iTrue; | 337 | return iTrue; |
332 | } | 338 | } |
339 | else if (deviceType_App() == tablet_AppDeviceType && equal_Command(cmd, "toolbar.showident")) { | ||
340 | /* No toolbar on tablet, so we handle this command here. */ | ||
341 | postCommand_App("preferences idents:1"); | ||
342 | return iTrue; | ||
343 | } | ||
344 | else if (equal_Command(cmd, "identmenu.open")) { | ||
345 | iWidget *toolBar = findWidget_Root("toolbar"); | ||
346 | iWidget *button = findWidget_Root(toolBar && isPortraitPhone_App() ? "toolbar.ident" : "navbar.ident"); | ||
347 | iArray items; | ||
348 | init_Array(&items, sizeof(iMenuItem)); | ||
349 | /* Current identity. */ | ||
350 | const iString *docUrl = url_DocumentWidget(document_App()); | ||
351 | const iGmIdentity *ident = identityForUrl_GmCerts(certs_App(), docUrl); | ||
352 | const iString *fp = ident ? collect_String(hexEncode_Block(&ident->fingerprint)) : NULL; | ||
353 | iString *str = NULL; | ||
354 | if (ident) { | ||
355 | str = copy_String(name_GmIdentity(ident)); | ||
356 | if (!isEmpty_String(&ident->notes)) { | ||
357 | appendFormat_String(str, "\n\x1b[0m" uiHeading_ColorEscape "%s", cstr_String(&ident->notes)); | ||
358 | } | ||
359 | } | ||
360 | pushBack_Array( | ||
361 | &items, | ||
362 | &(iMenuItem){ format_CStr("```" uiHeading_ColorEscape "\x1b[1m%s", | ||
363 | str ? cstr_String(str) : "${menu.identity.notactive}") }); | ||
364 | if (ident && isUsedOn_GmIdentity(ident, docUrl)) { | ||
365 | pushBack_Array(&items, | ||
366 | &(iMenuItem){ close_Icon " ${ident.stopuse}", | ||
367 | 0, | ||
368 | 0, | ||
369 | format_CStr("ident.signout ident:%s url:%s", | ||
370 | cstr_String(fp), | ||
371 | cstr_String(docUrl)) }); | ||
372 | } | ||
373 | pushBack_Array(&items, &(iMenuItem){ "---" }); | ||
374 | delete_String(str); | ||
375 | /* Alternate identities. */ | ||
376 | const iString *site = collectNewRange_String(urlRoot_String(docUrl)); | ||
377 | iBool haveAlts = iFalse; | ||
378 | iConstForEach(StringArray, i, strings_SiteSpec(site, usedIdentities_SiteSpecKey)) { | ||
379 | if (!fp || !equal_String(i.value, fp)) { | ||
380 | const iBlock *otherFp = collect_Block(hexDecode_Rangecc(range_String(i.value))); | ||
381 | const iGmIdentity *other = findIdentity_GmCerts(certs_App(), otherFp); | ||
382 | if (other && other != ident) { | ||
383 | pushBack_Array( | ||
384 | &items, | ||
385 | &(iMenuItem){ | ||
386 | format_CStr(translateCStr_Lang("\U0001f816 ${ident.switch}"), | ||
387 | format_CStr("\x1b[1m%s", | ||
388 | cstr_String(name_GmIdentity(other)))), | ||
389 | 0, | ||
390 | 0, | ||
391 | format_CStr("ident.switch fp:%s", cstr_String(i.value)) }); | ||
392 | haveAlts = iTrue; | ||
393 | } | ||
394 | } | ||
395 | } | ||
396 | if (haveAlts) { | ||
397 | pushBack_Array(&items, &(iMenuItem){ "---" }); | ||
398 | } | ||
399 | iSidebarWidget *sidebar = findWidget_App("sidebar"); | ||
400 | pushBackN_Array( | ||
401 | &items, | ||
402 | (iMenuItem[]){ | ||
403 | { add_Icon " ${menu.identity.new}", newIdentity_KeyShortcut, "ident.new" }, | ||
404 | { "${menu.identity.import}", SDLK_i, KMOD_PRIMARY | KMOD_SHIFT, "ident.import" }, | ||
405 | { "---" } }, 3); | ||
406 | if (deviceType_App() == desktop_AppDeviceType) { | ||
407 | pushBack_Array(&items, | ||
408 | &(iMenuItem){ isVisible_Widget(sidebar) && mode_SidebarWidget(sidebar) == | ||
409 | identities_SidebarMode | ||
410 | ? leftHalf_Icon " ${menu.hide.identities}" | ||
411 | : leftHalf_Icon " ${menu.show.identities}", | ||
412 | 0, | ||
413 | 0, | ||
414 | "sidebar.mode arg:3 toggle:1" }); | ||
415 | } | ||
416 | else { | ||
417 | pushBack_Array(&items, &(iMenuItem){ gear_Icon " ${menu.identities}", 0, 0, | ||
418 | "toolbar.showident"}); | ||
419 | } | ||
420 | iWidget *menu = | ||
421 | makeMenu_Widget(button, constData_Array(&items), size_Array(&items)); | ||
422 | openMenu_Widget(menu, bottomLeft_Rect(bounds_Widget(button))); | ||
423 | deinit_Array(&items); | ||
424 | return iTrue; | ||
425 | } | ||
333 | else if (equal_Command(cmd, "contextclick")) { | 426 | else if (equal_Command(cmd, "contextclick")) { |
334 | iBool showBarMenu = iFalse; | 427 | iBool showBarMenu = iFalse; |
335 | if (equal_Rangecc(range_Command(cmd, "id"), "buttons")) { | 428 | if (equal_Rangecc(range_Command(cmd, "id"), "buttons")) { |
@@ -351,7 +444,7 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) { | |||
351 | return iFalse; | 444 | return iFalse; |
352 | } | 445 | } |
353 | else if (equal_Command(cmd, "focus.set")) { | 446 | else if (equal_Command(cmd, "focus.set")) { |
354 | setFocus_Widget(findWidget_App(cstr_Rangecc(range_Command(cmd, "id")))); | 447 | setFocus_Widget(findWidget_App(cstr_Command(cmd, "id"))); |
355 | return iTrue; | 448 | return iTrue; |
356 | } | 449 | } |
357 | else if (equal_Command(cmd, "input.resized")) { | 450 | else if (equal_Command(cmd, "input.resized")) { |
@@ -401,14 +494,38 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) { | |||
401 | SDL_PushEvent(&(SDL_Event){ .type = SDL_QUIT }); | 494 | SDL_PushEvent(&(SDL_Event){ .type = SDL_QUIT }); |
402 | return iTrue; | 495 | return iTrue; |
403 | } | 496 | } |
497 | else if (deviceType_App() == tablet_AppDeviceType && equal_Command(cmd, "window.resized")) { | ||
498 | iSidebarWidget *sidebar = findChild_Widget(root, "sidebar"); | ||
499 | iSidebarWidget *sidebar2 = findChild_Widget(root, "sidebar2"); | ||
500 | setWidth_SidebarWidget(sidebar, 73.0f); | ||
501 | setWidth_SidebarWidget(sidebar2, 73.0f); | ||
502 | return iFalse; | ||
503 | } | ||
404 | else if (deviceType_App() == phone_AppDeviceType && equal_Command(cmd, "window.resized")) { | 504 | else if (deviceType_App() == phone_AppDeviceType && equal_Command(cmd, "window.resized")) { |
405 | /* Place the sidebar next to or under doctabs depending on orientation. */ | 505 | /* Place the sidebar next to or under doctabs depending on orientation. */ |
406 | iSidebarWidget *sidebar = findChild_Widget(root, "sidebar"); | 506 | iSidebarWidget *sidebar = findChild_Widget(root, "sidebar"); |
407 | iSidebarWidget *sidebar2 = findChild_Widget(root, "sidebar2"); | ||
408 | removeChild_Widget(parent_Widget(sidebar), sidebar); | 507 | removeChild_Widget(parent_Widget(sidebar), sidebar); |
409 | // setBackgroundColor_Widget(findChild_Widget(as_Widget(sidebar), "buttons"), | 508 | iChangeFlags(as_Widget(sidebar)->flags2, fadeBackground_WidgetFlag2, isPortrait_App()); |
410 | // isPortrait_App() ? uiBackgroundUnfocusedSelection_ColorId | 509 | if (isLandscape_App()) { |
411 | // : uiBackgroundSidebar_ColorId); | 510 | setVisualOffset_Widget(as_Widget(sidebar), 0, 0, 0); |
511 | addChildPos_Widget(findChild_Widget(root, "tabs.content"), iClob(sidebar), front_WidgetAddPos); | ||
512 | setWidth_SidebarWidget(sidebar, 73.0f); | ||
513 | setFlags_Widget(as_Widget(sidebar), fixedHeight_WidgetFlag | fixedPosition_WidgetFlag, iFalse); | ||
514 | } | ||
515 | else { | ||
516 | addChild_Widget(root, iClob(sidebar)); | ||
517 | setWidth_SidebarWidget(sidebar, (float) width_Widget(root) / (float) gap_UI); | ||
518 | int midHeight = height_Widget(root) / 2;// + lineHeight_Text(uiLabelLarge_FontId); | ||
519 | #if defined (iPlatformAndroidMobile) | ||
520 | midHeight += 2 * lineHeight_Text(uiLabelLarge_FontId); | ||
521 | #endif | ||
522 | setMidHeight_SidebarWidget(sidebar, midHeight); | ||
523 | setFixedSize_Widget(as_Widget(sidebar), init_I2(-1, midHeight)); | ||
524 | setPos_Widget(as_Widget(sidebar), init_I2(0, height_Widget(root) - midHeight)); | ||
525 | } | ||
526 | #if 0 | ||
527 | iSidebarWidget *sidebar = findChild_Widget(root, "sidebar"); | ||
528 | iSidebarWidget *sidebar2 = findChild_Widget(root, "sidebar2"); | ||
412 | setFlags_Widget(findChild_Widget(as_Widget(sidebar), "buttons"), | 529 | setFlags_Widget(findChild_Widget(as_Widget(sidebar), "buttons"), |
413 | borderTop_WidgetFlag, | 530 | borderTop_WidgetFlag, |
414 | isPortrait_App()); | 531 | isPortrait_App()); |
@@ -424,6 +541,20 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) { | |||
424 | setWidth_SidebarWidget(sidebar, (float) width_Widget(root) / (float) gap_UI); | 541 | setWidth_SidebarWidget(sidebar, (float) width_Widget(root) / (float) gap_UI); |
425 | setWidth_SidebarWidget(sidebar2, (float) width_Widget(root) / (float) gap_UI); | 542 | setWidth_SidebarWidget(sidebar2, (float) width_Widget(root) / (float) gap_UI); |
426 | } | 543 | } |
544 | #endif | ||
545 | return iFalse; | ||
546 | } | ||
547 | else if (equal_Command(cmd, "root.arrange")) { | ||
548 | iWidget *prefs = findWidget_Root("prefs"); | ||
549 | if (prefs) { | ||
550 | updatePreferencesLayout_Widget(prefs); | ||
551 | } | ||
552 | root->root->pendingArrange = iFalse; | ||
553 | return iTrue; | ||
554 | } | ||
555 | else if (equal_Command(cmd, "theme.changed")) { | ||
556 | /* The phone toolbar is draw-buffered so it needs refreshing. */ | ||
557 | refresh_Widget(findWidget_App("toolbar")); | ||
427 | return iFalse; | 558 | return iFalse; |
428 | } | 559 | } |
429 | else if (handleCommand_App(cmd)) { | 560 | else if (handleCommand_App(cmd)) { |
@@ -449,22 +580,54 @@ static void updateNavBarIdentity_(iWidget *navBar) { | |||
449 | iLabelWidget *toolName = findWidget_App("toolbar.name"); | 580 | iLabelWidget *toolName = findWidget_App("toolbar.name"); |
450 | if (toolName) { | 581 | if (toolName) { |
451 | setOutline_LabelWidget(toolButton, ident == NULL); | 582 | setOutline_LabelWidget(toolButton, ident == NULL); |
452 | updateTextCStr_LabelWidget(toolName, subjectName ? cstr_String(subjectName) : ""); | 583 | /* Fit the name in the widget. */ |
584 | if (subjectName) { | ||
585 | const char *endPos; | ||
586 | tryAdvanceNoWrap_Text(uiLabelTiny_FontId, range_String(subjectName), width_Widget(toolName), | ||
587 | &endPos); | ||
588 | updateText_LabelWidget( | ||
589 | toolName, | ||
590 | collectNewRange_String((iRangecc){ constBegin_String(subjectName), endPos })); | ||
591 | } | ||
592 | else { | ||
593 | updateTextCStr_LabelWidget(toolName, ""); | ||
594 | } | ||
453 | setFont_LabelWidget(toolButton, subjectName ? uiLabelMedium_FontId : uiLabelLarge_FontId); | 595 | setFont_LabelWidget(toolButton, subjectName ? uiLabelMedium_FontId : uiLabelLarge_FontId); |
454 | arrange_Widget(parent_Widget(toolButton)); | 596 | setTextOffset_LabelWidget(toolButton, init_I2(0, subjectName ? -1.5f * gap_UI : 0)); |
597 | arrange_Widget(parent_Widget(toolButton)); | ||
455 | } | 598 | } |
456 | } | 599 | } |
457 | 600 | ||
458 | static void updateNavDirButtons_(iWidget *navBar) { | 601 | static void updateNavDirButtons_(iWidget *navBar) { |
459 | const iHistory *history = history_DocumentWidget(document_App()); | 602 | iBeginCollect(); |
460 | setFlags_Widget(findChild_Widget(navBar, "navbar.back"), disabled_WidgetFlag, | 603 | const iHistory *history = history_DocumentWidget(document_App()); |
461 | atOldest_History(history)); | 604 | const iBool atOldest = atOldest_History(history); |
462 | setFlags_Widget(findChild_Widget(navBar, "navbar.forward"), disabled_WidgetFlag, | 605 | const iBool atNewest = atNewest_History(history); |
463 | atLatest_History(history)); | 606 | /* Reset button state. */ |
464 | setFlags_Widget(findWidget_App("toolbar.back"), disabled_WidgetFlag, | 607 | for (size_t i = 0; i < maxNavbarActions_Prefs; i++) { |
465 | atOldest_History(history)); | 608 | const char *id = format_CStr("navbar.action%d", i + 1); |
466 | setFlags_Widget(findWidget_App("toolbar.forward"), disabled_WidgetFlag, | 609 | setFlags_Widget(findChild_Widget(navBar, id), disabled_WidgetFlag, iFalse); |
467 | atLatest_History(history)); | 610 | } |
611 | setFlags_Widget(as_Widget(findMenuItem_Widget(navBar, "navigate.back")), disabled_WidgetFlag, atOldest); | ||
612 | setFlags_Widget(as_Widget(findMenuItem_Widget(navBar, "navigate.forward")), disabled_WidgetFlag, atNewest); | ||
613 | iWidget *toolBar = findWidget_App("toolbar"); | ||
614 | if (toolBar) { | ||
615 | /* Reset the state. */ | ||
616 | for (int i = 0; i < 2; i++) { | ||
617 | const char *id = (i == 0 ? "toolbar.action1" : "toolbar.action2"); | ||
618 | setFlags_Widget(findChild_Widget(toolBar, id), disabled_WidgetFlag, iFalse); | ||
619 | setOutline_LabelWidget(findChild_Widget(toolBar, id), iFalse); | ||
620 | } | ||
621 | /* Disable certain actions. */ | ||
622 | iLabelWidget *back = findMenuItem_Widget(toolBar, "navigate.back"); | ||
623 | iLabelWidget *fwd = findMenuItem_Widget(toolBar, "navigate.forward"); | ||
624 | setFlags_Widget(as_Widget(back), disabled_WidgetFlag, atOldest); | ||
625 | setOutline_LabelWidget(back, atOldest); | ||
626 | setFlags_Widget(as_Widget(fwd), disabled_WidgetFlag, atNewest); | ||
627 | setOutline_LabelWidget(fwd, atNewest); | ||
628 | refresh_Widget(toolBar); | ||
629 | } | ||
630 | iEndCollect(); | ||
468 | } | 631 | } |
469 | 632 | ||
470 | static const char *loadAnimationCStr_(void) { | 633 | static const char *loadAnimationCStr_(void) { |
@@ -502,10 +665,10 @@ static void checkLoadAnimation_Root_(iRoot *d) { | |||
502 | 665 | ||
503 | void updatePadding_Root(iRoot *d) { | 666 | void updatePadding_Root(iRoot *d) { |
504 | if (d == NULL) return; | 667 | if (d == NULL) return; |
505 | iWidget *toolBar = findChild_Widget(d->widget, "toolbar"); | ||
506 | float bottom = 0.0f; | ||
507 | #if defined (iPlatformAppleMobile) | 668 | #if defined (iPlatformAppleMobile) |
669 | iWidget *toolBar = findChild_Widget(d->widget, "toolbar"); | ||
508 | float left, top, right; | 670 | float left, top, right; |
671 | float bottom = 0.0f; | ||
509 | safeAreaInsets_iOS(&left, &top, &right, &bottom); | 672 | safeAreaInsets_iOS(&left, &top, &right, &bottom); |
510 | /* Respect the safe area insets. */ { | 673 | /* Respect the safe area insets. */ { |
511 | setPadding_Widget(findChild_Widget(d->widget, "navdiv"), left, top, right, 0); | 674 | setPadding_Widget(findChild_Widget(d->widget, "navdiv"), left, top, right, 0); |
@@ -514,15 +677,6 @@ void updatePadding_Root(iRoot *d) { | |||
514 | } | 677 | } |
515 | } | 678 | } |
516 | #endif | 679 | #endif |
517 | if (toolBar) { | ||
518 | /* TODO: get this from toolBar height, but it's buggy for some reason */ | ||
519 | const int sidebarBottomPad = isPortrait_App() ? 11 * gap_UI + bottom : 0; | ||
520 | setPadding_Widget(findChild_Widget(d->widget, "sidebar"), 0, 0, 0, sidebarBottomPad); | ||
521 | setPadding_Widget(findChild_Widget(d->widget, "sidebar2"), 0, 0, 0, sidebarBottomPad); | ||
522 | /* TODO: There seems to be unrelated layout glitch in the sidebar where its children | ||
523 | are not arranged correctly until it's hidden and reshown. */ | ||
524 | } | ||
525 | /* Note that `handleNavBarCommands_` also adjusts padding and spacing. */ | ||
526 | } | 680 | } |
527 | 681 | ||
528 | void updateToolbarColors_Root(iRoot *d) { | 682 | void updateToolbarColors_Root(iRoot *d) { |
@@ -536,17 +690,25 @@ void updateToolbarColors_Root(iRoot *d) { | |||
536 | tmBannerBackground_ColorId; | 690 | tmBannerBackground_ColorId; |
537 | setBackgroundColor_Widget(toolBar, bg); | 691 | setBackgroundColor_Widget(toolBar, bg); |
538 | iForEach(ObjectList, i, children_Widget(toolBar)) { | 692 | iForEach(ObjectList, i, children_Widget(toolBar)) { |
539 | iLabelWidget *btn = i.object; | 693 | // iLabelWidget *btn = i.object; |
540 | setTextColor_LabelWidget(i.object, isSidebarVisible ? uiTextDim_ColorId : | 694 | setTextColor_LabelWidget(i.object, isSidebarVisible ? uiTextDim_ColorId : |
541 | tmBannerIcon_ColorId); | 695 | tmBannerIcon_ColorId); |
542 | setBackgroundColor_Widget(i.object, bg); /* using noBackground, but ident has outline */ | 696 | setBackgroundColor_Widget(i.object, bg); /* using noBackground, but ident has outline */ |
543 | } | 697 | } |
698 | setTextColor_LabelWidget(findChild_Widget(toolBar, "toolbar.name"), | ||
699 | isSidebarVisible ? uiTextDim_ColorId : tmBannerIcon_ColorId); | ||
544 | } | 700 | } |
545 | #else | 701 | #else |
546 | iUnused(d); | 702 | iUnused(d); |
547 | #endif | 703 | #endif |
548 | } | 704 | } |
549 | 705 | ||
706 | void notifyVisualOffsetChange_Root(iRoot *d) { | ||
707 | if (d && (d->didAnimateVisualOffsets || d->didChangeArrangement)) { | ||
708 | iNotifyAudience(d, visualOffsetsChanged, RootVisualOffsetsChanged); | ||
709 | } | ||
710 | } | ||
711 | |||
550 | void dismissPortraitPhoneSidebars_Root(iRoot *d) { | 712 | void dismissPortraitPhoneSidebars_Root(iRoot *d) { |
551 | if (deviceType_App() == phone_AppDeviceType && isPortrait_App()) { | 713 | if (deviceType_App() == phone_AppDeviceType && isPortrait_App()) { |
552 | iWidget *sidebar = findChild_Widget(d->widget, "sidebar"); | 714 | iWidget *sidebar = findChild_Widget(d->widget, "sidebar"); |
@@ -617,16 +779,15 @@ static void updateNavBarSize_(iWidget *navBar) { | |||
617 | const iBool isPhone = deviceType_App() == phone_AppDeviceType; | 779 | const iBool isPhone = deviceType_App() == phone_AppDeviceType; |
618 | const iBool isNarrow = !isPhone && isNarrow_Root(navBar->root); | 780 | const iBool isNarrow = !isPhone && isNarrow_Root(navBar->root); |
619 | /* Adjust navbar padding. */ { | 781 | /* Adjust navbar padding. */ { |
620 | int hPad = isPhone && isPortrait_App() ? 0 : (isPhone || isNarrow) ? gap_UI / 2 | 782 | int hPad = isPortraitPhone_App() ? 0 : isPhone || isNarrow ? gap_UI / 2 : (gap_UI * 3 / 2); |
621 | : gap_UI * 3 / 2; | 783 | int vPad = gap_UI * 3 / 2; |
622 | int vPad = gap_UI * 3 / 2; | ||
623 | int topPad = !findWidget_Root("winbar") ? gap_UI / 2 : 0; | 784 | int topPad = !findWidget_Root("winbar") ? gap_UI / 2 : 0; |
624 | setPadding_Widget(navBar, hPad, vPad / 3 + topPad, hPad, vPad / 2); | 785 | setPadding_Widget(navBar, hPad, vPad / 3 + topPad, hPad, vPad / 2); |
625 | } | 786 | } |
626 | /* Button sizing. */ | 787 | /* Button sizing. */ |
627 | if (isNarrow ^ ((flags_Widget(navBar) & tight_WidgetFlag) != 0)) { | 788 | if (isNarrow ^ ((flags_Widget(navBar) & tight_WidgetFlag) != 0)) { |
628 | setFlags_Widget(navBar, tight_WidgetFlag, isNarrow); | 789 | setFlags_Widget(navBar, tight_WidgetFlag, isNarrow); |
629 | showCollapsed_Widget(findChild_Widget(navBar, "navbar.sidebar"), !isNarrow); | 790 | showCollapsed_Widget(findChild_Widget(navBar, "navbar.action3"), !isNarrow); |
630 | iObjectList *lists[] = { | 791 | iObjectList *lists[] = { |
631 | children_Widget(navBar), | 792 | children_Widget(navBar), |
632 | children_Widget(findChild_Widget(navBar, "url")), | 793 | children_Widget(findChild_Widget(navBar, "url")), |
@@ -647,8 +808,8 @@ static void updateNavBarSize_(iWidget *navBar) { | |||
647 | updateUrlInputContentPadding_(navBar); | 808 | updateUrlInputContentPadding_(navBar); |
648 | } | 809 | } |
649 | if (isPhone) { | 810 | if (isPhone) { |
650 | static const char *buttons[] = { "navbar.back", "navbar.forward", "navbar.sidebar", | 811 | static const char *buttons[] = { "navbar.action1", "navbar.action2", "navbar.action3", |
651 | "navbar.ident", "navbar.home", "navbar.menu" }; | 812 | "navbar.action4", "navbar.ident", "navbar.menu" }; |
652 | iWidget *toolBar = findWidget_Root("toolbar"); | 813 | iWidget *toolBar = findWidget_Root("toolbar"); |
653 | setVisualOffset_Widget(toolBar, 0, 0, 0); | 814 | setVisualOffset_Widget(toolBar, 0, 0, 0); |
654 | setFlags_Widget(toolBar, hidden_WidgetFlag, isLandscape_App()); | 815 | setFlags_Widget(toolBar, hidden_WidgetFlag, isLandscape_App()); |
@@ -673,6 +834,22 @@ static void updateNavBarSize_(iWidget *navBar) { | |||
673 | postCommand_Widget(navBar, "layout.changed id:navbar"); | 834 | postCommand_Widget(navBar, "layout.changed id:navbar"); |
674 | } | 835 | } |
675 | 836 | ||
837 | static void updateNavBarActions_(iWidget *navBar) { | ||
838 | const iPrefs *prefs = prefs_App(); | ||
839 | for (size_t i = 0; i < iElemCount(prefs->navbarActions); i++) { | ||
840 | iBeginCollect(); | ||
841 | const int action = prefs->navbarActions[i]; | ||
842 | iLabelWidget *button = | ||
843 | findChild_Widget(navBar, format_CStr("navbar.action%d", i + 1)); | ||
844 | if (button) { | ||
845 | setFlags_Widget(as_Widget(button), disabled_WidgetFlag, iFalse); | ||
846 | updateTextCStr_LabelWidget(button, toolbarActions_Mobile[action].icon); | ||
847 | setCommand_LabelWidget(button, collectNewCStr_String(toolbarActions_Mobile[action].command)); | ||
848 | } | ||
849 | iEndCollect(); | ||
850 | } | ||
851 | } | ||
852 | |||
676 | static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) { | 853 | static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) { |
677 | if (equal_Command(cmd, "window.resized") || equal_Command(cmd, "metrics.changed")) { | 854 | if (equal_Command(cmd, "window.resized") || equal_Command(cmd, "metrics.changed")) { |
678 | updateNavBarSize_(navBar); | 855 | updateNavBarSize_(navBar); |
@@ -685,7 +862,41 @@ static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) { | |||
685 | } | 862 | } |
686 | return iFalse; | 863 | return iFalse; |
687 | } | 864 | } |
865 | else if (equal_Command(cmd, "navbar.actions.changed")) { | ||
866 | updateNavBarActions_(navBar); | ||
867 | return iTrue; | ||
868 | } | ||
869 | else if (equal_Command(cmd, "contextclick")) { | ||
870 | const iRangecc id = range_Command(cmd, "id"); | ||
871 | if (id.start && startsWith_CStr(id.start, "navbar.action")) { | ||
872 | const int buttonIndex = id.end[-1] - '1'; | ||
873 | iArray items; | ||
874 | init_Array(&items, sizeof(iMenuItem)); | ||
875 | pushBack_Array(&items, &(iMenuItem){ "```${menu.toolbar.setaction}" }); | ||
876 | for (size_t i = 0; i < max_ToolbarAction; i++) { | ||
877 | pushBack_Array( | ||
878 | &items, | ||
879 | &(iMenuItem){ | ||
880 | format_CStr( | ||
881 | "%s %s", toolbarActions_Mobile[i].icon, toolbarActions_Mobile[i].label), | ||
882 | 0, | ||
883 | 0, | ||
884 | format_CStr("navbar.action.set arg:%d button:%d", i, buttonIndex) }); | ||
885 | } | ||
886 | openMenu_Widget( | ||
887 | makeMenu_Widget(get_Root()->widget, constData_Array(&items), size_Array(&items)), | ||
888 | coord_Command(cmd)); | ||
889 | deinit_Array(&items); | ||
890 | return iTrue; | ||
891 | } | ||
892 | return iFalse; | ||
893 | } | ||
688 | else if (equal_Command(cmd, "navigate.focus")) { | 894 | else if (equal_Command(cmd, "navigate.focus")) { |
895 | /* The upload dialog has its own path field. */ | ||
896 | if (findWidget_App("upload")) { | ||
897 | postCommand_App("focus.set id:upload.path"); | ||
898 | return iTrue; | ||
899 | } | ||
689 | iWidget *url = findChild_Widget(navBar, "url"); | 900 | iWidget *url = findChild_Widget(navBar, "url"); |
690 | if (focus_Widget() != url) { | 901 | if (focus_Widget() != url) { |
691 | setFocus_Widget(findChild_Widget(navBar, "url")); | 902 | setFocus_Widget(findChild_Widget(navBar, "url")); |
@@ -710,6 +921,8 @@ static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) { | |||
710 | } | 921 | } |
711 | else if (equal_Command(cmd, "navbar.clear")) { | 922 | else if (equal_Command(cmd, "navbar.clear")) { |
712 | iInputWidget *url = findChild_Widget(navBar, "url"); | 923 | iInputWidget *url = findChild_Widget(navBar, "url"); |
924 | setText_InputWidget(url, collectNew_String()); | ||
925 | #if 0 | ||
713 | selectAll_InputWidget(url); | 926 | selectAll_InputWidget(url); |
714 | /* Emulate a Backspace keypress. */ | 927 | /* Emulate a Backspace keypress. */ |
715 | class_InputWidget(url)->processEvent( | 928 | class_InputWidget(url)->processEvent( |
@@ -718,6 +931,7 @@ static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) { | |||
718 | .timestamp = SDL_GetTicks(), | 931 | .timestamp = SDL_GetTicks(), |
719 | .state = SDL_PRESSED, | 932 | .state = SDL_PRESSED, |
720 | .keysym = { .sym = SDLK_BACKSPACE } }); | 933 | .keysym = { .sym = SDLK_BACKSPACE } }); |
934 | #endif | ||
721 | return iTrue; | 935 | return iTrue; |
722 | } | 936 | } |
723 | else if (equal_Command(cmd, "navbar.cancel")) { | 937 | else if (equal_Command(cmd, "navbar.cancel")) { |
@@ -780,6 +994,26 @@ static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) { | |||
780 | dismissPortraitPhoneSidebars_Root(get_Root()); | 994 | dismissPortraitPhoneSidebars_Root(get_Root()); |
781 | updateNavBarIdentity_(navBar); | 995 | updateNavBarIdentity_(navBar); |
782 | updateNavDirButtons_(navBar); | 996 | updateNavDirButtons_(navBar); |
997 | /* Update site-specific used identities. */ { | ||
998 | const iGmIdentity *ident = | ||
999 | identityForUrl_GmCerts(certs_App(), url_DocumentWidget(document_App())); | ||
1000 | if (ident) { | ||
1001 | const iString *site = | ||
1002 | collectNewRange_String(urlRoot_String(canonicalUrl_String(urlStr))); | ||
1003 | const iStringArray *usedIdents = | ||
1004 | strings_SiteSpec(site, usedIdentities_SiteSpecKey); | ||
1005 | const iString *fingerprint = collect_String(hexEncode_Block(&ident->fingerprint)); | ||
1006 | /* Keep this identity at the end of the list. */ | ||
1007 | removeString_SiteSpec(site, usedIdentities_SiteSpecKey, fingerprint); | ||
1008 | insertString_SiteSpec(site, usedIdentities_SiteSpecKey, fingerprint); | ||
1009 | /* Keep the list short. */ | ||
1010 | while (size_StringArray(usedIdents) > 5) { | ||
1011 | removeString_SiteSpec(site, | ||
1012 | usedIdentities_SiteSpecKey, | ||
1013 | constAt_StringArray(usedIdents, 0)); | ||
1014 | } | ||
1015 | } | ||
1016 | } | ||
783 | /* Icon updates should be limited to automatically chosen icons if the user | 1017 | /* Icon updates should be limited to automatically chosen icons if the user |
784 | is allowed to pick their own in the future. */ | 1018 | is allowed to pick their own in the future. */ |
785 | if (updateBookmarkIcon_Bookmarks(bookmarks_App(), urlStr, | 1019 | if (updateBookmarkIcon_Bookmarks(bookmarks_App(), urlStr, |
@@ -860,7 +1094,14 @@ static iBool handleSearchBarCommands_(iWidget *searchBar, const char *cmd) { | |||
860 | else if (equal_Command(cmd, "focus.gained")) { | 1094 | else if (equal_Command(cmd, "focus.gained")) { |
861 | if (pointer_Command(cmd) == findChild_Widget(searchBar, "find.input")) { | 1095 | if (pointer_Command(cmd) == findChild_Widget(searchBar, "find.input")) { |
862 | if (!isVisible_Widget(searchBar)) { | 1096 | if (!isVisible_Widget(searchBar)) { |
1097 | /* InputWidget will unfocus itself if there isn't enough space for editing | ||
1098 | text. A collapsed widget will not have been arranged yet, so on the first | ||
1099 | time the widget will just be unfocused immediately. */ | ||
1100 | const iBool wasArranged = area_Rect(bounds_Widget(searchBar)) > 0; | ||
863 | showCollapsed_Widget(searchBar, iTrue); | 1101 | showCollapsed_Widget(searchBar, iTrue); |
1102 | if (!wasArranged) { | ||
1103 | postCommand_App("focus.set id:find.input"); | ||
1104 | } | ||
864 | } | 1105 | } |
865 | } | 1106 | } |
866 | } | 1107 | } |
@@ -878,14 +1119,21 @@ static iBool handleSearchBarCommands_(iWidget *searchBar, const char *cmd) { | |||
878 | } | 1119 | } |
879 | 1120 | ||
880 | #if defined (iPlatformMobile) | 1121 | #if defined (iPlatformMobile) |
881 | static void dismissSidebar_(iWidget *sidebar, const char *toolButtonId) { | 1122 | |
882 | if (isVisible_Widget(sidebar)) { | 1123 | static void updateToolBarActions_(iWidget *toolBar) { |
883 | postCommandf_App("%s.toggle", cstr_String(id_Widget(sidebar))); | 1124 | const iPrefs *prefs = prefs_App(); |
884 | // if (toolButtonId) { | 1125 | for (int i = 0; i < 2; i++) { |
885 | // setFlags_Widget(findWidget_App(toolButtonId), noBackground_WidgetFlag, iTrue); | 1126 | const int action = prefs->toolbarActions[i]; |
886 | // } | 1127 | iLabelWidget *button = |
887 | setVisualOffset_Widget(sidebar, height_Widget(sidebar), 250, easeIn_AnimFlag); | 1128 | findChild_Widget(toolBar, i == 0 ? "toolbar.action1" : "toolbar.action2"); |
1129 | if (button) { | ||
1130 | setFlags_Widget(as_Widget(button), disabled_WidgetFlag, iFalse); | ||
1131 | setOutline_LabelWidget(button, iFalse); | ||
1132 | updateTextCStr_LabelWidget(button, toolbarActions_Mobile[action].icon); | ||
1133 | setCommand_LabelWidget(button, collectNewCStr_String(toolbarActions_Mobile[action].command)); | ||
1134 | } | ||
888 | } | 1135 | } |
1136 | refresh_Widget(toolBar); | ||
889 | } | 1137 | } |
890 | 1138 | ||
891 | static iBool handleToolBarCommands_(iWidget *toolBar, const char *cmd) { | 1139 | static iBool handleToolBarCommands_(iWidget *toolBar, const char *cmd) { |
@@ -897,57 +1145,20 @@ static iBool handleToolBarCommands_(iWidget *toolBar, const char *cmd) { | |||
897 | return iTrue; | 1145 | return iTrue; |
898 | } | 1146 | } |
899 | else if (equal_Command(cmd, "toolbar.showview")) { | 1147 | else if (equal_Command(cmd, "toolbar.showview")) { |
900 | /* TODO: Clean this up. */ | ||
901 | iWidget *sidebar = findWidget_App("sidebar"); | ||
902 | iWidget *sidebar2 = findWidget_App("sidebar2"); | ||
903 | dismissSidebar_(sidebar2, "toolbar.ident"); | ||
904 | const iBool isVisible = isVisible_Widget(sidebar); | ||
905 | // setFlags_Widget(findChild_Widget(toolBar, "toolbar.view"), noBackground_WidgetFlag, | ||
906 | // isVisible); | ||
907 | /* If a sidebar hasn't been shown yet, it's height is zero. */ | ||
908 | const int viewHeight = size_Root(get_Root()).y; | ||
909 | if (arg_Command(cmd) >= 0) { | 1148 | if (arg_Command(cmd) >= 0) { |
910 | postCommandf_App("sidebar.mode arg:%d show:1", arg_Command(cmd)); | 1149 | postCommandf_App("sidebar.mode arg:%d show:1", arg_Command(cmd)); |
911 | // if (!isVisible) { | ||
912 | // setVisualOffset_Widget(sidebar, viewHeight, 0, 0); | ||
913 | // setVisualOffset_Widget(sidebar, 0, 400, easeOut_AnimFlag | softer_AnimFlag); | ||
914 | // } | ||
915 | } | 1150 | } |
916 | else { | 1151 | else { |
917 | postCommandf_App("sidebar.toggle"); | 1152 | postCommandf_App("sidebar.toggle"); |
918 | // if (isVisible) { | ||
919 | // setVisualOffset_Widget(sidebar, height_Widget(sidebar), 250, easeIn_AnimFlag); | ||
920 | // } | ||
921 | // else { | ||
922 | // setVisualOffset_Widget(sidebar, viewHeight, 0, 0); | ||
923 | // setVisualOffset_Widget(sidebar, 0, 400, easeOut_AnimFlag | softer_AnimFlag); | ||
924 | // } | ||
925 | } | 1153 | } |
926 | return iTrue; | 1154 | return iTrue; |
927 | } | 1155 | } |
928 | else if (equal_Command(cmd, "toolbar.showident")) { | 1156 | else if (equal_Command(cmd, "toolbar.showident")) { |
929 | /* TODO: Clean this up. */ | 1157 | iWidget *sidebar = findWidget_App("sidebar"); |
930 | iWidget *sidebar = findWidget_App("sidebar"); | ||
931 | iWidget *sidebar2 = findWidget_App("sidebar2"); | ||
932 | //dismissSidebar_(sidebar, "toolbar.view"); | ||
933 | if (isVisible_Widget(sidebar)) { | 1158 | if (isVisible_Widget(sidebar)) { |
934 | postCommandf_App("sidebar.toggle"); | 1159 | postCommandf_App("sidebar.toggle"); |
935 | } | 1160 | } |
936 | const iBool isVisible = isVisible_Widget(sidebar2); | 1161 | postCommand_App("preferences idents:1"); |
937 | // setFlags_Widget(findChild_Widget(toolBar, "toolbar.ident"), noBackground_WidgetFlag, | ||
938 | // isVisible); | ||
939 | /* If a sidebar hasn't been shown yet, it's height is zero. */ | ||
940 | const int viewHeight = size_Root(get_Root()).y; | ||
941 | if (isVisible) { | ||
942 | dismissSidebar_(sidebar2, NULL); | ||
943 | } | ||
944 | else { | ||
945 | postCommand_App("sidebar2.mode arg:3 show:1"); | ||
946 | int offset = height_Widget(sidebar2); | ||
947 | if (offset == 0) offset = size_Root(get_Root()).y; | ||
948 | setVisualOffset_Widget(sidebar2, offset, 0, 0); | ||
949 | setVisualOffset_Widget(sidebar2, 0, 400, easeOut_AnimFlag | softer_AnimFlag); | ||
950 | } | ||
951 | return iTrue; | 1162 | return iTrue; |
952 | } | 1163 | } |
953 | else if (equal_Command(cmd, "sidebar.mode.changed")) { | 1164 | else if (equal_Command(cmd, "sidebar.mode.changed")) { |
@@ -955,8 +1166,13 @@ static iBool handleToolBarCommands_(iWidget *toolBar, const char *cmd) { | |||
955 | updateTextCStr_LabelWidget(viewTool, icon_SidebarMode(arg_Command(cmd))); | 1166 | updateTextCStr_LabelWidget(viewTool, icon_SidebarMode(arg_Command(cmd))); |
956 | return iFalse; | 1167 | return iFalse; |
957 | } | 1168 | } |
1169 | else if (equal_Command(cmd, "toolbar.actions.changed")) { | ||
1170 | updateToolBarActions_(toolBar); | ||
1171 | return iFalse; | ||
1172 | } | ||
958 | return iFalse; | 1173 | return iFalse; |
959 | } | 1174 | } |
1175 | |||
960 | #endif /* defined (iPlatformMobile) */ | 1176 | #endif /* defined (iPlatformMobile) */ |
961 | 1177 | ||
962 | static iLabelWidget *newLargeIcon_LabelWidget(const char *text, const char *cmd) { | 1178 | static iLabelWidget *newLargeIcon_LabelWidget(const char *text, const char *cmd) { |
@@ -987,40 +1203,22 @@ void updateMetrics_Root(iRoot *d) { | |||
987 | setFixedSize_Widget(appClose, appMin->rect.size); | 1203 | setFixedSize_Widget(appClose, appMin->rect.size); |
988 | setFixedSize_Widget(appIcon, init_I2(appIconSize_Root(), appMin->rect.size.y)); | 1204 | setFixedSize_Widget(appIcon, init_I2(appIconSize_Root(), appMin->rect.size.y)); |
989 | } | 1205 | } |
990 | iWidget *navBar = findChild_Widget(d->widget, "navbar"); | 1206 | iWidget *navBar = findChild_Widget(d->widget, "navbar"); |
991 | // iWidget *lock = findChild_Widget(navBar, "navbar.lock"); | 1207 | iWidget *url = findChild_Widget(d->widget, "url"); |
992 | iWidget *url = findChild_Widget(d->widget, "url"); | 1208 | iWidget *rightEmbed = findChild_Widget(navBar, "url.rightembed"); |
993 | iWidget *rightEmbed = findChild_Widget(navBar, "url.rightembed"); | 1209 | iWidget *embedPad = findChild_Widget(navBar, "url.embedpad"); |
994 | iWidget *embedPad = findChild_Widget(navBar, "url.embedpad"); | 1210 | iWidget *urlButtons = findChild_Widget(navBar, "url.buttons"); |
995 | iWidget *urlButtons = findChild_Widget(navBar, "url.buttons"); | 1211 | iLabelWidget *idName = findChild_Widget(d->widget, "toolbar.name"); |
996 | setPadding_Widget(as_Widget(url), 0, gap_UI, 0, gap_UI); | 1212 | setPadding_Widget(as_Widget(url), 0, gap_UI, 0, gap_UI); |
997 | navBar->rect.size.y = 0; /* recalculate height based on children (FIXME: shouldn't be needed) */ | 1213 | // navBar->rect.size.y = 0; /* recalculate height based on children (FIXME: shouldn't be needed) */ |
998 | // updateSize_LabelWidget((iLabelWidget *) lock); | ||
999 | // updateSize_LabelWidget((iLabelWidget *) findChild_Widget(navBar, "reload")); | ||
1000 | // arrange_Widget(urlButtons); | ||
1001 | setFixedSize_Widget(embedPad, init_I2(width_Widget(urlButtons) + gap_UI / 2, 1)); | 1214 | setFixedSize_Widget(embedPad, init_I2(width_Widget(urlButtons) + gap_UI / 2, 1)); |
1002 | // setContentPadding_InputWidget((iInputWidget *) url, width_Widget(lock) * 0.75, | ||
1003 | // width_Widget(lock) * 0.75); | ||
1004 | rightEmbed->rect.pos.y = gap_UI; | 1215 | rightEmbed->rect.pos.y = gap_UI; |
1005 | updatePadding_Root(d); | 1216 | updatePadding_Root(d); |
1006 | arrange_Widget(d->widget); | 1217 | arrange_Widget(d->widget); |
1007 | updateUrlInputContentPadding_(navBar); | 1218 | updateUrlInputContentPadding_(navBar); |
1008 | /* Position the toolbar identity name label manually. */ { | 1219 | if (idName) { |
1009 | iLabelWidget *idName = findChild_Widget(d->widget, "toolbar.name"); | 1220 | setFixedSize_Widget(as_Widget(idName), |
1010 | if (idName) { | 1221 | init_I2(-1, 2 * gap_UI + lineHeight_Text(uiLabelTiny_FontId))); |
1011 | const iWidget *toolBar = findChild_Widget(d->widget, "toolbar"); | ||
1012 | const iWidget *viewButton = findChild_Widget(d->widget, "toolbar.view"); | ||
1013 | const iWidget *idButton = findChild_Widget(toolBar, "toolbar.ident"); | ||
1014 | const int font = uiLabelTiny_FontId; | ||
1015 | setFont_LabelWidget(idName, font); | ||
1016 | setPos_Widget(as_Widget(idName), | ||
1017 | windowToLocal_Widget(as_Widget(idName), | ||
1018 | init_I2(left_Rect(bounds_Widget(idButton)), | ||
1019 | bottom_Rect(bounds_Widget(viewButton)) - | ||
1020 | lineHeight_Text(font) - gap_UI / 2))); | ||
1021 | setFixedSize_Widget(as_Widget(idName), init_I2(width_Widget(idButton), | ||
1022 | lineHeight_Text(font))); | ||
1023 | } | ||
1024 | } | 1222 | } |
1025 | postRefresh_App(); | 1223 | postRefresh_App(); |
1026 | } | 1224 | } |
@@ -1044,11 +1242,9 @@ void createUserInterface_Root(iRoot *d) { | |||
1044 | setFlags_Widget( | 1242 | setFlags_Widget( |
1045 | root, resizeChildren_WidgetFlag | fixedSize_WidgetFlag | focusRoot_WidgetFlag, iTrue); | 1243 | root, resizeChildren_WidgetFlag | fixedSize_WidgetFlag | focusRoot_WidgetFlag, iTrue); |
1046 | setCommandHandler_Widget(root, handleRootCommands_); | 1244 | setCommandHandler_Widget(root, handleRootCommands_); |
1047 | |||
1048 | iWidget *div = makeVDiv_Widget(); | 1245 | iWidget *div = makeVDiv_Widget(); |
1049 | setId_Widget(div, "navdiv"); | 1246 | setId_Widget(div, "navdiv"); |
1050 | addChild_Widget(root, iClob(div)); | 1247 | addChild_Widget(root, iClob(div)); |
1051 | |||
1052 | #if defined (LAGRANGE_ENABLE_CUSTOM_FRAME) | 1248 | #if defined (LAGRANGE_ENABLE_CUSTOM_FRAME) |
1053 | /* Window title bar. */ | 1249 | /* Window title bar. */ |
1054 | if (prefs_App()->customFrame) { | 1250 | if (prefs_App()->customFrame) { |
@@ -1117,14 +1313,14 @@ void createUserInterface_Root(iRoot *d) { | |||
1117 | addUnsplitButton_(navBar); | 1313 | addUnsplitButton_(navBar); |
1118 | #endif | 1314 | #endif |
1119 | iWidget *navBack; | 1315 | iWidget *navBack; |
1120 | setId_Widget(navBack = addChildFlags_Widget(navBar, iClob(newIcon_LabelWidget(backArrow_Icon, 0, 0, "navigate.back")), collapse_WidgetFlag), "navbar.back"); | 1316 | setId_Widget(navBack = addChildFlags_Widget(navBar, iClob(newIcon_LabelWidget(backArrow_Icon, 0, 0, "navigate.back")), collapse_WidgetFlag), "navbar.action1"); |
1121 | setId_Widget(addChildFlags_Widget(navBar, iClob(newIcon_LabelWidget(forwardArrow_Icon, 0, 0, "navigate.forward")), collapse_WidgetFlag), "navbar.forward"); | 1317 | setId_Widget(addChildFlags_Widget(navBar, iClob(newIcon_LabelWidget(forwardArrow_Icon, 0, 0, "navigate.forward")), collapse_WidgetFlag), "navbar.action2"); |
1122 | /* Button for toggling the left sidebar. */ | 1318 | /* Button for toggling the left sidebar. */ |
1123 | setId_Widget(addChildFlags_Widget( | 1319 | setId_Widget(addChildFlags_Widget( |
1124 | navBar, | 1320 | navBar, |
1125 | iClob(newIcon_LabelWidget(leftHalf_Icon, 0, 0, "sidebar.toggle")), | 1321 | iClob(newIcon_LabelWidget(leftHalf_Icon, 0, 0, "sidebar.toggle")), |
1126 | collapse_WidgetFlag), | 1322 | collapse_WidgetFlag), |
1127 | "navbar.sidebar"); | 1323 | "navbar.action3"); |
1128 | addChildFlags_Widget(navBar, iClob(new_Widget()), expand_WidgetFlag); | 1324 | addChildFlags_Widget(navBar, iClob(new_Widget()), expand_WidgetFlag); |
1129 | iInputWidget *url; | 1325 | iInputWidget *url; |
1130 | /* URL input field. */ { | 1326 | /* URL input field. */ { |
@@ -1245,22 +1441,25 @@ void createUserInterface_Root(iRoot *d) { | |||
1245 | { book_Icon " ${menu.page.import}", 0, 0, "bookmark.links confirm:1" }, | 1441 | { book_Icon " ${menu.page.import}", 0, 0, "bookmark.links confirm:1" }, |
1246 | { globe_Icon " ${menu.page.translate}", 0, 0, "document.translate" }, | 1442 | { globe_Icon " ${menu.page.translate}", 0, 0, "document.translate" }, |
1247 | { upload_Icon " ${menu.page.upload}", 0, 0, "document.upload" }, | 1443 | { upload_Icon " ${menu.page.upload}", 0, 0, "document.upload" }, |
1444 | { "${menu.page.upload.edit}", 0, 0, "document.upload copy:1" }, | ||
1248 | { "---" }, | 1445 | { "---" }, |
1249 | { "${menu.page.copyurl}", 0, 0, "document.copylink" }, | 1446 | { "${menu.page.copyurl}", 0, 0, "document.copylink" }, |
1250 | { "${menu.page.copysource}", 'c', KMOD_PRIMARY, "copy" }, | 1447 | { "${menu.page.copysource}", 'c', KMOD_PRIMARY, "copy" }, |
1251 | { download_Icon " " saveToDownloads_Label, SDLK_s, KMOD_PRIMARY, "document.save" } }, | 1448 | { download_Icon " " saveToDownloads_Label, SDLK_s, KMOD_PRIMARY, "document.save" } }, |
1252 | 12); | 1449 | 14); |
1253 | setId_Widget(as_Widget(pageMenuButton), "pagemenubutton"); | 1450 | setId_Widget(as_Widget(pageMenuButton), "pagemenubutton"); |
1254 | setFont_LabelWidget(pageMenuButton, uiContentBold_FontId); | 1451 | setFont_LabelWidget(pageMenuButton, uiContentBold_FontId); |
1255 | setAlignVisually_LabelWidget(pageMenuButton, iTrue); | 1452 | setAlignVisually_LabelWidget(pageMenuButton, iTrue); |
1256 | addChildFlags_Widget(urlButtons, iClob(pageMenuButton), | 1453 | addChildFlags_Widget(urlButtons, iClob(pageMenuButton), |
1257 | embedFlags | tight_WidgetFlag | collapse_WidgetFlag); | 1454 | embedFlags | tight_WidgetFlag | collapse_WidgetFlag | |
1455 | resizeToParentHeight_WidgetFlag); | ||
1258 | updateSize_LabelWidget(pageMenuButton); | 1456 | updateSize_LabelWidget(pageMenuButton); |
1259 | } | 1457 | } |
1260 | /* Reload button. */ { | 1458 | /* Reload button. */ { |
1261 | iLabelWidget *reload = newIcon_LabelWidget(reloadCStr_, 0, 0, "navigate.reload"); | 1459 | iLabelWidget *reload = newIcon_LabelWidget(reloadCStr_, 0, 0, "navigate.reload"); |
1262 | setId_Widget(as_Widget(reload), "reload"); | 1460 | setId_Widget(as_Widget(reload), "reload"); |
1263 | addChildFlags_Widget(urlButtons, iClob(reload), embedFlags | collapse_WidgetFlag); | 1461 | addChildFlags_Widget(urlButtons, iClob(reload), embedFlags | collapse_WidgetFlag | |
1462 | resizeToParentHeight_WidgetFlag); | ||
1264 | updateSize_LabelWidget(reload); | 1463 | updateSize_LabelWidget(reload); |
1265 | } | 1464 | } |
1266 | addChildFlags_Widget(as_Widget(url), iClob(urlButtons), moveToParentRightEdge_WidgetFlag); | 1465 | addChildFlags_Widget(as_Widget(url), iClob(urlButtons), moveToParentRightEdge_WidgetFlag); |
@@ -1268,17 +1467,16 @@ void createUserInterface_Root(iRoot *d) { | |||
1268 | setId_Widget(addChild_Widget(rightEmbed, iClob(makePadding_Widget(0))), "url.embedpad"); | 1467 | setId_Widget(addChild_Widget(rightEmbed, iClob(makePadding_Widget(0))), "url.embedpad"); |
1269 | } | 1468 | } |
1270 | /* The active identity menu. */ { | 1469 | /* The active identity menu. */ { |
1271 | iLabelWidget *idMenu = makeMenuButton_LabelWidget( | 1470 | iLabelWidget *idButton = new_LabelWidget(person_Icon, "identmenu.open"); |
1272 | "\U0001f464", identityButtonMenuItems_, iElemCount(identityButtonMenuItems_)); | 1471 | setAlignVisually_LabelWidget(idButton, iTrue); |
1273 | setAlignVisually_LabelWidget(idMenu, iTrue); | 1472 | setId_Widget(addChildFlags_Widget(navBar, iClob(idButton), collapse_WidgetFlag), "navbar.ident"); |
1274 | setId_Widget(addChildFlags_Widget(navBar, iClob(idMenu), collapse_WidgetFlag), "navbar.ident"); | ||
1275 | } | 1473 | } |
1276 | addChildFlags_Widget(navBar, iClob(new_Widget()), expand_WidgetFlag); | 1474 | addChildFlags_Widget(navBar, iClob(new_Widget()), expand_WidgetFlag); |
1277 | setId_Widget(addChildFlags_Widget(navBar, | 1475 | setId_Widget(addChildFlags_Widget(navBar, |
1278 | iClob(newIcon_LabelWidget( | 1476 | iClob(newIcon_LabelWidget( |
1279 | home_Icon, SDLK_h, KMOD_PRIMARY | KMOD_SHIFT, "navigate.home")), | 1477 | home_Icon, 0, 0, "navigate.home")), |
1280 | collapse_WidgetFlag), | 1478 | collapse_WidgetFlag), |
1281 | "navbar.home"); | 1479 | "navbar.action4"); |
1282 | #if defined (iPlatformMobile) | 1480 | #if defined (iPlatformMobile) |
1283 | const iBool isPhone = (deviceType_App() == phone_AppDeviceType); | 1481 | const iBool isPhone = (deviceType_App() == phone_AppDeviceType); |
1284 | #endif | 1482 | #endif |
@@ -1291,6 +1489,7 @@ void createUserInterface_Root(iRoot *d) { | |||
1291 | iLabelWidget *navMenu = | 1489 | iLabelWidget *navMenu = |
1292 | makeMenuButton_LabelWidget(menu_Icon, navMenuItems_, iElemCount(navMenuItems_)); | 1490 | makeMenuButton_LabelWidget(menu_Icon, navMenuItems_, iElemCount(navMenuItems_)); |
1293 | # endif | 1491 | # endif |
1492 | setCommand_LabelWidget(navMenu, collectNewCStr_String("menu.open under:1")); | ||
1294 | setAlignVisually_LabelWidget(navMenu, iTrue); | 1493 | setAlignVisually_LabelWidget(navMenu, iTrue); |
1295 | setId_Widget(addChildFlags_Widget(navBar, iClob(navMenu), collapse_WidgetFlag), "navbar.menu"); | 1494 | setId_Widget(addChildFlags_Widget(navBar, iClob(navMenu), collapse_WidgetFlag), "navbar.menu"); |
1296 | #endif | 1495 | #endif |
@@ -1322,17 +1521,18 @@ void createUserInterface_Root(iRoot *d) { | |||
1322 | "newtab"); | 1521 | "newtab"); |
1323 | } | 1522 | } |
1324 | /* Sidebars. */ { | 1523 | /* Sidebars. */ { |
1325 | iWidget *content = findChild_Widget(root, "tabs.content"); | ||
1326 | iSidebarWidget *sidebar1 = new_SidebarWidget(left_SidebarSide); | 1524 | iSidebarWidget *sidebar1 = new_SidebarWidget(left_SidebarSide); |
1327 | addChildPos_Widget(content, iClob(sidebar1), front_WidgetAddPos); | ||
1328 | iSidebarWidget *sidebar2 = new_SidebarWidget(right_SidebarSide); | ||
1329 | if (deviceType_App() != phone_AppDeviceType) { | 1525 | if (deviceType_App() != phone_AppDeviceType) { |
1526 | /* Sidebars are next to the tab content. */ | ||
1527 | iWidget *content = findChild_Widget(root, "tabs.content"); | ||
1528 | addChildPos_Widget(content, iClob(sidebar1), front_WidgetAddPos); | ||
1529 | iSidebarWidget *sidebar2 = new_SidebarWidget(right_SidebarSide); | ||
1330 | addChildPos_Widget(content, iClob(sidebar2), back_WidgetAddPos); | 1530 | addChildPos_Widget(content, iClob(sidebar2), back_WidgetAddPos); |
1331 | } | 1531 | } |
1332 | else { | 1532 | else { |
1333 | /* The identities sidebar is always in the main area. */ | 1533 | /* Sidebar is a slide-over sheet. */ |
1334 | addChild_Widget(findChild_Widget(root, "stack"), iClob(sidebar2)); | 1534 | addChild_Widget(root, iClob(sidebar1)); |
1335 | setFlags_Widget(as_Widget(sidebar2), hidden_WidgetFlag, iTrue); | 1535 | setFlags_Widget(as_Widget(sidebar1), hidden_WidgetFlag, iTrue); |
1336 | } | 1536 | } |
1337 | } | 1537 | } |
1338 | /* Lookup results. */ { | 1538 | /* Lookup results. */ { |
@@ -1386,40 +1586,46 @@ void createUserInterface_Root(iRoot *d) { | |||
1386 | commandOnClick_WidgetFlag | | 1586 | commandOnClick_WidgetFlag | |
1387 | drawBackgroundToBottom_WidgetFlag, iTrue); | 1587 | drawBackgroundToBottom_WidgetFlag, iTrue); |
1388 | setId_Widget(addChildFlags_Widget(toolBar, | 1588 | setId_Widget(addChildFlags_Widget(toolBar, |
1389 | iClob(newLargeIcon_LabelWidget(backArrow_Icon, "navigate.back")), | 1589 | iClob(newLargeIcon_LabelWidget("", "...")), |
1390 | frameless_WidgetFlag), | 1590 | frameless_WidgetFlag), |
1391 | "toolbar.back"); | 1591 | "toolbar.action1"); |
1392 | setId_Widget(addChildFlags_Widget(toolBar, | 1592 | setId_Widget(addChildFlags_Widget(toolBar, |
1393 | iClob(newLargeIcon_LabelWidget(forwardArrow_Icon, "navigate.forward")), | 1593 | iClob(newLargeIcon_LabelWidget("", "...")), |
1394 | frameless_WidgetFlag), | ||
1395 | "toolbar.forward"); | ||
1396 | setId_Widget(addChildFlags_Widget(toolBar, | ||
1397 | iClob(newLargeIcon_LabelWidget("\U0001f464", "toolbar.showident")), | ||
1398 | frameless_WidgetFlag), | 1594 | frameless_WidgetFlag), |
1595 | "toolbar.action2"); | ||
1596 | iWidget *identButton; | ||
1597 | setId_Widget(identButton = addChildFlags_Widget( | ||
1598 | toolBar, | ||
1599 | iClob(newLargeIcon_LabelWidget("\U0001f464", "identmenu.open")), | ||
1600 | frameless_WidgetFlag | fixedHeight_WidgetFlag), | ||
1399 | "toolbar.ident"); | 1601 | "toolbar.ident"); |
1400 | setId_Widget(addChildFlags_Widget(toolBar, | 1602 | setId_Widget(addChildFlags_Widget(toolBar, |
1401 | iClob(newLargeIcon_LabelWidget(book_Icon, "toolbar.showview arg:-1")), | 1603 | iClob(newLargeIcon_LabelWidget(book_Icon, "toolbar.showview arg:-1")), |
1402 | frameless_WidgetFlag | commandOnClick_WidgetFlag), | 1604 | frameless_WidgetFlag | commandOnClick_WidgetFlag), |
1403 | "toolbar.view"); | 1605 | "toolbar.view"); |
1404 | setId_Widget(addChildFlags_Widget(toolBar, | 1606 | iLabelWidget *idName; |
1405 | iClob(new_LabelWidget("", "toolbar.showident")), | 1607 | setId_Widget(addChildFlags_Widget(identButton, |
1608 | iClob(idName = new_LabelWidget("", NULL)), | ||
1406 | frameless_WidgetFlag | | 1609 | frameless_WidgetFlag | |
1407 | noBackground_WidgetFlag | | 1610 | noBackground_WidgetFlag | |
1408 | fixedPosition_WidgetFlag | | 1611 | moveToParentBottomEdge_WidgetFlag | |
1612 | resizeToParentWidth_WidgetFlag | ||
1613 | /*fixedPosition_WidgetFlag | | ||
1409 | fixedSize_WidgetFlag | | 1614 | fixedSize_WidgetFlag | |
1410 | ignoreForParentWidth_WidgetFlag | | 1615 | ignoreForParentWidth_WidgetFlag | |
1411 | ignoreForParentHeight_WidgetFlag), | 1616 | ignoreForParentHeight_WidgetFlag*/), |
1412 | "toolbar.name"); | 1617 | "toolbar.name"); |
1618 | setFont_LabelWidget(idName, uiLabelTiny_FontId); | ||
1413 | iLabelWidget *menuButton = makeMenuButton_LabelWidget(menu_Icon, phoneNavMenuItems_, | 1619 | iLabelWidget *menuButton = makeMenuButton_LabelWidget(menu_Icon, phoneNavMenuItems_, |
1414 | iElemCount(phoneNavMenuItems_)); | 1620 | iElemCount(phoneNavMenuItems_)); |
1415 | setFont_LabelWidget(menuButton, uiLabelLarge_FontId); | 1621 | setFont_LabelWidget(menuButton, uiLabelLarge_FontId); |
1416 | setId_Widget(as_Widget(menuButton), "toolbar.navmenu"); | 1622 | setId_Widget(as_Widget(menuButton), "toolbar.navmenu"); |
1417 | addChildFlags_Widget(toolBar, iClob(menuButton), frameless_WidgetFlag); | 1623 | addChildFlags_Widget(toolBar, iClob(menuButton), frameless_WidgetFlag); |
1418 | iForEach(ObjectList, i, children_Widget(toolBar)) { | 1624 | iForEach(ObjectList, i, children_Widget(toolBar)) { |
1419 | iLabelWidget *btn = i.object; | ||
1420 | setFlags_Widget(i.object, noBackground_WidgetFlag, iTrue); | 1625 | setFlags_Widget(i.object, noBackground_WidgetFlag, iTrue); |
1421 | } | 1626 | } |
1422 | updateToolbarColors_Root(d); | 1627 | updateToolbarColors_Root(d); |
1628 | updateToolBarActions_(toolBar); | ||
1423 | const iMenuItem items[] = { | 1629 | const iMenuItem items[] = { |
1424 | { book_Icon " ${sidebar.bookmarks}", 0, 0, "toolbar.showview arg:0" }, | 1630 | { book_Icon " ${sidebar.bookmarks}", 0, 0, "toolbar.showview arg:0" }, |
1425 | { star_Icon " ${sidebar.feeds}", 0, 0, "toolbar.showview arg:1" }, | 1631 | { star_Icon " ${sidebar.feeds}", 0, 0, "toolbar.showview arg:1" }, |
@@ -1431,6 +1637,7 @@ void createUserInterface_Root(iRoot *d) { | |||
1431 | setId_Widget(menu, "toolbar.menu"); /* view menu */ | 1637 | setId_Widget(menu, "toolbar.menu"); /* view menu */ |
1432 | } | 1638 | } |
1433 | #endif | 1639 | #endif |
1640 | updateNavBarActions_(navBar); | ||
1434 | updatePadding_Root(d); | 1641 | updatePadding_Root(d); |
1435 | /* Global context menus. */ { | 1642 | /* Global context menus. */ { |
1436 | iWidget *tabsMenu = makeMenu_Widget( | 1643 | iWidget *tabsMenu = makeMenu_Widget( |
@@ -1474,6 +1681,12 @@ void createUserInterface_Root(iRoot *d) { | |||
1474 | { select_Icon " ${menu.selectall}", 0, 0, "input.selectall" }, | 1681 | { select_Icon " ${menu.selectall}", 0, 0, "input.selectall" }, |
1475 | }, 8); | 1682 | }, 8); |
1476 | #endif | 1683 | #endif |
1684 | if (deviceType_App() == phone_AppDeviceType) { | ||
1685 | /* Small screen; conserve space by removing the Cancel item. */ | ||
1686 | iRelease(removeChild_Widget(clipMenu, lastChild_Widget(clipMenu))); | ||
1687 | iRelease(removeChild_Widget(clipMenu, lastChild_Widget(clipMenu))); | ||
1688 | iRelease(removeChild_Widget(clipMenu, lastChild_Widget(clipMenu))); | ||
1689 | } | ||
1477 | iWidget *splitMenu = makeMenu_Widget(root, (iMenuItem[]){ | 1690 | iWidget *splitMenu = makeMenu_Widget(root, (iMenuItem[]){ |
1478 | { "${menu.split.merge}", '1', 0, "ui.split arg:0" }, | 1691 | { "${menu.split.merge}", '1', 0, "ui.split arg:0" }, |
1479 | { "${menu.split.swap}", SDLK_x, 0, "ui.split swap:1" }, | 1692 | { "${menu.split.swap}", SDLK_x, 0, "ui.split swap:1" }, |
@@ -1493,6 +1706,7 @@ void createUserInterface_Root(iRoot *d) { | |||
1493 | setId_Widget(splitMenu, "splitmenu"); | 1706 | setId_Widget(splitMenu, "splitmenu"); |
1494 | } | 1707 | } |
1495 | /* Global keyboard shortcuts. */ { | 1708 | /* Global keyboard shortcuts. */ { |
1709 | addAction_Widget(root, SDLK_h, KMOD_PRIMARY | KMOD_SHIFT, "navigate.home"); | ||
1496 | addAction_Widget(root, 'l', KMOD_PRIMARY, "navigate.focus"); | 1710 | addAction_Widget(root, 'l', KMOD_PRIMARY, "navigate.focus"); |
1497 | addAction_Widget(root, 'f', KMOD_PRIMARY, "focus.set id:find.input"); | 1711 | addAction_Widget(root, 'f', KMOD_PRIMARY, "focus.set id:find.input"); |
1498 | addAction_Widget(root, '1', KMOD_PRIMARY, "sidebar.mode arg:0 toggle:1"); | 1712 | addAction_Widget(root, '1', KMOD_PRIMARY, "sidebar.mode arg:0 toggle:1"); |
@@ -1558,9 +1772,10 @@ iRect safeRect_Root(const iRoot *d) { | |||
1558 | 1772 | ||
1559 | iRect visibleRect_Root(const iRoot *d) { | 1773 | iRect visibleRect_Root(const iRoot *d) { |
1560 | iRect visRect = rect_Root(d); | 1774 | iRect visRect = rect_Root(d); |
1775 | float bottom = 0.0f; | ||
1561 | #if defined (iPlatformAppleMobile) | 1776 | #if defined (iPlatformAppleMobile) |
1562 | /* TODO: Check this on device... Maybe DisplayUsableBounds would be good here, too? */ | 1777 | /* TODO: Check this on device... Maybe DisplayUsableBounds would be good here, too? */ |
1563 | float left, top, right, bottom; | 1778 | float left, top, right; |
1564 | safeAreaInsets_iOS(&left, &top, &right, &bottom); | 1779 | safeAreaInsets_iOS(&left, &top, &right, &bottom); |
1565 | visRect.pos.x = (int) left; | 1780 | visRect.pos.x = (int) left; |
1566 | visRect.size.x -= (int) (left + right); | 1781 | visRect.size.x -= (int) (left + right); |
@@ -1585,6 +1800,6 @@ iRect visibleRect_Root(const iRoot *d) { | |||
1585 | visRect = intersect_Rect(visRect, init_Rect(usable.x, usable.y, usable.w, usable.h)); | 1800 | visRect = intersect_Rect(visRect, init_Rect(usable.x, usable.y, usable.w, usable.h)); |
1586 | } | 1801 | } |
1587 | #endif | 1802 | #endif |
1588 | adjustEdges_Rect(&visRect, 0, 0, -get_MainWindow()->keyboardHeight, 0); | 1803 | adjustEdges_Rect(&visRect, 0, 0, -get_MainWindow()->keyboardHeight + bottom, 0); |
1589 | return visRect; | 1804 | return visRect; |
1590 | } | 1805 | } |
diff --git a/src/ui/root.h b/src/ui/root.h index 851d927d..7e831be3 100644 --- a/src/ui/root.h +++ b/src/ui/root.h | |||
@@ -2,11 +2,15 @@ | |||
2 | 2 | ||
3 | #include "widget.h" | 3 | #include "widget.h" |
4 | #include "color.h" | 4 | #include "color.h" |
5 | #include <the_Foundation/audience.h> | ||
5 | #include <the_Foundation/ptrset.h> | 6 | #include <the_Foundation/ptrset.h> |
6 | #include <the_Foundation/vec2.h> | 7 | #include <the_Foundation/vec2.h> |
7 | 8 | ||
8 | iDeclareType(Root) | 9 | iDeclareType(Root) |
9 | 10 | ||
11 | iDeclareNotifyFunc(Root, VisualOffsetsChanged) | ||
12 | iDeclareAudienceGetter(Root, visualOffsetsChanged) | ||
13 | |||
10 | struct Impl_Root { | 14 | struct Impl_Root { |
11 | iWidget * widget; | 15 | iWidget * widget; |
12 | iWindow * window; | 16 | iWindow * window; |
@@ -14,6 +18,9 @@ struct Impl_Root { | |||
14 | iPtrSet * pendingDestruction; | 18 | iPtrSet * pendingDestruction; |
15 | iBool pendingArrange; | 19 | iBool pendingArrange; |
16 | int loadAnimTimer; | 20 | int loadAnimTimer; |
21 | iBool didAnimateVisualOffsets; | ||
22 | iBool didChangeArrangement; | ||
23 | iAudience *visualOffsetsChanged; /* called after running tickers */ | ||
17 | iColor tmPalette[tmMax_ColorId]; /* theme-specific palette */ | 24 | iColor tmPalette[tmMax_ColorId]; /* theme-specific palette */ |
18 | }; | 25 | }; |
19 | 26 | ||
@@ -36,6 +43,7 @@ void updatePadding_Root (iRoot *); /* TODO: is part of m | |||
36 | void dismissPortraitPhoneSidebars_Root (iRoot *); | 43 | void dismissPortraitPhoneSidebars_Root (iRoot *); |
37 | void showToolbar_Root (iRoot *, iBool show); | 44 | void showToolbar_Root (iRoot *, iBool show); |
38 | void updateToolbarColors_Root (iRoot *); | 45 | void updateToolbarColors_Root (iRoot *); |
46 | void notifyVisualOffsetChange_Root (iRoot *); | ||
39 | 47 | ||
40 | iInt2 size_Root (const iRoot *); | 48 | iInt2 size_Root (const iRoot *); |
41 | iRect rect_Root (const iRoot *); | 49 | iRect rect_Root (const iRoot *); |
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) { | |||
107 | static iRect thumbRect_ScrollWidget_(const iScrollWidget *d) { | 107 | static iRect thumbRect_ScrollWidget_(const iScrollWidget *d) { |
108 | const iRect bounds = bounds_ScrollWidget_(d); | 108 | const iRect bounds = bounds_ScrollWidget_(d); |
109 | iRect rect = init_Rect(bounds.pos.x, bounds.pos.y, bounds.size.x, 0); | 109 | iRect rect = init_Rect(bounds.pos.x, bounds.pos.y, bounds.size.x, 0); |
110 | const int total = size_Range(&d->range); | 110 | const int total = (int) size_Range(&d->range); |
111 | if (total > 0) { | 111 | if (total > 0) { |
112 | const int tsize = thumbSize_ScrollWidget_(d); | 112 | const int tsize = thumbSize_ScrollWidget_(d); |
113 | // iAssert(tsize <= height_Rect(bounds)); | 113 | // iAssert(tsize <= height_Rect(bounds)); |
@@ -197,7 +197,7 @@ static iBool processEvent_ScrollWidget_(iScrollWidget *d, const SDL_Event *ev) { | |||
197 | case drag_ClickResult: { | 197 | case drag_ClickResult: { |
198 | const iRect bounds = bounds_ScrollWidget_(d); | 198 | const iRect bounds = bounds_ScrollWidget_(d); |
199 | const int offset = delta_Click(&d->click).y; | 199 | const int offset = delta_Click(&d->click).y; |
200 | const int total = size_Range(&d->range); | 200 | const int total = (int) size_Range(&d->range); |
201 | int dpos = (float) offset / (float) (height_Rect(bounds) - thumbSize_ScrollWidget_(d)) * total; | 201 | int dpos = (float) offset / (float) (height_Rect(bounds) - thumbSize_ScrollWidget_(d)) * total; |
202 | d->thumb = iClamp(d->startThumb + dpos, d->range.start, d->range.end); | 202 | d->thumb = iClamp(d->startThumb + dpos, d->range.start, d->range.end); |
203 | postCommand_Widget(w, "scroll.moved arg:%d", d->thumb); | 203 | postCommand_Widget(w, "scroll.moved arg:%d", d->thumb); |
diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index 2219eba9..16677f9e 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. */ | |||
25 | #include "app.h" | 25 | #include "app.h" |
26 | #include "defs.h" | 26 | #include "defs.h" |
27 | #include "bookmarks.h" | 27 | #include "bookmarks.h" |
28 | #include "certlistwidget.h" | ||
28 | #include "command.h" | 29 | #include "command.h" |
29 | #include "documentwidget.h" | 30 | #include "documentwidget.h" |
30 | #include "feeds.h" | 31 | #include "feeds.h" |
@@ -34,10 +35,12 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
34 | #include "inputwidget.h" | 35 | #include "inputwidget.h" |
35 | #include "labelwidget.h" | 36 | #include "labelwidget.h" |
36 | #include "listwidget.h" | 37 | #include "listwidget.h" |
38 | #include "mobile.h" | ||
37 | #include "keys.h" | 39 | #include "keys.h" |
38 | #include "paint.h" | 40 | #include "paint.h" |
39 | #include "root.h" | 41 | #include "root.h" |
40 | #include "scrollwidget.h" | 42 | #include "scrollwidget.h" |
43 | #include "touch.h" | ||
41 | #include "util.h" | 44 | #include "util.h" |
42 | #include "visited.h" | 45 | #include "visited.h" |
43 | 46 | ||
@@ -88,6 +91,22 @@ iDefineObjectConstruction(SidebarItem) | |||
88 | 91 | ||
89 | /*----------------------------------------------------------------------------------------------*/ | 92 | /*----------------------------------------------------------------------------------------------*/ |
90 | 93 | ||
94 | static const char *normalModeLabels_[max_SidebarMode] = { | ||
95 | book_Icon " ${sidebar.bookmarks}", | ||
96 | star_Icon " ${sidebar.feeds}", | ||
97 | clock_Icon " ${sidebar.history}", | ||
98 | person_Icon " ${sidebar.identities}", | ||
99 | page_Icon " ${sidebar.outline}", | ||
100 | }; | ||
101 | |||
102 | static const char *tightModeLabels_[max_SidebarMode] = { | ||
103 | book_Icon, | ||
104 | star_Icon, | ||
105 | clock_Icon, | ||
106 | person_Icon, | ||
107 | page_Icon, | ||
108 | }; | ||
109 | |||
91 | struct Impl_SidebarWidget { | 110 | struct Impl_SidebarWidget { |
92 | iWidget widget; | 111 | iWidget widget; |
93 | enum iSidebarSide side; | 112 | enum iSidebarSide side; |
@@ -96,7 +115,10 @@ struct Impl_SidebarWidget { | |||
96 | iString cmdPrefix; | 115 | iString cmdPrefix; |
97 | iWidget * blank; | 116 | iWidget * blank; |
98 | iListWidget * list; | 117 | iListWidget * list; |
118 | iCertListWidget * certList; | ||
99 | iWidget * actions; /* below the list, area for buttons */ | 119 | iWidget * actions; /* below the list, area for buttons */ |
120 | int midHeight; /* on portrait phone, the height for the middle state */ | ||
121 | iBool isEditing; /* mobile edit mode */ | ||
100 | int modeScroll[max_SidebarMode]; | 122 | int modeScroll[max_SidebarMode]; |
101 | iLabelWidget * modeButtons[max_SidebarMode]; | 123 | iLabelWidget * modeButtons[max_SidebarMode]; |
102 | int maxButtonLabelWidth; | 124 | int maxButtonLabelWidth; |
@@ -114,6 +136,10 @@ struct Impl_SidebarWidget { | |||
114 | 136 | ||
115 | iDefineObjectConstructionArgs(SidebarWidget, (enum iSidebarSide side), side) | 137 | iDefineObjectConstructionArgs(SidebarWidget, (enum iSidebarSide side), side) |
116 | 138 | ||
139 | iLocalDef iListWidget *list_SidebarWidget_(iSidebarWidget *d) { | ||
140 | return d->mode == identities_SidebarMode ? (iListWidget *) d->certList : d->list; | ||
141 | } | ||
142 | |||
117 | static iBool isResizing_SidebarWidget_(const iSidebarWidget *d) { | 143 | static iBool isResizing_SidebarWidget_(const iSidebarWidget *d) { |
118 | return (flags_Widget(d->resizer) & pressed_WidgetFlag) != 0; | 144 | return (flags_Widget(d->resizer) & pressed_WidgetFlag) != 0; |
119 | } | 145 | } |
@@ -181,77 +207,31 @@ int cmpTree_Bookmark(const iBookmark **a, const iBookmark **b) { | |||
181 | return cmpStringCase_String(&bm1->title, &bm2->title); | 207 | return cmpStringCase_String(&bm1->title, &bm2->title); |
182 | } | 208 | } |
183 | 209 | ||
210 | static enum iFontId actionButtonFont_SidebarWidget_(const iSidebarWidget *d) { | ||
211 | switch (deviceType_App()) { | ||
212 | default: | ||
213 | break; | ||
214 | case phone_AppDeviceType: | ||
215 | return isPortrait_App() ? uiLabelBig_FontId : uiLabelMedium_FontId; | ||
216 | case tablet_AppDeviceType: | ||
217 | return uiLabelMedium_FontId; | ||
218 | } | ||
219 | return d->buttonFont; | ||
220 | } | ||
221 | |||
184 | static iLabelWidget *addActionButton_SidebarWidget_(iSidebarWidget *d, const char *label, | 222 | static iLabelWidget *addActionButton_SidebarWidget_(iSidebarWidget *d, const char *label, |
185 | const char *command, int64_t flags) { | 223 | const char *command, int64_t flags) { |
186 | iLabelWidget *btn = addChildFlags_Widget(d->actions, | 224 | iLabelWidget *btn = addChildFlags_Widget(d->actions, |
187 | iClob(new_LabelWidget(label, command)), | 225 | iClob(new_LabelWidget(label, command)), |
188 | //(deviceType_App() != desktop_AppDeviceType ? | ||
189 | // extraPadding_WidgetFlag : 0) | | ||
190 | flags); | 226 | flags); |
191 | setFont_LabelWidget(btn, deviceType_App() == phone_AppDeviceType && d->side == right_SidebarSide | 227 | setFont_LabelWidget(btn, actionButtonFont_SidebarWidget_(d)); |
192 | ? uiLabelBig_FontId | ||
193 | : d->buttonFont); | ||
194 | checkIcon_LabelWidget(btn); | 228 | checkIcon_LabelWidget(btn); |
195 | return btn; | 229 | if (deviceType_App() != desktop_AppDeviceType) { |
196 | } | 230 | setFlags_Widget(as_Widget(btn), frameless_WidgetFlag, iTrue); |
197 | 231 | setTextColor_LabelWidget(btn, uiTextAction_ColorId); | |
198 | static iGmIdentity *menuIdentity_SidebarWidget_(const iSidebarWidget *d) { | 232 | setBackgroundColor_Widget(as_Widget(btn), uiBackground_ColorId); |
199 | if (d->mode == identities_SidebarMode) { | ||
200 | if (d->contextItem) { | ||
201 | return identity_GmCerts(certs_App(), d->contextItem->id); | ||
202 | } | ||
203 | } | ||
204 | return NULL; | ||
205 | } | ||
206 | |||
207 | static void updateContextMenu_SidebarWidget_(iSidebarWidget *d) { | ||
208 | if (d->mode != identities_SidebarMode) { | ||
209 | return; | ||
210 | } | 233 | } |
211 | iArray *items = collectNew_Array(sizeof(iMenuItem)); | 234 | return btn; |
212 | pushBackN_Array(items, (iMenuItem[]){ | ||
213 | { person_Icon " ${ident.use}", 0, 0, "ident.use arg:1" }, | ||
214 | { close_Icon " ${ident.stopuse}", 0, 0, "ident.use arg:0" }, | ||
215 | { close_Icon " ${ident.stopuse.all}", 0, 0, "ident.use arg:0 clear:1" }, | ||
216 | { "---", 0, 0, NULL }, | ||
217 | { edit_Icon " ${menu.edit.notes}", 0, 0, "ident.edit" }, | ||
218 | { "${ident.fingerprint}", 0, 0, "ident.fingerprint" }, | ||
219 | { export_Icon " ${ident.export}", 0, 0, "ident.export" }, | ||
220 | { "---", 0, 0, NULL }, | ||
221 | { delete_Icon " " uiTextCaution_ColorEscape "${ident.delete}", 0, 0, "ident.delete confirm:1" }, | ||
222 | }, 9); | ||
223 | /* Used URLs. */ | ||
224 | const iGmIdentity *ident = menuIdentity_SidebarWidget_(d); | ||
225 | if (ident) { | ||
226 | size_t insertPos = 3; | ||
227 | if (!isEmpty_StringSet(ident->useUrls)) { | ||
228 | insert_Array(items, insertPos++, &(iMenuItem){ "---", 0, 0, NULL }); | ||
229 | } | ||
230 | const iString *docUrl = url_DocumentWidget(document_App()); | ||
231 | iBool usedOnCurrentPage = iFalse; | ||
232 | iConstForEach(StringSet, i, ident->useUrls) { | ||
233 | const iString *url = i.value; | ||
234 | usedOnCurrentPage |= equalCase_String(docUrl, url); | ||
235 | iRangecc urlStr = range_String(url); | ||
236 | if (startsWith_Rangecc(urlStr, "gemini://")) { | ||
237 | urlStr.start += 9; /* omit the default scheme */ | ||
238 | } | ||
239 | if (endsWith_Rangecc(urlStr, "/")) { | ||
240 | urlStr.end--; /* looks cleaner */ | ||
241 | } | ||
242 | insert_Array(items, | ||
243 | insertPos++, | ||
244 | &(iMenuItem){ format_CStr(globe_Icon " %s", cstr_Rangecc(urlStr)), | ||
245 | 0, | ||
246 | 0, | ||
247 | format_CStr("!open url:%s", cstr_String(url)) }); | ||
248 | } | ||
249 | if (!usedOnCurrentPage) { | ||
250 | remove_Array(items, 1); | ||
251 | } | ||
252 | } | ||
253 | destroy_Widget(d->menu); | ||
254 | d->menu = makeMenu_Widget(as_Widget(d), data_Array(items), size_Array(items)); | ||
255 | } | 235 | } |
256 | 236 | ||
257 | static iBool isBookmarkFolded_SidebarWidget_(const iSidebarWidget *d, const iBookmark *bm) { | 237 | static iBool isBookmarkFolded_SidebarWidget_(const iSidebarWidget *d, const iBookmark *bm) { |
@@ -264,11 +244,29 @@ static iBool isBookmarkFolded_SidebarWidget_(const iSidebarWidget *d, const iBoo | |||
264 | return iFalse; | 244 | return iFalse; |
265 | } | 245 | } |
266 | 246 | ||
247 | static iBool isSlidingSheet_SidebarWidget_(const iSidebarWidget *d) { | ||
248 | return isPortraitPhone_App(); | ||
249 | } | ||
250 | |||
251 | static void setMobileEditMode_SidebarWidget_(iSidebarWidget *d, iBool editing) { | ||
252 | iWidget *w = as_Widget(d); | ||
253 | d->isEditing = editing; | ||
254 | if (d->actions) { | ||
255 | setFlags_Widget(findChild_Widget(w, "sidebar.close"), hidden_WidgetFlag, editing); | ||
256 | setFlags_Widget(child_Widget(d->actions, 0), hidden_WidgetFlag, !editing); | ||
257 | setTextCStr_LabelWidget(child_Widget(as_Widget(d->actions), 2), | ||
258 | editing ? "${sidebar.close}" : "${sidebar.action.bookmarks.edit}"); | ||
259 | setDragHandleWidth_ListWidget(d->list, editing ? itemHeight_ListWidget(d->list) * 3 / 2 : 0); | ||
260 | arrange_Widget(d->actions); | ||
261 | } | ||
262 | } | ||
263 | |||
267 | static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepActions) { | 264 | static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepActions) { |
265 | const iBool isMobile = (deviceType_App() != desktop_AppDeviceType); | ||
268 | clear_ListWidget(d->list); | 266 | clear_ListWidget(d->list); |
269 | releaseChildren_Widget(d->blank); | 267 | releaseChildren_Widget(d->blank); |
270 | if (!keepActions) { | 268 | if (!keepActions) { |
271 | releaseChildren_Widget(d->actions); | 269 | releaseChildren_Widget(d->actions); |
272 | } | 270 | } |
273 | d->actions->rect.size.y = 0; | 271 | d->actions->rect.size.y = 0; |
274 | destroy_Widget(d->menu); | 272 | destroy_Widget(d->menu); |
@@ -288,7 +286,8 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct | |||
288 | iZap(on); | 286 | iZap(on); |
289 | size_t numItems = 0; | 287 | size_t numItems = 0; |
290 | isEmpty = iTrue; | 288 | isEmpty = iTrue; |
291 | iConstForEach(PtrArray, i, listEntries_Feeds()) { | 289 | const iPtrArray *feedEntries = listEntries_Feeds(); |
290 | iConstForEach(PtrArray, i, feedEntries) { | ||
292 | const iFeedEntry *entry = i.ptr; | 291 | const iFeedEntry *entry = i.ptr; |
293 | if (isHidden_FeedEntry(entry)) { | 292 | if (isHidden_FeedEntry(entry)) { |
294 | continue; /* A hidden entry. */ | 293 | continue; /* A hidden entry. */ |
@@ -301,12 +300,12 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct | |||
301 | if (secondsSince_Time(&now, &entry->discovered) > maxAge_Visited) { | 300 | if (secondsSince_Time(&now, &entry->discovered) > maxAge_Visited) { |
302 | break; /* the rest are even older */ | 301 | break; /* the rest are even older */ |
303 | } | 302 | } |
304 | isEmpty = iFalse; | ||
305 | const iBool isOpen = equal_String(docUrl, &entry->url); | 303 | const iBool isOpen = equal_String(docUrl, &entry->url); |
306 | const iBool isUnread = isUnread_FeedEntry(entry); | 304 | const iBool isUnread = isUnread_FeedEntry(entry); |
307 | if (d->feedsMode == unread_FeedsMode && !isUnread && !isOpen) { | 305 | if (d->feedsMode == unread_FeedsMode && !isUnread && !isOpen) { |
308 | continue; | 306 | continue; |
309 | } | 307 | } |
308 | isEmpty = iFalse; | ||
310 | /* Insert date separators. */ { | 309 | /* Insert date separators. */ { |
311 | iDate entryDate; | 310 | iDate entryDate; |
312 | init_Date(&entryDate, &entry->posted); | 311 | init_Date(&entryDate, &entry->posted); |
@@ -351,34 +350,67 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct | |||
351 | } | 350 | } |
352 | } | 351 | } |
353 | /* Actions. */ | 352 | /* Actions. */ |
354 | if (!keepActions) { | 353 | if (!isMobile) { |
355 | addActionButton_SidebarWidget_( | 354 | if (!keepActions && !isEmpty_PtrArray(feedEntries)) { |
356 | d, check_Icon " ${sidebar.action.feeds.markallread}", "feeds.markallread", expand_WidgetFlag | | 355 | addActionButton_SidebarWidget_(d, |
357 | tight_WidgetFlag); | 356 | check_Icon |
358 | updateSize_LabelWidget(addChildFlags_Widget(d->actions, | 357 | " ${sidebar.action.feeds.markallread}", |
359 | iClob(new_LabelWidget("${sidebar.action.show}", NULL)), | 358 | "feeds.markallread", |
360 | frameless_WidgetFlag | tight_WidgetFlag)); | 359 | expand_WidgetFlag | tight_WidgetFlag); |
361 | const iMenuItem items[] = { | 360 | updateSize_LabelWidget( |
362 | { "${sidebar.action.feeds.showall}", SDLK_u, KMOD_SHIFT, "feeds.mode arg:0" }, | 361 | addChildFlags_Widget(d->actions, |
363 | { "${sidebar.action.feeds.showunread}", SDLK_u, 0, "feeds.mode arg:1" }, | 362 | iClob(new_LabelWidget("${sidebar.action.show}", NULL)), |
364 | }; | 363 | frameless_WidgetFlag | tight_WidgetFlag)); |
365 | iWidget *dropButton = addChild_Widget( | 364 | const iMenuItem items[] = { |
366 | d->actions, | 365 | { page_Icon " ${sidebar.action.feeds.showall}", |
367 | iClob(makeMenuButton_LabelWidget(items[d->feedsMode].label, items, 2))); | 366 | SDLK_u, |
368 | setId_Widget(dropButton, "feeds.modebutton"); | 367 | KMOD_SHIFT, |
369 | checkIcon_LabelWidget((iLabelWidget *) dropButton); | 368 | "feeds.mode arg:0" }, |
370 | setFixedSize_Widget( | 369 | { circle_Icon " ${sidebar.action.feeds.showunread}", |
371 | dropButton, | 370 | SDLK_u, |
372 | init_I2(iMaxi(20 * gap_UI, measure_Text( | 371 | 0, |
373 | default_FontId, | 372 | "feeds.mode arg:1" }, |
374 | translateCStr_Lang(items[findWidestLabel_MenuItem(items, 2)].label)) | 373 | }; |
375 | .advance.x + | 374 | iWidget *dropButton = addChild_Widget( |
376 | 6 * gap_UI), | 375 | d->actions, |
376 | iClob(makeMenuButton_LabelWidget(items[d->feedsMode].label, items, 2))); | ||
377 | setId_Widget(dropButton, "feeds.modebutton"); | ||
378 | checkIcon_LabelWidget((iLabelWidget *) dropButton); | ||
379 | setFixedSize_Widget( | ||
380 | dropButton, | ||
381 | init_I2( | ||
382 | iMaxi(20 * gap_UI, | ||
383 | measure_Text(default_FontId, | ||
384 | translateCStr_Lang( | ||
385 | items[findWidestLabel_MenuItem(items, 2)].label)) | ||
386 | .advance.x + | ||
387 | 13 * gap_UI), | ||
377 | -1)); | 388 | -1)); |
389 | } | ||
390 | else { | ||
391 | updateDropdownSelection_LabelWidget( | ||
392 | findChild_Widget(d->actions, "feeds.modebutton"), | ||
393 | format_CStr(" arg:%d", d->feedsMode)); | ||
394 | } | ||
378 | } | 395 | } |
379 | else { | 396 | else { |
380 | updateDropdownSelection_LabelWidget(findChild_Widget(d->actions, "feeds.modebutton"), | 397 | if (!keepActions) { |
381 | format_CStr(" arg:%d", d->feedsMode)); | 398 | iLabelWidget *readAll = addActionButton_SidebarWidget_(d, |
399 | check_Icon, | ||
400 | "feeds.markallread confirm:1", | ||
401 | 0); | ||
402 | setTextColor_LabelWidget(readAll, uiTextCaution_ColorId); | ||
403 | addActionButton_SidebarWidget_(d, | ||
404 | page_Icon, | ||
405 | "feeds.mode arg:0", | ||
406 | 0); | ||
407 | addActionButton_SidebarWidget_(d, | ||
408 | circle_Icon, | ||
409 | "feeds.mode arg:1", | ||
410 | 0); | ||
411 | } | ||
412 | setOutline_LabelWidget(child_Widget(d->actions, 1), d->feedsMode != all_FeedsMode); | ||
413 | setOutline_LabelWidget(child_Widget(d->actions, 2), d->feedsMode != unread_FeedsMode); | ||
382 | } | 414 | } |
383 | d->menu = makeMenu_Widget( | 415 | d->menu = makeMenu_Widget( |
384 | as_Widget(d), | 416 | as_Widget(d), |
@@ -416,11 +448,6 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct | |||
416 | break; | 448 | break; |
417 | } | 449 | } |
418 | case bookmarks_SidebarMode: { | 450 | case bookmarks_SidebarMode: { |
419 | iRegExp *homeTag = iClob(new_RegExp("\\b" homepage_BookmarkTag "\\b", caseSensitive_RegExpOption)); | ||
420 | iRegExp *subTag = iClob(new_RegExp("\\b" subscribed_BookmarkTag "\\b", caseSensitive_RegExpOption)); | ||
421 | iRegExp *remoteSourceTag = iClob(new_RegExp("\\b" remoteSource_BookmarkTag "\\b", caseSensitive_RegExpOption)); | ||
422 | iRegExp *remoteTag = iClob(new_RegExp("\\b" remote_BookmarkTag "\\b", caseSensitive_RegExpOption)); | ||
423 | iRegExp *linkSplitTag = iClob(new_RegExp("\\b" linkSplit_BookmarkTag "\\b", caseSensitive_RegExpOption)); | ||
424 | iConstForEach(PtrArray, i, list_Bookmarks(bookmarks_App(), cmpTree_Bookmark, NULL, NULL)) { | 451 | iConstForEach(PtrArray, i, list_Bookmarks(bookmarks_App(), cmpTree_Bookmark, NULL, NULL)) { |
425 | const iBookmark *bm = i.ptr; | 452 | const iBookmark *bm = i.ptr; |
426 | if (isBookmarkFolded_SidebarWidget_(d, bm)) { | 453 | if (isBookmarkFolded_SidebarWidget_(d, bm)) { |
@@ -439,27 +466,21 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct | |||
439 | } | 466 | } |
440 | set_String(&item->url, &bm->url); | 467 | set_String(&item->url, &bm->url); |
441 | set_String(&item->label, &bm->title); | 468 | set_String(&item->label, &bm->title); |
442 | /* Icons for special tags. */ { | 469 | /* Icons for special behaviors. */ { |
443 | iRegExpMatch m; | 470 | if (bm->flags & subscribed_BookmarkFlag) { |
444 | init_RegExpMatch(&m); | ||
445 | if (matchString_RegExp(subTag, &bm->tags, &m)) { | ||
446 | appendChar_String(&item->meta, 0x2605); | 471 | appendChar_String(&item->meta, 0x2605); |
447 | } | 472 | } |
448 | init_RegExpMatch(&m); | 473 | if (bm->flags & homepage_BookmarkFlag) { |
449 | if (matchString_RegExp(homeTag, &bm->tags, &m)) { | ||
450 | appendChar_String(&item->meta, 0x1f3e0); | 474 | appendChar_String(&item->meta, 0x1f3e0); |
451 | } | 475 | } |
452 | init_RegExpMatch(&m); | 476 | if (bm->flags & remote_BookmarkFlag) { |
453 | if (matchString_RegExp(remoteTag, &bm->tags, &m)) { | ||
454 | item->listItem.isDraggable = iFalse; | 477 | item->listItem.isDraggable = iFalse; |
455 | } | 478 | } |
456 | init_RegExpMatch(&m); | 479 | if (bm->flags & remoteSource_BookmarkFlag) { |
457 | if (matchString_RegExp(remoteSourceTag, &bm->tags, &m)) { | ||
458 | appendChar_String(&item->meta, 0x2913); | 480 | appendChar_String(&item->meta, 0x2913); |
459 | item->isBold = iTrue; | 481 | item->isBold = iTrue; |
460 | } | 482 | } |
461 | init_RegExpMatch(&m); | 483 | if (bm->flags & linkSplit_BookmarkFlag) { |
462 | if (matchString_RegExp(linkSplitTag, &bm->tags, &m)) { | ||
463 | appendChar_String(&item->meta, 0x25e7); | 484 | appendChar_String(&item->meta, 0x25e7); |
464 | } | 485 | } |
465 | } | 486 | } |
@@ -495,6 +516,14 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct | |||
495 | { "---", 0, 0, NULL }, | 516 | { "---", 0, 0, NULL }, |
496 | { reload_Icon " ${bookmarks.reload}", 0, 0, "bookmarks.reload.remote" } }, | 517 | { reload_Icon " ${bookmarks.reload}", 0, 0, "bookmarks.reload.remote" } }, |
497 | 6); | 518 | 6); |
519 | if (isMobile) { | ||
520 | addActionButton_SidebarWidget_(d, "${sidebar.action.bookmarks.newfolder}", | ||
521 | "bookmarks.addfolder", !d->isEditing ? hidden_WidgetFlag : 0); | ||
522 | addChildFlags_Widget(d->actions, iClob(new_Widget()), expand_WidgetFlag); | ||
523 | iLabelWidget *btn = addActionButton_SidebarWidget_(d, | ||
524 | d->isEditing ? "${sidebar.close}" : "${sidebar.action.bookmarks.edit}", | ||
525 | "sidebar.bookmarks.edit", 0); | ||
526 | } | ||
498 | break; | 527 | break; |
499 | } | 528 | } |
500 | case history_SidebarMode: { | 529 | case history_SidebarMode: { |
@@ -554,49 +583,15 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct | |||
554 | (iMenuItem[]){ | 583 | (iMenuItem[]){ |
555 | { delete_Icon " " uiTextCaution_ColorEscape "${history.clear}", 0, 0, "history.clear confirm:1" }, | 584 | { delete_Icon " " uiTextCaution_ColorEscape "${history.clear}", 0, 0, "history.clear confirm:1" }, |
556 | }, 1); | 585 | }, 1); |
586 | if (isMobile) { | ||
587 | addChildFlags_Widget(d->actions, iClob(new_Widget()), expand_WidgetFlag); | ||
588 | iLabelWidget *btn = addActionButton_SidebarWidget_(d, "${sidebar.action.history.clear}", | ||
589 | "history.clear confirm:1", 0); | ||
590 | } | ||
557 | break; | 591 | break; |
558 | } | 592 | } |
559 | case identities_SidebarMode: { | 593 | case identities_SidebarMode: { |
560 | const iString *tabUrl = url_DocumentWidget(document_App()); | 594 | isEmpty = !updateItems_CertListWidget(d->certList); |
561 | const iRangecc tabHost = urlHost_String(tabUrl); | ||
562 | isEmpty = iTrue; | ||
563 | iConstForEach(PtrArray, i, identities_GmCerts(certs_App())) { | ||
564 | const iGmIdentity *ident = i.ptr; | ||
565 | iSidebarItem *item = new_SidebarItem(); | ||
566 | item->id = (uint32_t) index_PtrArrayConstIterator(&i); | ||
567 | item->icon = 0x1f464; /* person */ | ||
568 | set_String(&item->label, name_GmIdentity(ident)); | ||
569 | iDate until; | ||
570 | validUntil_TlsCertificate(ident->cert, &until); | ||
571 | const iBool isActive = isUsedOn_GmIdentity(ident, tabUrl); | ||
572 | format_String(&item->meta, | ||
573 | "%s", | ||
574 | isActive ? cstr_Lang("ident.using") | ||
575 | : isUsed_GmIdentity(ident) | ||
576 | ? formatCStrs_Lang("ident.usedonurls.n", size_StringSet(ident->useUrls)) | ||
577 | : cstr_Lang("ident.notused")); | ||
578 | const char *expiry = | ||
579 | ident->flags & temporary_GmIdentityFlag | ||
580 | ? cstr_Lang("ident.temporary") | ||
581 | : cstrCollect_String(format_Date(&until, cstr_Lang("ident.expiry"))); | ||
582 | if (isEmpty_String(&ident->notes)) { | ||
583 | appendFormat_String(&item->meta, "\n%s", expiry); | ||
584 | } | ||
585 | else { | ||
586 | appendFormat_String(&item->meta, | ||
587 | " \u2014 %s\n%s%s", | ||
588 | expiry, | ||
589 | escape_Color(uiHeading_ColorId), | ||
590 | cstr_String(&ident->notes)); | ||
591 | } | ||
592 | item->listItem.isSelected = isActive; | ||
593 | if (isUsedOnDomain_GmIdentity(ident, tabHost)) { | ||
594 | item->indent = 1; /* will be highlighted */ | ||
595 | } | ||
596 | addItem_ListWidget(d->list, item); | ||
597 | iRelease(item); | ||
598 | isEmpty = iFalse; | ||
599 | } | ||
600 | /* Actions. */ | 595 | /* Actions. */ |
601 | if (!isEmpty) { | 596 | if (!isEmpty) { |
602 | addActionButton_SidebarWidget_(d, add_Icon " ${sidebar.action.ident.new}", "ident.new", 0); | 597 | addActionButton_SidebarWidget_(d, add_Icon " ${sidebar.action.ident.new}", "ident.new", 0); |
@@ -607,16 +602,27 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct | |||
607 | default: | 602 | default: |
608 | break; | 603 | break; |
609 | } | 604 | } |
610 | scrollOffset_ListWidget(d->list, 0); | 605 | setFlags_Widget(as_Widget(d->list), hidden_WidgetFlag, d->mode == identities_SidebarMode); |
611 | updateVisible_ListWidget(d->list); | 606 | setFlags_Widget(as_Widget(d->certList), hidden_WidgetFlag, d->mode != identities_SidebarMode); |
612 | invalidate_ListWidget(d->list); | 607 | scrollOffset_ListWidget(list_SidebarWidget_(d), 0); |
608 | updateVisible_ListWidget(list_SidebarWidget_(d)); | ||
609 | invalidate_ListWidget(list_SidebarWidget_(d)); | ||
613 | /* Content for a blank tab. */ | 610 | /* Content for a blank tab. */ |
614 | if (isEmpty) { | 611 | if (isEmpty) { |
615 | if (d->mode == feeds_SidebarMode) { | 612 | if (d->mode == feeds_SidebarMode) { |
616 | iWidget *div = makeVDiv_Widget(); | 613 | iWidget *div = makeVDiv_Widget(); |
617 | setPadding_Widget(div, 3 * gap_UI, 0, 3 * gap_UI, 2 * gap_UI); | 614 | setPadding_Widget(div, 3 * gap_UI, 0, 3 * gap_UI, 2 * gap_UI); |
615 | arrange_Widget(d->actions); | ||
616 | // setPadding_Widget(div, 0, 0, 0, height_Widget(d->actions)); | ||
618 | addChildFlags_Widget(div, iClob(new_Widget()), expand_WidgetFlag); /* pad */ | 617 | addChildFlags_Widget(div, iClob(new_Widget()), expand_WidgetFlag); /* pad */ |
619 | addChild_Widget(div, iClob(new_LabelWidget("${menu.feeds.refresh}", "feeds.refresh"))); | 618 | if (d->feedsMode == all_FeedsMode) { |
619 | addChild_Widget(div, iClob(new_LabelWidget("${menu.feeds.refresh}", "feeds.refresh"))); | ||
620 | } | ||
621 | else { | ||
622 | iLabelWidget *msg = addChildFlags_Widget(div, iClob(new_LabelWidget("${sidebar.empty.unread}", NULL)), | ||
623 | frameless_WidgetFlag); | ||
624 | setFont_LabelWidget(msg, uiLabelLarge_FontId); | ||
625 | } | ||
620 | addChildFlags_Widget(div, iClob(new_Widget()), expand_WidgetFlag); /* pad */ | 626 | addChildFlags_Widget(div, iClob(new_Widget()), expand_WidgetFlag); /* pad */ |
621 | addChild_Widget(d->blank, iClob(div)); | 627 | addChild_Widget(d->blank, iClob(div)); |
622 | } | 628 | } |
@@ -645,7 +651,7 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct | |||
645 | setWrap_LabelWidget(linkLabel, iTrue); | 651 | setWrap_LabelWidget(linkLabel, iTrue); |
646 | addChild_Widget(d->blank, iClob(div)); | 652 | addChild_Widget(d->blank, iClob(div)); |
647 | } | 653 | } |
648 | // arrange_Widget(d->blank); | 654 | arrange_Widget(d->blank); |
649 | } | 655 | } |
650 | #if 0 | 656 | #if 0 |
651 | if (deviceType_App() != desktop_AppDeviceType) { | 657 | if (deviceType_App() != desktop_AppDeviceType) { |
@@ -659,7 +665,7 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct | |||
659 | #endif | 665 | #endif |
660 | arrange_Widget(d->actions); | 666 | arrange_Widget(d->actions); |
661 | arrange_Widget(as_Widget(d)); | 667 | arrange_Widget(as_Widget(d)); |
662 | updateMouseHover_ListWidget(d->list); | 668 | updateMouseHover_ListWidget(list_SidebarWidget_(d)); |
663 | } | 669 | } |
664 | 670 | ||
665 | static void updateItems_SidebarWidget_(iSidebarWidget *d) { | 671 | static void updateItems_SidebarWidget_(iSidebarWidget *d) { |
@@ -678,35 +684,59 @@ static size_t findItem_SidebarWidget_(const iSidebarWidget *d, int id) { | |||
678 | } | 684 | } |
679 | 685 | ||
680 | static void updateItemHeight_SidebarWidget_(iSidebarWidget *d) { | 686 | static void updateItemHeight_SidebarWidget_(iSidebarWidget *d) { |
687 | const float heights[max_SidebarMode] = { 1.333f, 2.333f, 1.333f, 3.5f, 1.2f }; | ||
681 | if (d->list) { | 688 | if (d->list) { |
682 | const float heights[max_SidebarMode] = { 1.333f, 2.333f, 1.333f, 3.5f, 1.2f }; | ||
683 | setItemHeight_ListWidget(d->list, heights[d->mode] * lineHeight_Text(d->itemFonts[0])); | 689 | setItemHeight_ListWidget(d->list, heights[d->mode] * lineHeight_Text(d->itemFonts[0])); |
684 | } | 690 | } |
691 | if (d->certList) { | ||
692 | updateItemHeight_CertListWidget(d->certList); | ||
693 | } | ||
685 | } | 694 | } |
686 | 695 | ||
687 | iBool setMode_SidebarWidget(iSidebarWidget *d, enum iSidebarMode mode) { | 696 | iBool setMode_SidebarWidget(iSidebarWidget *d, enum iSidebarMode mode) { |
688 | if (d->mode == mode) { | 697 | if (d->mode == mode) { |
689 | return iFalse; | 698 | return iFalse; |
690 | } | 699 | } |
700 | if (mode == identities_SidebarMode && deviceType_App() != desktop_AppDeviceType) { | ||
701 | return iFalse; /* Identities are in Settings. */ | ||
702 | } | ||
691 | if (d->mode >= 0 && d->mode < max_SidebarMode) { | 703 | if (d->mode >= 0 && d->mode < max_SidebarMode) { |
692 | d->modeScroll[d->mode] = scrollPos_ListWidget(d->list); /* saved for later */ | 704 | d->modeScroll[d->mode] = scrollPos_ListWidget(list_SidebarWidget_(d)); /* saved for later */ |
693 | } | 705 | } |
694 | d->mode = mode; | 706 | d->mode = mode; |
695 | for (enum iSidebarMode i = 0; i < max_SidebarMode; i++) { | 707 | for (enum iSidebarMode i = 0; i < max_SidebarMode; i++) { |
696 | setFlags_Widget(as_Widget(d->modeButtons[i]), selected_WidgetFlag, i == d->mode); | 708 | setFlags_Widget(as_Widget(d->modeButtons[i]), selected_WidgetFlag, i == d->mode); |
697 | } | 709 | } |
698 | setBackgroundColor_Widget(as_Widget(d->list), | 710 | setBackgroundColor_Widget(as_Widget(list_SidebarWidget_(d)), |
699 | d->mode == documentOutline_SidebarMode ? tmBannerBackground_ColorId | 711 | d->mode == documentOutline_SidebarMode ? tmBannerBackground_ColorId |
700 | : uiBackgroundSidebar_ColorId); | 712 | : uiBackgroundSidebar_ColorId); |
701 | updateItemHeight_SidebarWidget_(d); | 713 | updateItemHeight_SidebarWidget_(d); |
714 | if (deviceType_App() != desktop_AppDeviceType && mode != bookmarks_SidebarMode) { | ||
715 | setMobileEditMode_SidebarWidget_(d, iFalse); | ||
716 | } | ||
702 | /* Restore previous scroll position. */ | 717 | /* Restore previous scroll position. */ |
703 | setScrollPos_ListWidget(d->list, d->modeScroll[mode]); | 718 | setScrollPos_ListWidget(list_SidebarWidget_(d), d->modeScroll[mode]); |
719 | /* Title of the mobile sliding sheet. */ | ||
720 | iLabelWidget *sheetTitle = findChild_Widget(&d->widget, "sidebar.title"); | ||
721 | if (sheetTitle) { | ||
722 | iString title; | ||
723 | initCStr_String(&title, normalModeLabels_[d->mode]); | ||
724 | removeIconPrefix_String(&title); | ||
725 | setText_LabelWidget(sheetTitle, &title); | ||
726 | deinit_String(&title); | ||
727 | } | ||
704 | return iTrue; | 728 | return iTrue; |
705 | } | 729 | } |
706 | 730 | ||
707 | void setClosedFolders_SidebarWidget(iSidebarWidget *d, const iIntSet *closedFolders) { | 731 | void setClosedFolders_SidebarWidget(iSidebarWidget *d, const iIntSet *closedFolders) { |
732 | if (d) { | ||
708 | delete_IntSet(d->closedFolders); | 733 | delete_IntSet(d->closedFolders); |
709 | d->closedFolders = copy_IntSet(closedFolders); | 734 | d->closedFolders = copy_IntSet(closedFolders); |
735 | } | ||
736 | } | ||
737 | |||
738 | void setMidHeight_SidebarWidget(iSidebarWidget *d, int midHeight) { | ||
739 | d->midHeight = midHeight; | ||
710 | } | 740 | } |
711 | 741 | ||
712 | enum iSidebarMode mode_SidebarWidget(const iSidebarWidget *d) { | 742 | enum iSidebarMode mode_SidebarWidget(const iSidebarWidget *d) { |
@@ -722,25 +752,9 @@ float width_SidebarWidget(const iSidebarWidget *d) { | |||
722 | } | 752 | } |
723 | 753 | ||
724 | const iIntSet *closedFolders_SidebarWidget(const iSidebarWidget *d) { | 754 | const iIntSet *closedFolders_SidebarWidget(const iSidebarWidget *d) { |
725 | return d->closedFolders; | 755 | return d ? d->closedFolders : collect_IntSet(new_IntSet()); |
726 | } | 756 | } |
727 | 757 | ||
728 | static const char *normalModeLabels_[max_SidebarMode] = { | ||
729 | book_Icon " ${sidebar.bookmarks}", | ||
730 | star_Icon " ${sidebar.feeds}", | ||
731 | clock_Icon " ${sidebar.history}", | ||
732 | person_Icon " ${sidebar.identities}", | ||
733 | page_Icon " ${sidebar.outline}", | ||
734 | }; | ||
735 | |||
736 | static const char *tightModeLabels_[max_SidebarMode] = { | ||
737 | book_Icon, | ||
738 | star_Icon, | ||
739 | clock_Icon, | ||
740 | person_Icon, | ||
741 | page_Icon, | ||
742 | }; | ||
743 | |||
744 | const char *icon_SidebarMode(enum iSidebarMode mode) { | 758 | const char *icon_SidebarMode(enum iSidebarMode mode) { |
745 | return tightModeLabels_[mode]; | 759 | return tightModeLabels_[mode]; |
746 | } | 760 | } |
@@ -762,6 +776,20 @@ static void updateMetrics_SidebarWidget_(iSidebarWidget *d) { | |||
762 | updateItemHeight_SidebarWidget_(d); | 776 | updateItemHeight_SidebarWidget_(d); |
763 | } | 777 | } |
764 | 778 | ||
779 | static void updateSlidingSheetHeight_SidebarWidget_(iSidebarWidget *sidebar, iRoot *root) { | ||
780 | if (!isPortraitPhone_App() || !isVisible_Widget(sidebar)) return; | ||
781 | iWidget *d = as_Widget(sidebar); | ||
782 | const int oldSize = d->rect.size.y; | ||
783 | const int newSize = bottom_Rect(safeRect_Root(d->root)) - top_Rect(bounds_Widget(d)); | ||
784 | if (oldSize != newSize) { | ||
785 | d->rect.size.y = newSize; | ||
786 | arrange_Widget(d); | ||
787 | } | ||
788 | // printf("[%p] %u: %d animating %d\n", d, window_Widget(d)->frameTime, | ||
789 | // (flags_Widget(d) & visualOffset_WidgetFlag) != 0, | ||
790 | // newSize); | ||
791 | } | ||
792 | |||
765 | void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) { | 793 | void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) { |
766 | iWidget *w = as_Widget(d); | 794 | iWidget *w = as_Widget(d); |
767 | init_Widget(w); | 795 | init_Widget(w); |
@@ -778,6 +806,8 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) { | |||
778 | d->side = side; | 806 | d->side = side; |
779 | d->mode = -1; | 807 | d->mode = -1; |
780 | d->feedsMode = all_FeedsMode; | 808 | d->feedsMode = all_FeedsMode; |
809 | d->midHeight = 0; | ||
810 | d->isEditing = iFalse; | ||
781 | d->numUnreadEntries = 0; | 811 | d->numUnreadEntries = 0; |
782 | d->buttonFont = uiLabel_FontId; /* wiil be changed later */ | 812 | d->buttonFont = uiLabel_FontId; /* wiil be changed later */ |
783 | d->itemFonts[0] = uiContent_FontId; | 813 | d->itemFonts[0] = uiContent_FontId; |
@@ -795,18 +825,38 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) { | |||
795 | iWidget *vdiv = makeVDiv_Widget(); | 825 | iWidget *vdiv = makeVDiv_Widget(); |
796 | addChildFlags_Widget(w, vdiv, resizeToParentWidth_WidgetFlag | resizeToParentHeight_WidgetFlag); | 826 | addChildFlags_Widget(w, vdiv, resizeToParentWidth_WidgetFlag | resizeToParentHeight_WidgetFlag); |
797 | iZap(d->modeButtons); | 827 | iZap(d->modeButtons); |
798 | d->resizer = NULL; | 828 | d->resizer = NULL; |
799 | d->list = NULL; | 829 | d->list = NULL; |
800 | d->actions = NULL; | 830 | d->certList = NULL; |
831 | d->actions = NULL; | ||
801 | d->closedFolders = new_IntSet(); | 832 | d->closedFolders = new_IntSet(); |
802 | /* On a phone, the right sidebar is used exclusively for Identities. */ | 833 | /* On a phone, the right sidebar is not used. */ |
803 | const iBool isPhone = deviceType_App() == phone_AppDeviceType; | 834 | const iBool isPhone = (deviceType_App() == phone_AppDeviceType); |
804 | if (!isPhone || d->side == left_SidebarSide) { | 835 | if (isPhone) { |
836 | iLabelWidget *sheetTitle = | ||
837 | addChildFlags_Widget(vdiv, | ||
838 | iClob(new_LabelWidget("", NULL)), | ||
839 | collapse_WidgetFlag | | ||
840 | extraPadding_WidgetFlag | frameless_WidgetFlag); | ||
841 | setBackgroundColor_Widget(as_Widget(sheetTitle), uiBackground_ColorId); | ||
842 | iLabelWidget *closeButton = addChildFlags_Widget(as_Widget(sheetTitle), | ||
843 | iClob(new_LabelWidget(uiTextAction_ColorEscape "${sidebar.close}", "sidebar.toggle")), | ||
844 | extraPadding_WidgetFlag | frameless_WidgetFlag | | ||
845 | alignRight_WidgetFlag | moveToParentRightEdge_WidgetFlag); | ||
846 | as_Widget(sheetTitle)->flags2 |= slidingSheetDraggable_WidgetFlag2; /* phone */ | ||
847 | as_Widget(closeButton)->flags2 |= slidingSheetDraggable_WidgetFlag2; /* phone */ | ||
848 | setId_Widget(as_Widget(sheetTitle), "sidebar.title"); | ||
849 | setId_Widget(as_Widget(closeButton), "sidebar.close"); | ||
850 | setFont_LabelWidget(sheetTitle, uiLabelBig_FontId); | ||
851 | setFont_LabelWidget(closeButton, uiLabelBigBold_FontId); | ||
852 | iConnect(Root, get_Root(), visualOffsetsChanged, d, updateSlidingSheetHeight_SidebarWidget_); | ||
853 | } | ||
805 | iWidget *buttons = new_Widget(); | 854 | iWidget *buttons = new_Widget(); |
806 | setId_Widget(buttons, "buttons"); | 855 | setId_Widget(buttons, "buttons"); |
807 | setDrawBufferEnabled_Widget(buttons, iTrue); | 856 | setDrawBufferEnabled_Widget(buttons, iTrue); |
808 | for (int i = 0; i < max_SidebarMode; i++) { | 857 | for (int i = 0; i < max_SidebarMode; i++) { |
809 | if (deviceType_App() == phone_AppDeviceType && i == identities_SidebarMode) { | 858 | if (i == identities_SidebarMode && deviceType_App() != desktop_AppDeviceType) { |
859 | /* On mobile, identities are managed via Settings. */ | ||
810 | continue; | 860 | continue; |
811 | } | 861 | } |
812 | d->modeButtons[i] = addChildFlags_Widget( | 862 | d->modeButtons[i] = addChildFlags_Widget( |
@@ -815,51 +865,54 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) { | |||
815 | tightModeLabels_[i], | 865 | tightModeLabels_[i], |
816 | format_CStr("%s.mode arg:%d", cstr_String(id_Widget(w)), i))), | 866 | format_CStr("%s.mode arg:%d", cstr_String(id_Widget(w)), i))), |
817 | frameless_WidgetFlag | noBackground_WidgetFlag); | 867 | frameless_WidgetFlag | noBackground_WidgetFlag); |
868 | as_Widget(d->modeButtons[i])->flags2 |= slidingSheetDraggable_WidgetFlag2; /* phone */ | ||
818 | } | 869 | } |
819 | setButtonFont_SidebarWidget(d, isPhone ? uiLabelBig_FontId : uiLabel_FontId); | 870 | setButtonFont_SidebarWidget(d, isPhone ? uiLabelBig_FontId : uiLabel_FontId); |
820 | addChildFlags_Widget(vdiv, | 871 | addChildFlags_Widget(vdiv, |
821 | iClob(buttons), | 872 | iClob(buttons), |
822 | arrangeHorizontal_WidgetFlag | | 873 | arrangeHorizontal_WidgetFlag | resizeWidthOfChildren_WidgetFlag | |
823 | resizeWidthOfChildren_WidgetFlag | | 874 | arrangeHeight_WidgetFlag | resizeToParentWidth_WidgetFlag); |
824 | arrangeHeight_WidgetFlag | resizeToParentWidth_WidgetFlag); // | | ||
825 | // drawBackgroundToHorizontalSafeArea_WidgetFlag); | ||
826 | setBackgroundColor_Widget(buttons, uiBackgroundSidebar_ColorId); | 875 | setBackgroundColor_Widget(buttons, uiBackgroundSidebar_ColorId); |
827 | } | ||
828 | else { | ||
829 | iLabelWidget *heading = new_LabelWidget(person_Icon " ${sidebar.identities}", NULL); | ||
830 | checkIcon_LabelWidget(heading); | ||
831 | setBackgroundColor_Widget(as_Widget(heading), uiBackgroundSidebar_ColorId); | ||
832 | setTextColor_LabelWidget(heading, uiTextSelected_ColorId); | ||
833 | setFont_LabelWidget(addChildFlags_Widget(vdiv, iClob(heading), borderTop_WidgetFlag | | ||
834 | alignLeft_WidgetFlag | frameless_WidgetFlag | | ||
835 | drawBackgroundToHorizontalSafeArea_WidgetFlag), | ||
836 | uiLabelLargeBold_FontId); | ||
837 | } | ||
838 | iWidget *content = new_Widget(); | 876 | iWidget *content = new_Widget(); |
839 | setFlags_Widget(content, resizeChildren_WidgetFlag, iTrue); | 877 | setFlags_Widget(content, resizeChildren_WidgetFlag, iTrue); |
840 | iWidget *listAndActions = makeVDiv_Widget(); | 878 | iWidget *listAndActions = makeVDiv_Widget(); |
841 | addChild_Widget(content, iClob(listAndActions)); | 879 | addChild_Widget(content, iClob(listAndActions)); |
880 | iWidget *listArea = new_Widget(); | ||
881 | setFlags_Widget(listArea, resizeChildren_WidgetFlag, iTrue); | ||
842 | d->list = new_ListWidget(); | 882 | d->list = new_ListWidget(); |
843 | setPadding_Widget(as_Widget(d->list), 0, gap_UI, 0, gap_UI); | 883 | setPadding_Widget(as_Widget(d->list), 0, gap_UI, 0, gap_UI); |
884 | addChild_Widget(listArea, iClob(d->list)); | ||
885 | if (!isPhone) { | ||
886 | d->certList = new_CertListWidget(); | ||
887 | setPadding_Widget(as_Widget(d->certList), 0, gap_UI, 0, gap_UI); | ||
888 | addChild_Widget(listArea, iClob(d->certList)); | ||
889 | } | ||
844 | addChildFlags_Widget(listAndActions, | 890 | addChildFlags_Widget(listAndActions, |
845 | iClob(d->list), | 891 | iClob(listArea), |
846 | expand_WidgetFlag); // | drawBackgroundToHorizontalSafeArea_WidgetFlag); | 892 | expand_WidgetFlag); // | drawBackgroundToHorizontalSafeArea_WidgetFlag); |
847 | setId_Widget(addChildPosFlags_Widget(listAndActions, | 893 | setId_Widget(addChildPosFlags_Widget(listAndActions, |
848 | iClob(d->actions = new_Widget()), | 894 | iClob(d->actions = new_Widget()), |
849 | isPhone ? front_WidgetAddPos : back_WidgetAddPos, | 895 | /*isPhone ? front_WidgetAddPos :*/ back_WidgetAddPos, |
850 | arrangeHorizontal_WidgetFlag | arrangeHeight_WidgetFlag | | 896 | arrangeHorizontal_WidgetFlag | arrangeHeight_WidgetFlag | |
851 | resizeWidthOfChildren_WidgetFlag), // | | 897 | resizeWidthOfChildren_WidgetFlag), // | |
852 | // drawBackgroundToHorizontalSafeArea_WidgetFlag), | 898 | // drawBackgroundToHorizontalSafeArea_WidgetFlag), |
853 | "actions"); | 899 | "actions"); |
900 | if (deviceType_App() != desktop_AppDeviceType) { | ||
901 | setFlags_Widget(findChild_Widget(w, "sidebar.title"), borderTop_WidgetFlag, iTrue); | ||
902 | setFlags_Widget(d->actions, drawBackgroundToBottom_WidgetFlag, iTrue); | ||
903 | setBackgroundColor_Widget(d->actions, uiBackground_ColorId); | ||
904 | } | ||
905 | else { | ||
854 | setBackgroundColor_Widget(d->actions, uiBackgroundSidebar_ColorId); | 906 | setBackgroundColor_Widget(d->actions, uiBackgroundSidebar_ColorId); |
907 | } | ||
855 | d->contextItem = NULL; | 908 | d->contextItem = NULL; |
856 | d->contextIndex = iInvalidPos; | 909 | d->contextIndex = iInvalidPos; |
857 | d->blank = new_Widget(); | 910 | d->blank = new_Widget(); |
858 | addChildFlags_Widget(content, iClob(d->blank), resizeChildren_WidgetFlag); | 911 | addChildFlags_Widget(content, iClob(d->blank), resizeChildren_WidgetFlag); |
859 | addChildFlags_Widget(vdiv, iClob(content), expand_WidgetFlag); | 912 | addChildFlags_Widget(vdiv, iClob(content), expand_WidgetFlag); |
860 | setMode_SidebarWidget(d, | 913 | setMode_SidebarWidget(d, |
861 | deviceType_App() == phone_AppDeviceType && d->side == right_SidebarSide ? | 914 | /*deviceType_App() == phone_AppDeviceType && d->side == right_SidebarSide ? |
862 | identities_SidebarMode : bookmarks_SidebarMode); | 915 | identities_SidebarMode :*/ bookmarks_SidebarMode); |
863 | d->resizer = | 916 | d->resizer = |
864 | addChildFlags_Widget(w, | 917 | addChildFlags_Widget(w, |
865 | iClob(new_Widget()), | 918 | iClob(new_Widget()), |
@@ -867,7 +920,7 @@ void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) { | |||
867 | resizeToParentHeight_WidgetFlag | | 920 | resizeToParentHeight_WidgetFlag | |
868 | (side == left_SidebarSide ? moveToParentRightEdge_WidgetFlag | 921 | (side == left_SidebarSide ? moveToParentRightEdge_WidgetFlag |
869 | : moveToParentLeftEdge_WidgetFlag)); | 922 | : moveToParentLeftEdge_WidgetFlag)); |
870 | if (deviceType_App() == phone_AppDeviceType) { | 923 | if (deviceType_App() != desktop_AppDeviceType) { |
871 | setFlags_Widget(d->resizer, hidden_WidgetFlag | disabled_WidgetFlag, iTrue); | 924 | setFlags_Widget(d->resizer, hidden_WidgetFlag | disabled_WidgetFlag, iTrue); |
872 | } | 925 | } |
873 | setId_Widget(d->resizer, side == left_SidebarSide ? "sidebar.grab" : "sidebar2.grab"); | 926 | setId_Widget(d->resizer, side == left_SidebarSide ? "sidebar.grab" : "sidebar2.grab"); |
@@ -902,16 +955,16 @@ iBool setButtonFont_SidebarWidget(iSidebarWidget *d, int font) { | |||
902 | 955 | ||
903 | static const iGmIdentity *constHoverIdentity_SidebarWidget_(const iSidebarWidget *d) { | 956 | static const iGmIdentity *constHoverIdentity_SidebarWidget_(const iSidebarWidget *d) { |
904 | if (d->mode == identities_SidebarMode) { | 957 | if (d->mode == identities_SidebarMode) { |
905 | const iSidebarItem *hoverItem = constHoverItem_ListWidget(d->list); | 958 | return constHoverIdentity_CertListWidget(d->certList); |
906 | if (hoverItem) { | ||
907 | return identity_GmCerts(certs_App(), hoverItem->id); | ||
908 | } | 959 | } |
909 | } | ||
910 | return NULL; | 960 | return NULL; |
911 | } | 961 | } |
912 | 962 | ||
913 | static iGmIdentity *hoverIdentity_SidebarWidget_(const iSidebarWidget *d) { | 963 | static iGmIdentity *hoverIdentity_SidebarWidget_(const iSidebarWidget *d) { |
914 | return iConstCast(iGmIdentity *, constHoverIdentity_SidebarWidget_(d)); | 964 | if (d->mode == identities_SidebarMode) { |
965 | return hoverIdentity_CertListWidget(d->certList); | ||
966 | } | ||
967 | return NULL; | ||
915 | } | 968 | } |
916 | 969 | ||
917 | static void itemClicked_SidebarWidget_(iSidebarWidget *d, iSidebarItem *item, size_t itemIndex) { | 970 | static void itemClicked_SidebarWidget_(iSidebarWidget *d, iSidebarItem *item, size_t itemIndex) { |
@@ -923,7 +976,6 @@ static void itemClicked_SidebarWidget_(iSidebarWidget *d, iSidebarItem *item, si | |||
923 | const iGmHeading *head = constAt_Array(headings_GmDocument(doc), item->id); | 976 | const iGmHeading *head = constAt_Array(headings_GmDocument(doc), item->id); |
924 | postCommandf_App("document.goto loc:%p", head->text.start); | 977 | postCommandf_App("document.goto loc:%p", head->text.start); |
925 | dismissPortraitPhoneSidebars_Root(as_Widget(d)->root); | 978 | dismissPortraitPhoneSidebars_Root(as_Widget(d)->root); |
926 | setOpenedFromSidebar_DocumentWidget(document_App(), iTrue); | ||
927 | } | 979 | } |
928 | break; | 980 | break; |
929 | } | 981 | } |
@@ -945,6 +997,12 @@ static void itemClicked_SidebarWidget_(iSidebarWidget *d, iSidebarItem *item, si | |||
945 | updateItems_SidebarWidget_(d); | 997 | updateItems_SidebarWidget_(d); |
946 | break; | 998 | break; |
947 | } | 999 | } |
1000 | if (d->isEditing) { | ||
1001 | d->contextItem = item; | ||
1002 | d->contextIndex = itemIndex; | ||
1003 | postCommand_Widget(d, "bookmark.edit"); | ||
1004 | break; | ||
1005 | } | ||
948 | /* fall through */ | 1006 | /* fall through */ |
949 | case history_SidebarMode: { | 1007 | case history_SidebarMode: { |
950 | if (!isEmpty_String(&item->url)) { | 1008 | if (!isEmpty_String(&item->url)) { |
@@ -954,23 +1012,6 @@ static void itemClicked_SidebarWidget_(iSidebarWidget *d, iSidebarItem *item, si | |||
954 | } | 1012 | } |
955 | break; | 1013 | break; |
956 | } | 1014 | } |
957 | case identities_SidebarMode: { | ||
958 | d->contextItem = item; | ||
959 | if (d->contextIndex != iInvalidPos) { | ||
960 | invalidateItem_ListWidget(d->list, d->contextIndex); | ||
961 | } | ||
962 | d->contextIndex = itemIndex; | ||
963 | if (itemIndex < numItems_ListWidget(d->list)) { | ||
964 | updateContextMenu_SidebarWidget_(d); | ||
965 | arrange_Widget(d->menu); | ||
966 | openMenu_Widget(d->menu, | ||
967 | d->side == left_SidebarSide | ||
968 | ? topRight_Rect(itemRect_ListWidget(d->list, itemIndex)) | ||
969 | : addX_I2(topLeft_Rect(itemRect_ListWidget(d->list, itemIndex)), | ||
970 | -width_Widget(d->menu))); | ||
971 | } | ||
972 | break; | ||
973 | } | ||
974 | default: | 1015 | default: |
975 | break; | 1016 | break; |
976 | } | 1017 | } |
@@ -990,7 +1031,7 @@ static void checkModeButtonLayout_SidebarWidget_(iSidebarWidget *d) { | |||
990 | // updateMetrics_SidebarWidget_(d); | 1031 | // updateMetrics_SidebarWidget_(d); |
991 | updateItemHeight_SidebarWidget_(d); | 1032 | updateItemHeight_SidebarWidget_(d); |
992 | } | 1033 | } |
993 | setButtonFont_SidebarWidget(d, isPortrait_App() ? uiLabelBig_FontId : uiLabel_FontId); | 1034 | setButtonFont_SidebarWidget(d, isPortrait_App() ? uiLabelMedium_FontId : uiLabel_FontId); |
994 | } | 1035 | } |
995 | const iBool isTight = | 1036 | const iBool isTight = |
996 | (width_Rect(bounds_Widget(as_Widget(d->modeButtons[0]))) < d->maxButtonLabelWidth); | 1037 | (width_Rect(bounds_Widget(as_Widget(d->modeButtons[0]))) < d->maxButtonLabelWidth); |
@@ -1018,6 +1059,7 @@ static void checkModeButtonLayout_SidebarWidget_(iSidebarWidget *d) { | |||
1018 | } | 1059 | } |
1019 | 1060 | ||
1020 | void setWidth_SidebarWidget(iSidebarWidget *d, float widthAsGaps) { | 1061 | void setWidth_SidebarWidget(iSidebarWidget *d, float widthAsGaps) { |
1062 | if (!d) return; | ||
1021 | iWidget *w = as_Widget(d); | 1063 | iWidget *w = as_Widget(d); |
1022 | const iBool isFixedWidth = deviceType_App() == phone_AppDeviceType; | 1064 | const iBool isFixedWidth = deviceType_App() == phone_AppDeviceType; |
1023 | int width = widthAsGaps * gap_UI; /* in pixels */ | 1065 | int width = widthAsGaps * gap_UI; /* in pixels */ |
@@ -1056,19 +1098,16 @@ iBool handleBookmarkEditorCommands_SidebarWidget_(iWidget *editor, const char *c | |||
1056 | set_String(&bm->url, url); | 1098 | set_String(&bm->url, url); |
1057 | set_String(&bm->tags, tags); | 1099 | set_String(&bm->tags, tags); |
1058 | if (isEmpty_String(icon)) { | 1100 | if (isEmpty_String(icon)) { |
1059 | removeTag_Bookmark(bm, userIcon_BookmarkTag); | 1101 | bm->flags &= ~userIcon_BookmarkFlag; |
1060 | bm->icon = 0; | 1102 | bm->icon = 0; |
1061 | } | 1103 | } |
1062 | else { | 1104 | else { |
1063 | addTagIfMissing_Bookmark(bm, userIcon_BookmarkTag); | 1105 | bm->flags |= userIcon_BookmarkFlag; |
1064 | bm->icon = first_String(icon); | 1106 | bm->icon = first_String(icon); |
1065 | } | 1107 | } |
1066 | addOrRemoveTag_Bookmark(bm, homepage_BookmarkTag, | 1108 | iChangeFlags(bm->flags, homepage_BookmarkFlag, isSelected_Widget(findChild_Widget(editor, "bmed.tag.home"))); |
1067 | isSelected_Widget(findChild_Widget(editor, "bmed.tag.home"))); | 1109 | iChangeFlags(bm->flags, remoteSource_BookmarkFlag, isSelected_Widget(findChild_Widget(editor, "bmed.tag.remote"))); |
1068 | addOrRemoveTag_Bookmark(bm, remoteSource_BookmarkTag, | 1110 | iChangeFlags(bm->flags, linkSplit_BookmarkFlag, isSelected_Widget(findChild_Widget(editor, "bmed.tag.linksplit"))); |
1069 | isSelected_Widget(findChild_Widget(editor, "bmed.tag.remote"))); | ||
1070 | addOrRemoveTag_Bookmark(bm, linkSplit_BookmarkTag, | ||
1071 | isSelected_Widget(findChild_Widget(editor, "bmed.tag.linksplit"))); | ||
1072 | } | 1111 | } |
1073 | const iBookmark *folder = userData_Object(findChild_Widget(editor, "bmed.folder")); | 1112 | const iBookmark *folder = userData_Object(findChild_Widget(editor, "bmed.folder")); |
1074 | if (!folder || !hasParent_Bookmark(folder, id_Bookmark(bm))) { | 1113 | if (!folder || !hasParent_Bookmark(folder, id_Bookmark(bm))) { |
@@ -1083,6 +1122,36 @@ iBool handleBookmarkEditorCommands_SidebarWidget_(iWidget *editor, const char *c | |||
1083 | return iFalse; | 1122 | return iFalse; |
1084 | } | 1123 | } |
1085 | 1124 | ||
1125 | enum iSlidingSheetPos { | ||
1126 | top_SlidingSheetPos, | ||
1127 | middle_SlidingSheetPos, | ||
1128 | bottom_SlidingSheetPos, | ||
1129 | }; | ||
1130 | |||
1131 | static void setSlidingSheetPos_SidebarWidget_(iSidebarWidget *d, enum iSlidingSheetPos slide) { | ||
1132 | iWidget *w = as_Widget(d); | ||
1133 | const int pos = w->rect.pos.y; | ||
1134 | const iRect safeRect = safeRect_Root(w->root); | ||
1135 | if (slide == top_SlidingSheetPos) { | ||
1136 | w->rect.pos.y = top_Rect(safeRect); | ||
1137 | w->rect.size.y = height_Rect(safeRect); | ||
1138 | setVisualOffset_Widget(w, pos - w->rect.pos.y, 0, 0); | ||
1139 | setVisualOffset_Widget(w, 0, 200, easeOut_AnimFlag | softer_AnimFlag); | ||
1140 | setScrollMode_ListWidget(d->list, disabledAtTopUpwards_ScrollMode); | ||
1141 | } | ||
1142 | else if (slide == bottom_SlidingSheetPos) { | ||
1143 | postCommand_Widget(w, "sidebar.toggle"); | ||
1144 | } | ||
1145 | else { | ||
1146 | w->rect.size.y = d->midHeight; | ||
1147 | w->rect.pos.y = height_Rect(safeRect) - w->rect.size.y; | ||
1148 | setVisualOffset_Widget(w, pos - w->rect.pos.y, 0, 0); | ||
1149 | setVisualOffset_Widget(w, 0, 200, easeOut_AnimFlag | softer_AnimFlag); | ||
1150 | setScrollMode_ListWidget(d->list, disabledAtTopBothDirections_ScrollMode); | ||
1151 | } | ||
1152 | // animateSlidingSheetHeight_SidebarWidget_(d); | ||
1153 | } | ||
1154 | |||
1086 | static iBool handleSidebarCommand_SidebarWidget_(iSidebarWidget *d, const char *cmd) { | 1155 | static iBool handleSidebarCommand_SidebarWidget_(iSidebarWidget *d, const char *cmd) { |
1087 | iWidget *w = as_Widget(d); | 1156 | iWidget *w = as_Widget(d); |
1088 | if (equal_Command(cmd, "width")) { | 1157 | if (equal_Command(cmd, "width")) { |
@@ -1112,13 +1181,18 @@ static iBool handleSidebarCommand_SidebarWidget_(iSidebarWidget *d, const char * | |||
1112 | argLabel_Command(cmd, "noanim") == 0 && | 1181 | argLabel_Command(cmd, "noanim") == 0 && |
1113 | (d->side == left_SidebarSide || deviceType_App() != phone_AppDeviceType); | 1182 | (d->side == left_SidebarSide || deviceType_App() != phone_AppDeviceType); |
1114 | int visX = 0; | 1183 | int visX = 0; |
1184 | int visY = 0; | ||
1115 | if (isVisible_Widget(w)) { | 1185 | if (isVisible_Widget(w)) { |
1116 | visX = left_Rect(bounds_Widget(w)) - left_Rect(w->root->widget->rect); | 1186 | visX = left_Rect(bounds_Widget(w)) - left_Rect(w->root->widget->rect); |
1187 | visY = top_Rect(bounds_Widget(w)) - top_Rect(w->root->widget->rect); | ||
1117 | } | 1188 | } |
1118 | setFlags_Widget(w, hidden_WidgetFlag, isVisible_Widget(w)); | 1189 | const iBool isHiding = isVisible_Widget(w); |
1190 | setFlags_Widget(w, hidden_WidgetFlag, isHiding); | ||
1119 | /* Safe area inset for mobile. */ | 1191 | /* Safe area inset for mobile. */ |
1120 | const int safePad = (d->side == left_SidebarSide ? left_Rect(safeRect_Root(w->root)) : 0); | 1192 | const int safePad = (d->side == left_SidebarSide ? left_Rect(safeRect_Root(w->root)) : 0); |
1121 | if (isVisible_Widget(w)) { | 1193 | const int animFlags = easeOut_AnimFlag | softer_AnimFlag; |
1194 | if (!isPortraitPhone_App()) { | ||
1195 | if (!isHiding) { | ||
1122 | setFlags_Widget(w, keepOnTop_WidgetFlag, iFalse); | 1196 | setFlags_Widget(w, keepOnTop_WidgetFlag, iFalse); |
1123 | w->rect.size.x = d->widthAsGaps * gap_UI; | 1197 | w->rect.size.x = d->widthAsGaps * gap_UI; |
1124 | invalidate_ListWidget(d->list); | 1198 | invalidate_ListWidget(d->list); |
@@ -1126,7 +1200,7 @@ static iBool handleSidebarCommand_SidebarWidget_(iSidebarWidget *d, const char * | |||
1126 | setFlags_Widget(w, horizontalOffset_WidgetFlag, iTrue); | 1200 | setFlags_Widget(w, horizontalOffset_WidgetFlag, iTrue); |
1127 | setVisualOffset_Widget( | 1201 | setVisualOffset_Widget( |
1128 | w, (d->side == left_SidebarSide ? -1 : 1) * (w->rect.size.x + safePad), 0, 0); | 1202 | w, (d->side == left_SidebarSide ? -1 : 1) * (w->rect.size.x + safePad), 0, 0); |
1129 | setVisualOffset_Widget(w, 0, 300, easeOut_AnimFlag | softer_AnimFlag); | 1203 | setVisualOffset_Widget(w, 0, 300, animFlags); |
1130 | } | 1204 | } |
1131 | } | 1205 | } |
1132 | else if (isAnimated) { | 1206 | else if (isAnimated) { |
@@ -1134,19 +1208,42 @@ static iBool handleSidebarCommand_SidebarWidget_(iSidebarWidget *d, const char * | |||
1134 | if (d->side == right_SidebarSide) { | 1208 | if (d->side == right_SidebarSide) { |
1135 | setVisualOffset_Widget(w, visX, 0, 0); | 1209 | setVisualOffset_Widget(w, visX, 0, 0); |
1136 | setVisualOffset_Widget( | 1210 | setVisualOffset_Widget( |
1137 | w, visX + w->rect.size.x + safePad, 300, easeOut_AnimFlag | softer_AnimFlag); | 1211 | w, visX + w->rect.size.x + safePad, 300, animFlags); |
1138 | } | 1212 | } |
1139 | else { | 1213 | else { |
1140 | setFlags_Widget(w, keepOnTop_WidgetFlag, iTrue); | 1214 | setFlags_Widget(w, keepOnTop_WidgetFlag, iTrue); |
1141 | setVisualOffset_Widget( | 1215 | setVisualOffset_Widget( |
1142 | w, -w->rect.size.x - safePad, 300, easeOut_AnimFlag | softer_AnimFlag); | 1216 | w, -w->rect.size.x - safePad, 300, animFlags); |
1217 | } | ||
1143 | } | 1218 | } |
1219 | setScrollMode_ListWidget(d->list, normal_ScrollMode); | ||
1220 | } | ||
1221 | else { | ||
1222 | /* Portrait phone sidebar works differently: it slides up from the bottom. */ | ||
1223 | setFlags_Widget(w, horizontalOffset_WidgetFlag, iFalse); | ||
1224 | if (!isHiding) { | ||
1225 | invalidate_ListWidget(d->list); | ||
1226 | w->rect.pos.y = height_Rect(safeRect_Root(w->root)) - d->midHeight; | ||
1227 | setVisualOffset_Widget(w, bottom_Rect(rect_Root(w->root)) - w->rect.pos.y, 0, 0); | ||
1228 | setVisualOffset_Widget(w, 0, 300, animFlags); | ||
1229 | //animateSlidingSheetHeight_SidebarWidget_(d); | ||
1230 | setScrollMode_ListWidget(d->list, disabledAtTopBothDirections_ScrollMode); | ||
1231 | } | ||
1232 | else { | ||
1233 | setVisualOffset_Widget(w, bottom_Rect(rect_Root(w->root)) - w->rect.pos.y, 300, animFlags); | ||
1234 | if (d->isEditing) { | ||
1235 | setMobileEditMode_SidebarWidget_(d, iFalse); | ||
1236 | } | ||
1237 | } | ||
1238 | showToolbar_Root(w->root, isHiding); | ||
1144 | } | 1239 | } |
1145 | updateToolbarColors_Root(w->root); | 1240 | updateToolbarColors_Root(w->root); |
1146 | arrange_Widget(w->parent); | 1241 | arrange_Widget(w->parent); |
1147 | /* BUG: Rearranging because the arrange above didn't fully resolve the height. */ | 1242 | /* BUG: Rearranging because the arrange above didn't fully resolve the height. */ |
1148 | arrange_Widget(w); | 1243 | arrange_Widget(w); |
1244 | if (!isPortraitPhone_App()) { | ||
1149 | updateSize_DocumentWidget(document_App()); | 1245 | updateSize_DocumentWidget(document_App()); |
1246 | } | ||
1150 | if (isVisible_Widget(w)) { | 1247 | if (isVisible_Widget(w)) { |
1151 | updateItems_SidebarWidget_(d); | 1248 | updateItems_SidebarWidget_(d); |
1152 | scrollOffset_ListWidget(d->list, 0); | 1249 | scrollOffset_ListWidget(d->list, 0); |
@@ -1154,6 +1251,10 @@ static iBool handleSidebarCommand_SidebarWidget_(iSidebarWidget *d, const char * | |||
1154 | refresh_Widget(w->parent); | 1251 | refresh_Widget(w->parent); |
1155 | return iTrue; | 1252 | return iTrue; |
1156 | } | 1253 | } |
1254 | else if (equal_Command(cmd, "bookmarks.edit")) { | ||
1255 | setMobileEditMode_SidebarWidget_(d, !d->isEditing); | ||
1256 | invalidate_ListWidget(d->list); | ||
1257 | } | ||
1157 | return iFalse; | 1258 | return iFalse; |
1158 | } | 1259 | } |
1159 | 1260 | ||
@@ -1166,7 +1267,7 @@ static void bookmarkMoved_SidebarWidget_(iSidebarWidget *d, size_t index, size_t | |||
1166 | : dstIndex); | 1267 | : dstIndex); |
1167 | if (isLast && isBefore) isBefore = iFalse; | 1268 | if (isLast && isBefore) isBefore = iFalse; |
1168 | const iBookmark *dst = get_Bookmarks(bookmarks_App(), dstItem->id); | 1269 | const iBookmark *dst = get_Bookmarks(bookmarks_App(), dstItem->id); |
1169 | if (hasParent_Bookmark(dst, movingItem->id) || hasTag_Bookmark(dst, remote_BookmarkTag)) { | 1270 | if (hasParent_Bookmark(dst, movingItem->id) || dst->flags & remote_BookmarkFlag) { |
1170 | /* Can't move a folder inside itself, and remote bookmarks cannot be reordered. */ | 1271 | /* Can't move a folder inside itself, and remote bookmarks cannot be reordered. */ |
1171 | return; | 1272 | return; |
1172 | } | 1273 | } |
@@ -1190,20 +1291,41 @@ static void bookmarkMovedOntoFolder_SidebarWidget_(iSidebarWidget *d, size_t ind | |||
1190 | static size_t numBookmarks_(const iPtrArray *bmList) { | 1291 | static size_t numBookmarks_(const iPtrArray *bmList) { |
1191 | size_t num = 0; | 1292 | size_t num = 0; |
1192 | iConstForEach(PtrArray, i, bmList) { | 1293 | iConstForEach(PtrArray, i, bmList) { |
1193 | if (!isFolder_Bookmark(i.ptr) && !hasTag_Bookmark(i.ptr, remote_BookmarkTag)) { | 1294 | const iBookmark *bm = i.ptr; |
1295 | if (!isFolder_Bookmark(bm) && ~bm->flags & remote_BookmarkFlag) { | ||
1194 | num++; | 1296 | num++; |
1195 | } | 1297 | } |
1196 | } | 1298 | } |
1197 | return num; | 1299 | return num; |
1198 | } | 1300 | } |
1199 | 1301 | ||
1302 | static iRangei SlidingSheetMiddleRegion_SidebarWidget_(const iSidebarWidget *d) { | ||
1303 | const iWidget *w = constAs_Widget(d); | ||
1304 | const iRect safeRect = safeRect_Root(w->root); | ||
1305 | const int midY = bottom_Rect(safeRect) - d->midHeight; | ||
1306 | const int topHalf = (top_Rect(safeRect) + midY) / 2; | ||
1307 | const int bottomHalf = (bottom_Rect(safeRect) + midY * 2) / 3; | ||
1308 | return (iRangei){ topHalf, bottomHalf }; | ||
1309 | } | ||
1310 | |||
1311 | static void gotoNearestSlidingSheetPos_SidebarWidget_(iSidebarWidget *d) { | ||
1312 | const iRangei midRegion = SlidingSheetMiddleRegion_SidebarWidget_(d); | ||
1313 | const int pos = top_Rect(d->widget.rect); | ||
1314 | setSlidingSheetPos_SidebarWidget_(d, pos < midRegion.start | ||
1315 | ? top_SlidingSheetPos | ||
1316 | : pos > midRegion.end ? bottom_SlidingSheetPos | ||
1317 | : middle_SlidingSheetPos); | ||
1318 | } | ||
1319 | |||
1200 | static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) { | 1320 | static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) { |
1201 | iWidget *w = as_Widget(d); | 1321 | iWidget *w = as_Widget(d); |
1202 | /* Handle commands. */ | 1322 | /* Handle commands. */ |
1203 | if (isResize_UserEvent(ev)) { | 1323 | if (isResize_UserEvent(ev)) { |
1204 | checkModeButtonLayout_SidebarWidget_(d); | 1324 | checkModeButtonLayout_SidebarWidget_(d); |
1205 | if (deviceType_App() == phone_AppDeviceType && d->side == left_SidebarSide) { | 1325 | if (deviceType_App() == phone_AppDeviceType) { |
1206 | setFlags_Widget(w, rightEdgeDraggable_WidgetFlag, isPortrait_App()); | 1326 | setPadding_Widget(d->actions, 0, 0, 0, 0); |
1327 | setFlags_Widget(findChild_Widget(w, "sidebar.title"), hidden_WidgetFlag, isLandscape_App()); | ||
1328 | setFlags_Widget(findChild_Widget(w, "sidebar.close"), hidden_WidgetFlag, isLandscape_App()); | ||
1207 | /* In landscape, visibility of the toolbar is controlled separately. */ | 1329 | /* In landscape, visibility of the toolbar is controlled separately. */ |
1208 | if (isVisible_Widget(w)) { | 1330 | if (isVisible_Widget(w)) { |
1209 | postCommand_Widget(w, "sidebar.toggle"); | 1331 | postCommand_Widget(w, "sidebar.toggle"); |
@@ -1217,8 +1339,16 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
1217 | setFlags_Widget(as_Widget(d->list), | 1339 | setFlags_Widget(as_Widget(d->list), |
1218 | drawBackgroundToHorizontalSafeArea_WidgetFlag, | 1340 | drawBackgroundToHorizontalSafeArea_WidgetFlag, |
1219 | isLandscape_App()); | 1341 | isLandscape_App()); |
1220 | return iFalse; | 1342 | setFlags_Widget(w, |
1343 | drawBackgroundToBottom_WidgetFlag, | ||
1344 | isPortrait_App()); | ||
1345 | setBackgroundColor_Widget(w, isPortrait_App() ? uiBackgroundSidebar_ColorId : none_ColorId); | ||
1346 | } | ||
1347 | if (!isPortraitPhone_App()) { | ||
1348 | /* In sliding sheet mode, sidebar is resized to fit in the safe area. */ | ||
1349 | setPadding_Widget(d->actions, 0, 0, 0, bottomSafeInset_Mobile()); | ||
1221 | } | 1350 | } |
1351 | return iFalse; | ||
1222 | } | 1352 | } |
1223 | else if (isMetricsChange_UserEvent(ev)) { | 1353 | else if (isMetricsChange_UserEvent(ev)) { |
1224 | if (isVisible_Widget(w)) { | 1354 | if (isVisible_Widget(w)) { |
@@ -1230,7 +1360,7 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
1230 | } | 1360 | } |
1231 | else if (ev->type == SDL_USEREVENT && ev->user.code == command_UserEventCode) { | 1361 | else if (ev->type == SDL_USEREVENT && ev->user.code == command_UserEventCode) { |
1232 | const char *cmd = command_UserEvent(ev); | 1362 | const char *cmd = command_UserEvent(ev); |
1233 | if (equal_Command(cmd, "tabs.changed") || equal_Command(cmd, "document.changed")) { | 1363 | if (startsWith_CStr(cmd, "tabs.changed id:doc") || equal_Command(cmd, "document.changed")) { |
1234 | updateItems_SidebarWidget_(d); | 1364 | updateItems_SidebarWidget_(d); |
1235 | scrollOffset_ListWidget(d->list, 0); | 1365 | scrollOffset_ListWidget(d->list, 0); |
1236 | } | 1366 | } |
@@ -1257,13 +1387,6 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
1257 | } | 1387 | } |
1258 | } | 1388 | } |
1259 | } | 1389 | } |
1260 | else if (equal_Command(cmd, "idents.changed") && d->mode == identities_SidebarMode) { | ||
1261 | updateItems_SidebarWidget_(d); | ||
1262 | } | ||
1263 | else if (deviceType_App() == tablet_AppDeviceType && equal_Command(cmd, "toolbar.showident")) { | ||
1264 | postCommandf_App("sidebar.mode arg:%d toggle:1", identities_SidebarMode); | ||
1265 | return iTrue; | ||
1266 | } | ||
1267 | else if (isPortraitPhone_App() && isVisible_Widget(w) && d->side == left_SidebarSide && | 1390 | else if (isPortraitPhone_App() && isVisible_Widget(w) && d->side == left_SidebarSide && |
1268 | equal_Command(cmd, "swipe.forward")) { | 1391 | equal_Command(cmd, "swipe.forward")) { |
1269 | postCommand_App("sidebar.toggle"); | 1392 | postCommand_App("sidebar.toggle"); |
@@ -1328,9 +1451,6 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
1328 | } | 1451 | } |
1329 | return iTrue; | 1452 | return iTrue; |
1330 | } | 1453 | } |
1331 | // else if (isCommand_Widget(w, ev, "menu.closed")) { | ||
1332 | // invalidateItem_ListWidget(d->list, d->contextIndex); | ||
1333 | // } | ||
1334 | else if (isCommand_Widget(w, ev, "bookmark.open")) { | 1454 | else if (isCommand_Widget(w, ev, "bookmark.open")) { |
1335 | const iSidebarItem *item = d->contextItem; | 1455 | const iSidebarItem *item = d->contextItem; |
1336 | if (d->mode == bookmarks_SidebarMode && item) { | 1456 | if (d->mode == bookmarks_SidebarMode && item) { |
@@ -1363,13 +1483,13 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
1363 | if (!isFolder_Bookmark(bm)) { | 1483 | if (!isFolder_Bookmark(bm)) { |
1364 | setText_InputWidget(urlInput, &bm->url); | 1484 | setText_InputWidget(urlInput, &bm->url); |
1365 | setText_InputWidget(tagsInput, &bm->tags); | 1485 | setText_InputWidget(tagsInput, &bm->tags); |
1366 | if (hasTag_Bookmark(bm, userIcon_BookmarkTag)) { | 1486 | if (bm->flags & userIcon_BookmarkFlag) { |
1367 | setText_InputWidget(iconInput, | 1487 | setText_InputWidget(iconInput, |
1368 | collect_String(newUnicodeN_String(&bm->icon, 1))); | 1488 | collect_String(newUnicodeN_String(&bm->icon, 1))); |
1369 | } | 1489 | } |
1370 | setToggle_Widget(homeTag, hasTag_Bookmark(bm, homepage_BookmarkTag)); | 1490 | setToggle_Widget(homeTag, bm->flags & homepage_BookmarkFlag); |
1371 | setToggle_Widget(remoteSourceTag, hasTag_Bookmark(bm, remoteSource_BookmarkTag)); | 1491 | setToggle_Widget(remoteSourceTag, bm->flags & remoteSource_BookmarkFlag); |
1372 | setToggle_Widget(linkSplitTag, hasTag_Bookmark(bm, linkSplit_BookmarkTag)); | 1492 | setToggle_Widget(linkSplitTag, bm->flags & linkSplit_BookmarkFlag); |
1373 | } | 1493 | } |
1374 | else { | 1494 | else { |
1375 | setFlags_Widget(findChild_Widget(dlg, "bmed.special"), | 1495 | setFlags_Widget(findChild_Widget(dlg, "bmed.special"), |
@@ -1390,7 +1510,7 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
1390 | const iSidebarItem *item = d->contextItem; | 1510 | const iSidebarItem *item = d->contextItem; |
1391 | if (d->mode == bookmarks_SidebarMode && item) { | 1511 | if (d->mode == bookmarks_SidebarMode && item) { |
1392 | iBookmark *bm = get_Bookmarks(bookmarks_App(), item->id); | 1512 | iBookmark *bm = get_Bookmarks(bookmarks_App(), item->id); |
1393 | const iBool isRemote = hasTag_Bookmark(bm, remote_BookmarkTag); | 1513 | const iBool isRemote = (bm->flags & remote_BookmarkFlag) != 0; |
1394 | iChar icon = isRemote ? 0x1f588 : bm->icon; | 1514 | iChar icon = isRemote ? 0x1f588 : bm->icon; |
1395 | iWidget *dlg = makeBookmarkCreation_Widget(&bm->url, &bm->title, icon); | 1515 | iWidget *dlg = makeBookmarkCreation_Widget(&bm->url, &bm->title, icon); |
1396 | setId_Widget(dlg, format_CStr("bmed.%s", cstr_String(id_Widget(w)))); | 1516 | setId_Widget(dlg, format_CStr("bmed.%s", cstr_String(id_Widget(w)))); |
@@ -1404,17 +1524,16 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
1404 | else if (isCommand_Widget(w, ev, "bookmark.tag")) { | 1524 | else if (isCommand_Widget(w, ev, "bookmark.tag")) { |
1405 | const iSidebarItem *item = d->contextItem; | 1525 | const iSidebarItem *item = d->contextItem; |
1406 | if (d->mode == bookmarks_SidebarMode && item) { | 1526 | if (d->mode == bookmarks_SidebarMode && item) { |
1407 | const char *tag = cstr_String(string_Command(cmd, "tag")); | 1527 | const iRangecc tag = range_Command(cmd, "tag"); |
1528 | const int flag = | ||
1529 | (equal_Rangecc(tag, "homepage") ? homepage_BookmarkFlag : 0) | | ||
1530 | (equal_Rangecc(tag, "subscribed") ? subscribed_BookmarkFlag : 0) | | ||
1531 | (equal_Rangecc(tag, "remotesource") ? remoteSource_BookmarkFlag : 0); | ||
1408 | iBookmark *bm = get_Bookmarks(bookmarks_App(), item->id); | 1532 | iBookmark *bm = get_Bookmarks(bookmarks_App(), item->id); |
1409 | if (hasTag_Bookmark(bm, tag)) { | 1533 | if (flag == subscribed_BookmarkFlag && (bm->flags & flag)) { |
1410 | removeTag_Bookmark(bm, tag); | 1534 | removeEntries_Feeds(item->id); /* get rid of unsubscribed entries */ |
1411 | if (!iCmpStr(tag, subscribed_BookmarkTag)) { | ||
1412 | removeEntries_Feeds(item->id); | ||
1413 | } | ||
1414 | } | ||
1415 | else { | ||
1416 | addTag_Bookmark(bm, tag); | ||
1417 | } | 1535 | } |
1536 | bm->flags ^= flag; | ||
1418 | postCommand_App("bookmarks.changed"); | 1537 | postCommand_App("bookmarks.changed"); |
1419 | } | 1538 | } |
1420 | return iTrue; | 1539 | return iTrue; |
@@ -1491,6 +1610,15 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
1491 | return iTrue; | 1610 | return iTrue; |
1492 | } | 1611 | } |
1493 | else if (equal_Command(cmd, "feeds.markallread") && d->mode == feeds_SidebarMode) { | 1612 | else if (equal_Command(cmd, "feeds.markallread") && d->mode == feeds_SidebarMode) { |
1613 | if (argLabel_Command(cmd, "confirm")) { | ||
1614 | /* This is used on mobile. */ | ||
1615 | iWidget *menu = makeMenu_Widget(w->root->widget, (iMenuItem[]){ | ||
1616 | check_Icon " " uiTextCaution_ColorEscape "${feeds.markallread}", 0, 0, | ||
1617 | "feeds.markallread" | ||
1618 | }, 1); | ||
1619 | openMenu_Widget(menu, topLeft_Rect(bounds_Widget(d->actions))); | ||
1620 | return iTrue; | ||
1621 | } | ||
1494 | iConstForEach(PtrArray, i, listEntries_Feeds()) { | 1622 | iConstForEach(PtrArray, i, listEntries_Feeds()) { |
1495 | const iFeedEntry *entry = i.ptr; | 1623 | const iFeedEntry *entry = i.ptr; |
1496 | const iString *url = url_FeedEntry(entry); | 1624 | const iString *url = url_FeedEntry(entry); |
@@ -1539,7 +1667,7 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
1539 | } | 1667 | } |
1540 | if (isCommand_Widget(w, ev, "feed.entry.unsubscribe")) { | 1668 | if (isCommand_Widget(w, ev, "feed.entry.unsubscribe")) { |
1541 | if (arg_Command(cmd)) { | 1669 | if (arg_Command(cmd)) { |
1542 | removeTag_Bookmark(feedBookmark, subscribed_BookmarkTag); | 1670 | feedBookmark->flags &= ~subscribed_BookmarkFlag; |
1543 | removeEntries_Feeds(id_Bookmark(feedBookmark)); | 1671 | removeEntries_Feeds(id_Bookmark(feedBookmark)); |
1544 | updateItems_SidebarWidget_(d); | 1672 | updateItems_SidebarWidget_(d); |
1545 | } | 1673 | } |
@@ -1555,108 +1683,11 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
1555 | 0, | 1683 | 0, |
1556 | format_CStr("!feed.entry.unsubscribe arg:1 ptr:%p", d) } }, | 1684 | format_CStr("!feed.entry.unsubscribe arg:1 ptr:%p", d) } }, |
1557 | 2); | 1685 | 2); |
1558 | } | ||
1559 | return iTrue; | ||
1560 | } | ||
1561 | } | ||
1562 | } | ||
1563 | } | ||
1564 | else if (isCommand_Widget(w, ev, "ident.use")) { | ||
1565 | iGmIdentity * ident = menuIdentity_SidebarWidget_(d); | ||
1566 | const iString *tabUrl = url_DocumentWidget(document_App()); | ||
1567 | if (ident) { | ||
1568 | if (argLabel_Command(cmd, "clear")) { | ||
1569 | clearUse_GmIdentity(ident); | ||
1570 | } | ||
1571 | else if (arg_Command(cmd)) { | ||
1572 | signIn_GmCerts(certs_App(), ident, tabUrl); | ||
1573 | postCommand_App("navigate.reload"); | ||
1574 | } | ||
1575 | else { | ||
1576 | signOut_GmCerts(certs_App(), tabUrl); | ||
1577 | postCommand_App("navigate.reload"); | ||
1578 | } | ||
1579 | saveIdentities_GmCerts(certs_App()); | ||
1580 | updateItems_SidebarWidget_(d); | ||
1581 | } | ||
1582 | return iTrue; | ||
1583 | } | ||
1584 | else if (isCommand_Widget(w, ev, "ident.edit")) { | ||
1585 | const iGmIdentity *ident = menuIdentity_SidebarWidget_(d); | ||
1586 | if (ident) { | ||
1587 | makeValueInput_Widget(get_Root()->widget, | ||
1588 | &ident->notes, | ||
1589 | uiHeading_ColorEscape "${heading.ident.notes}", | ||
1590 | format_CStr(cstr_Lang("dlg.ident.notes"), cstr_String(name_GmIdentity(ident))), | ||
1591 | uiTextAction_ColorEscape "${dlg.default}", | ||
1592 | format_CStr("!ident.setnotes ident:%p ptr:%p", ident, d)); | ||
1593 | } | ||
1594 | return iTrue; | ||
1595 | } | ||
1596 | else if (isCommand_Widget(w, ev, "ident.fingerprint")) { | ||
1597 | const iGmIdentity *ident = menuIdentity_SidebarWidget_(d); | ||
1598 | if (ident) { | ||
1599 | const iString *fps = collect_String( | ||
1600 | hexEncode_Block(collect_Block(fingerprint_TlsCertificate(ident->cert)))); | ||
1601 | SDL_SetClipboardText(cstr_String(fps)); | ||
1602 | } | 1686 | } |
1603 | return iTrue; | 1687 | return iTrue; |
1604 | } | 1688 | } |
1605 | else if (isCommand_Widget(w, ev, "ident.export")) { | ||
1606 | const iGmIdentity *ident = menuIdentity_SidebarWidget_(d); | ||
1607 | if (ident) { | ||
1608 | iString *pem = collect_String(pem_TlsCertificate(ident->cert)); | ||
1609 | append_String(pem, collect_String(privateKeyPem_TlsCertificate(ident->cert))); | ||
1610 | iDocumentWidget *expTab = newTab_App(NULL, iTrue); | ||
1611 | setUrlAndSource_DocumentWidget( | ||
1612 | expTab, | ||
1613 | collectNewFormat_String("file:%s.pem", cstr_String(name_GmIdentity(ident))), | ||
1614 | collectNewCStr_String("text/plain"), | ||
1615 | utf8_String(pem)); | ||
1616 | } | 1689 | } |
1617 | return iTrue; | ||
1618 | } | ||
1619 | else if (isCommand_Widget(w, ev, "ident.setnotes")) { | ||
1620 | iGmIdentity *ident = pointerLabel_Command(cmd, "ident"); | ||
1621 | if (ident) { | ||
1622 | setCStr_String(&ident->notes, suffixPtr_Command(cmd, "value")); | ||
1623 | updateItems_SidebarWidget_(d); | ||
1624 | } | ||
1625 | return iTrue; | ||
1626 | } | ||
1627 | else if (isCommand_Widget(w, ev, "ident.pickicon")) { | ||
1628 | return iTrue; | ||
1629 | } | ||
1630 | else if (isCommand_Widget(w, ev, "ident.reveal")) { | ||
1631 | const iGmIdentity *ident = menuIdentity_SidebarWidget_(d); | ||
1632 | if (ident) { | ||
1633 | const iString *crtPath = certificatePath_GmCerts(certs_App(), ident); | ||
1634 | if (crtPath) { | ||
1635 | revealPath_App(crtPath); | ||
1636 | } | ||
1637 | } | 1690 | } |
1638 | return iTrue; | ||
1639 | } | ||
1640 | else if (isCommand_Widget(w, ev, "ident.delete")) { | ||
1641 | iSidebarItem *item = d->contextItem; | ||
1642 | if (argLabel_Command(cmd, "confirm")) { | ||
1643 | makeQuestion_Widget( | ||
1644 | uiTextCaution_ColorEscape "${heading.ident.delete}", | ||
1645 | format_CStr(cstr_Lang("dlg.confirm.ident.delete"), | ||
1646 | uiTextAction_ColorEscape, | ||
1647 | cstr_String(&item->label), | ||
1648 | uiText_ColorEscape), | ||
1649 | (iMenuItem[]){ { "${cancel}", 0, 0, NULL }, | ||
1650 | { uiTextCaution_ColorEscape "${dlg.ident.delete}", | ||
1651 | 0, | ||
1652 | 0, | ||
1653 | format_CStr("!ident.delete confirm:0 ptr:%p", d) } }, | ||
1654 | 2); | ||
1655 | return iTrue; | ||
1656 | } | ||
1657 | deleteIdentity_GmCerts(certs_App(), menuIdentity_SidebarWidget_(d)); | ||
1658 | postCommand_App("idents.changed"); | ||
1659 | return iTrue; | ||
1660 | } | 1691 | } |
1661 | else if (isCommand_Widget(w, ev, "history.delete")) { | 1692 | else if (isCommand_Widget(w, ev, "history.delete")) { |
1662 | if (d->contextItem && !isEmpty_String(&d->contextItem->url)) { | 1693 | if (d->contextItem && !isEmpty_String(&d->contextItem->url)) { |
@@ -1711,14 +1742,10 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
1711 | /* Update cursor. */ | 1742 | /* Update cursor. */ |
1712 | else if (contains_Widget(w, mouse)) { | 1743 | else if (contains_Widget(w, mouse)) { |
1713 | const iSidebarItem *item = constHoverItem_ListWidget(d->list); | 1744 | const iSidebarItem *item = constHoverItem_ListWidget(d->list); |
1714 | if (item && d->mode != identities_SidebarMode) { | ||
1715 | setCursor_Window(get_Window(), | 1745 | setCursor_Window(get_Window(), |
1716 | item->listItem.isSeparator ? SDL_SYSTEM_CURSOR_ARROW | 1746 | item ? (item->listItem.isSeparator ? SDL_SYSTEM_CURSOR_ARROW |
1717 | : SDL_SYSTEM_CURSOR_HAND); | 1747 | : SDL_SYSTEM_CURSOR_HAND) |
1718 | } | 1748 | : SDL_SYSTEM_CURSOR_ARROW); |
1719 | else { | ||
1720 | setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW); | ||
1721 | } | ||
1722 | } | 1749 | } |
1723 | if (d->contextIndex != iInvalidPos) { | 1750 | if (d->contextIndex != iInvalidPos) { |
1724 | invalidateItem_ListWidget(d->list, d->contextIndex); | 1751 | invalidateItem_ListWidget(d->list, d->contextIndex); |
@@ -1726,7 +1753,14 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
1726 | } | 1753 | } |
1727 | } | 1754 | } |
1728 | /* Update context menu items. */ | 1755 | /* Update context menu items. */ |
1729 | if ((d->menu || d->mode == identities_SidebarMode) && ev->type == SDL_MOUSEBUTTONDOWN) { | 1756 | if (d->menu && ev->type == SDL_MOUSEBUTTONDOWN) { |
1757 | if (isSlidingSheet_SidebarWidget_(d) && | ||
1758 | ev->button.button == SDL_BUTTON_LEFT && | ||
1759 | isVisible_Widget(d) && | ||
1760 | !contains_Widget(w, init_I2(ev->button.x, ev->button.y))) { | ||
1761 | setSlidingSheetPos_SidebarWidget_(d, bottom_SlidingSheetPos); | ||
1762 | return iTrue; | ||
1763 | } | ||
1730 | if (ev->button.button == SDL_BUTTON_RIGHT) { | 1764 | if (ev->button.button == SDL_BUTTON_RIGHT) { |
1731 | d->contextItem = NULL; | 1765 | d->contextItem = NULL; |
1732 | if (!isVisible_Widget(d->menu)) { | 1766 | if (!isVisible_Widget(d->menu)) { |
@@ -1739,7 +1773,6 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
1739 | invalidateItem_ListWidget(d->list, d->contextIndex); | 1773 | invalidateItem_ListWidget(d->list, d->contextIndex); |
1740 | } | 1774 | } |
1741 | d->contextIndex = hoverItemIndex_ListWidget(d->list); | 1775 | d->contextIndex = hoverItemIndex_ListWidget(d->list); |
1742 | updateContextMenu_SidebarWidget_(d); | ||
1743 | /* TODO: Some callback-based mechanism would be nice for updating menus right | 1776 | /* TODO: Some callback-based mechanism would be nice for updating menus right |
1744 | before they open? At least move these to `updateContextMenu_ */ | 1777 | before they open? At least move these to `updateContextMenu_ */ |
1745 | if (d->mode == bookmarks_SidebarMode && d->contextItem) { | 1778 | if (d->mode == bookmarks_SidebarMode && d->contextItem) { |
@@ -1747,17 +1780,17 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
1747 | if (bm) { | 1780 | if (bm) { |
1748 | setMenuItemLabel_Widget(d->menu, | 1781 | setMenuItemLabel_Widget(d->menu, |
1749 | "bookmark.tag tag:homepage", | 1782 | "bookmark.tag tag:homepage", |
1750 | hasTag_Bookmark(bm, homepage_BookmarkTag) | 1783 | bm->flags & homepage_BookmarkFlag |
1751 | ? home_Icon " ${bookmark.untag.home}" | 1784 | ? home_Icon " ${bookmark.untag.home}" |
1752 | : home_Icon " ${bookmark.tag.home}"); | 1785 | : home_Icon " ${bookmark.tag.home}"); |
1753 | setMenuItemLabel_Widget(d->menu, | 1786 | setMenuItemLabel_Widget(d->menu, |
1754 | "bookmark.tag tag:subscribed", | 1787 | "bookmark.tag tag:subscribed", |
1755 | hasTag_Bookmark(bm, subscribed_BookmarkTag) | 1788 | bm->flags & subscribed_BookmarkFlag |
1756 | ? star_Icon " ${bookmark.untag.sub}" | 1789 | ? star_Icon " ${bookmark.untag.sub}" |
1757 | : star_Icon " ${bookmark.tag.sub}"); | 1790 | : star_Icon " ${bookmark.tag.sub}"); |
1758 | setMenuItemLabel_Widget(d->menu, | 1791 | setMenuItemLabel_Widget(d->menu, |
1759 | "bookmark.tag tag:remotesource", | 1792 | "bookmark.tag tag:remotesource", |
1760 | hasTag_Bookmark(bm, remoteSource_BookmarkTag) | 1793 | bm->flags & remoteSource_BookmarkFlag |
1761 | ? downArrowBar_Icon " ${bookmark.untag.remote}" | 1794 | ? downArrowBar_Icon " ${bookmark.untag.remote}" |
1762 | : downArrowBar_Icon " ${bookmark.tag.remote}"); | 1795 | : downArrowBar_Icon " ${bookmark.tag.remote}"); |
1763 | } | 1796 | } |
@@ -1769,29 +1802,9 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
1769 | isRead ? circle_Icon " ${feeds.entry.markunread}" | 1802 | isRead ? circle_Icon " ${feeds.entry.markunread}" |
1770 | : circleWhite_Icon " ${feeds.entry.markread}"); | 1803 | : circleWhite_Icon " ${feeds.entry.markread}"); |
1771 | } | 1804 | } |
1772 | else if (d->mode == identities_SidebarMode) { | ||
1773 | const iGmIdentity *ident = constHoverIdentity_SidebarWidget_(d); | ||
1774 | const iString * docUrl = url_DocumentWidget(document_App()); | ||
1775 | iForEach(ObjectList, i, children_Widget(d->menu)) { | ||
1776 | if (isInstance_Object(i.object, &Class_LabelWidget)) { | ||
1777 | iLabelWidget *menuItem = i.object; | ||
1778 | const char * cmdItem = cstr_String(command_LabelWidget(menuItem)); | ||
1779 | if (equal_Command(cmdItem, "ident.use")) { | ||
1780 | const iBool cmdUse = arg_Command(cmdItem) != 0; | ||
1781 | const iBool cmdClear = argLabel_Command(cmdItem, "clear") != 0; | ||
1782 | setFlags_Widget( | ||
1783 | as_Widget(menuItem), | ||
1784 | disabled_WidgetFlag, | ||
1785 | (cmdClear && !isUsed_GmIdentity(ident)) || | ||
1786 | (!cmdClear && cmdUse && isUsedOn_GmIdentity(ident, docUrl)) || | ||
1787 | (!cmdClear && !cmdUse && !isUsedOn_GmIdentity(ident, docUrl))); | ||
1788 | } | 1805 | } |
1789 | } | 1806 | } |
1790 | } | 1807 | } |
1791 | } | ||
1792 | } | ||
1793 | } | ||
1794 | } | ||
1795 | if (ev->type == SDL_KEYDOWN) { | 1808 | if (ev->type == SDL_KEYDOWN) { |
1796 | const int key = ev->key.keysym.sym; | 1809 | const int key = ev->key.keysym.sym; |
1797 | const int kmods = keyMods_Sym(ev->key.keysym.mod); | 1810 | const int kmods = keyMods_Sym(ev->key.keysym.mod); |
@@ -1801,6 +1814,61 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
1801 | return iTrue; | 1814 | return iTrue; |
1802 | } | 1815 | } |
1803 | } | 1816 | } |
1817 | if (isSlidingSheet_SidebarWidget_(d)) { | ||
1818 | if (ev->type == SDL_MOUSEWHEEL) { | ||
1819 | enum iWidgetTouchMode touchMode = widgetMode_Touch(w); | ||
1820 | if (touchMode == momentum_WidgetTouchMode) { | ||
1821 | /* We don't do momentum. */ | ||
1822 | float swipe = stopWidgetMomentum_Touch(w) / gap_UI; | ||
1823 | // printf("swipe: %f\n", swipe); | ||
1824 | const iRangei midRegion = SlidingSheetMiddleRegion_SidebarWidget_(d); | ||
1825 | const int pos = top_Rect(w->rect); | ||
1826 | if (swipe < 170) { | ||
1827 | gotoNearestSlidingSheetPos_SidebarWidget_(d); | ||
1828 | } | ||
1829 | else if (swipe > 500 && ev->wheel.y > 0) { | ||
1830 | /* Fast swipe down will dismiss. */ | ||
1831 | setSlidingSheetPos_SidebarWidget_(d, bottom_SlidingSheetPos); | ||
1832 | } | ||
1833 | else if (ev->wheel.y < 0) { | ||
1834 | setSlidingSheetPos_SidebarWidget_(d, top_SlidingSheetPos); | ||
1835 | } | ||
1836 | else if (pos < (midRegion.start + midRegion.end) / 2) { | ||
1837 | setSlidingSheetPos_SidebarWidget_(d, middle_SlidingSheetPos); | ||
1838 | } | ||
1839 | else { | ||
1840 | setSlidingSheetPos_SidebarWidget_(d, bottom_SlidingSheetPos); | ||
1841 | } | ||
1842 | } | ||
1843 | else if (touchMode == touch_WidgetTouchMode) { | ||
1844 | /* Move with the finger. */ | ||
1845 | adjustEdges_Rect(&w->rect, ev->wheel.y, 0, 0, 0); | ||
1846 | /* Upon reaching the top, scrolling is switched back to the list. */ | ||
1847 | const iRect rootRect = safeRect_Root(w->root); | ||
1848 | const int top = top_Rect(rootRect); | ||
1849 | if (w->rect.pos.y < top) { | ||
1850 | setScrollMode_ListWidget(d->list, disabledAtTopUpwards_ScrollMode); | ||
1851 | setScrollPos_ListWidget(d->list, top - w->rect.pos.y); | ||
1852 | transferAffinity_Touch(w, as_Widget(d->list)); | ||
1853 | w->rect.pos.y = top; | ||
1854 | w->rect.size.y = height_Rect(rootRect); | ||
1855 | } | ||
1856 | else { | ||
1857 | setScrollMode_ListWidget(d->list, disabled_ScrollMode); | ||
1858 | } | ||
1859 | arrange_Widget(w); | ||
1860 | refresh_Widget(w); | ||
1861 | } | ||
1862 | else { | ||
1863 | return iFalse; | ||
1864 | } | ||
1865 | return iTrue; | ||
1866 | } | ||
1867 | if (ev->type == SDL_USEREVENT && ev->user.code == widgetTouchEnds_UserEventCode) { | ||
1868 | gotoNearestSlidingSheetPos_SidebarWidget_(d); | ||
1869 | return iTrue; | ||
1870 | } | ||
1871 | } | ||
1804 | if (ev->type == SDL_MOUSEBUTTONDOWN && | 1872 | if (ev->type == SDL_MOUSEBUTTONDOWN && |
1805 | contains_Widget(as_Widget(d->list), init_I2(ev->button.x, ev->button.y))) { | 1873 | contains_Widget(as_Widget(d->list), init_I2(ev->button.x, ev->button.y))) { |
1806 | if (hoverItem_ListWidget(d->list) || isVisible_Widget(d->menu)) { | 1874 | if (hoverItem_ListWidget(d->list) || isVisible_Widget(d->menu)) { |
@@ -1811,13 +1879,12 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
1811 | const iSidebarItem *hoverItem = hoverItem_ListWidget(d->list); | 1879 | const iSidebarItem *hoverItem = hoverItem_ListWidget(d->list); |
1812 | iAssert(hoverItem); | 1880 | iAssert(hoverItem); |
1813 | const iBookmark * bm = get_Bookmarks(bookmarks_App(), hoverItem->id); | 1881 | const iBookmark * bm = get_Bookmarks(bookmarks_App(), hoverItem->id); |
1814 | const iBool isRemote = hasTag_Bookmark(bm, remote_BookmarkTag); | 1882 | const iBool isRemote = (bm->flags & remote_BookmarkFlag) != 0; |
1815 | static const char *localOnlyCmds[] = { "bookmark.edit", | 1883 | static const char *localOnlyCmds[] = { "bookmark.edit", |
1816 | "bookmark.delete", | 1884 | "bookmark.delete", |
1817 | "bookmark.tag tag:" subscribed_BookmarkTag, | 1885 | "bookmark.tag tag:subscribed", |
1818 | "bookmark.tag tag:" homepage_BookmarkTag, | 1886 | "bookmark.tag tag:homepage", |
1819 | "bookmark.tag tag:" remoteSource_BookmarkTag, | 1887 | "bookmark.tag tag:remotesource" }; |
1820 | "bookmark.tag tag:" subscribed_BookmarkTag }; | ||
1821 | iForIndices(i, localOnlyCmds) { | 1888 | iForIndices(i, localOnlyCmds) { |
1822 | setFlags_Widget(as_Widget(findMenuItem_Widget(d->menu, localOnlyCmds[i])), | 1889 | setFlags_Widget(as_Widget(findMenuItem_Widget(d->menu, localOnlyCmds[i])), |
1823 | disabled_WidgetFlag, | 1890 | disabled_WidgetFlag, |
@@ -1838,6 +1905,9 @@ static void draw_SidebarWidget_(const iSidebarWidget *d) { | |||
1838 | const iRect bounds = bounds_Widget(w); | 1905 | const iRect bounds = bounds_Widget(w); |
1839 | iPaint p; | 1906 | iPaint p; |
1840 | init_Paint(&p); | 1907 | init_Paint(&p); |
1908 | if (d->mode == documentOutline_SidebarMode) { | ||
1909 | makePaletteGlobal_GmDocument(document_DocumentWidget(document_App())); | ||
1910 | } | ||
1841 | if (!isPortraitPhone_App()) { /* this would erase page contents during transition on the phone */ | 1911 | if (!isPortraitPhone_App()) { /* this would erase page contents during transition on the phone */ |
1842 | if (flags_Widget(w) & visualOffset_WidgetFlag && | 1912 | if (flags_Widget(w) & visualOffset_WidgetFlag && |
1843 | flags_Widget(w) & horizontalOffset_WidgetFlag && isVisible_Widget(w)) { | 1913 | flags_Widget(w) & horizontalOffset_WidgetFlag && isVisible_Widget(w)) { |
@@ -1860,6 +1930,7 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect, | |||
1860 | &Class_SidebarWidget); | 1930 | &Class_SidebarWidget); |
1861 | const iBool isMenuVisible = isVisible_Widget(sidebar->menu); | 1931 | const iBool isMenuVisible = isVisible_Widget(sidebar->menu); |
1862 | const iBool isDragging = constDragItem_ListWidget(list) == d; | 1932 | const iBool isDragging = constDragItem_ListWidget(list) == d; |
1933 | const iBool isEditing = sidebar->isEditing; /* only on mobile */ | ||
1863 | const iBool isPressing = isMouseDown_ListWidget(list) && !isDragging; | 1934 | const iBool isPressing = isMouseDown_ListWidget(list) && !isDragging; |
1864 | const iBool isHover = | 1935 | const iBool isHover = |
1865 | (!isMenuVisible && | 1936 | (!isMenuVisible && |
@@ -2013,6 +2084,16 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect, | |||
2013 | drawRange_Text(font, textPos, fg, range_String(&d->label)); | 2084 | drawRange_Text(font, textPos, fg, range_String(&d->label)); |
2014 | const int metaFont = uiLabel_FontId; | 2085 | const int metaFont = uiLabel_FontId; |
2015 | const int metaIconWidth = 4.5f * gap_UI; | 2086 | const int metaIconWidth = 4.5f * gap_UI; |
2087 | if (isEditing) { | ||
2088 | iRect dragRect = { | ||
2089 | addX_I2(topRight_Rect(itemRect), -itemHeight * 3 / 2), | ||
2090 | init_I2(itemHeight * 3 / 2, itemHeight) | ||
2091 | }; | ||
2092 | fillRect_Paint(p, dragRect, bg); | ||
2093 | drawVLine_Paint(p, topLeft_Rect(dragRect), height_Rect(dragRect), uiSeparator_ColorId); | ||
2094 | drawCentered_Text(uiContent_FontId, dragRect, iTrue, uiAnnotation_ColorId, menu_Icon); | ||
2095 | adjustEdges_Rect(&itemRect, 0, -width_Rect(dragRect), 0, 0); | ||
2096 | } | ||
2016 | const iInt2 metaPos = | 2097 | const iInt2 metaPos = |
2017 | init_I2(right_Rect(itemRect) - | 2098 | init_I2(right_Rect(itemRect) - |
2018 | length_String(&d->meta) * | 2099 | length_String(&d->meta) * |
@@ -2091,40 +2172,6 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect, | |||
2091 | } | 2172 | } |
2092 | iEndCollect(); | 2173 | iEndCollect(); |
2093 | } | 2174 | } |
2094 | else if (sidebar->mode == identities_SidebarMode) { | ||
2095 | const int fg = isHover ? (isPressing ? uiTextPressed_ColorId : uiTextFramelessHover_ColorId) | ||
2096 | : uiTextStrong_ColorId; | ||
2097 | const iBool isUsedOnDomain = (d->indent != 0); | ||
2098 | iString icon; | ||
2099 | initUnicodeN_String(&icon, &d->icon, 1); | ||
2100 | iInt2 cPos = topLeft_Rect(itemRect); | ||
2101 | const int indent = 1.4f * lineHeight_Text(font); | ||
2102 | addv_I2(&cPos, | ||
2103 | init_I2(3 * gap_UI, | ||
2104 | (itemHeight - lineHeight_Text(uiLabel_FontId) * 2 - lineHeight_Text(font)) / | ||
2105 | 2)); | ||
2106 | const int metaFg = isHover ? permanent_ColorId | (isPressing ? uiTextPressed_ColorId | ||
2107 | : uiTextFramelessHover_ColorId) | ||
2108 | : uiTextDim_ColorId; | ||
2109 | if (!d->listItem.isSelected && !isUsedOnDomain) { | ||
2110 | drawOutline_Text(font, cPos, metaFg, none_ColorId, range_String(&icon)); | ||
2111 | } | ||
2112 | drawRange_Text(font, | ||
2113 | cPos, | ||
2114 | d->listItem.isSelected ? iconColor | ||
2115 | : isUsedOnDomain ? altIconColor | ||
2116 | : uiBackgroundSidebar_ColorId, | ||
2117 | range_String(&icon)); | ||
2118 | deinit_String(&icon); | ||
2119 | drawRange_Text(d->listItem.isSelected ? sidebar->itemFonts[1] : font, | ||
2120 | add_I2(cPos, init_I2(indent, 0)), | ||
2121 | fg, | ||
2122 | range_String(&d->label)); | ||
2123 | drawRange_Text(uiLabel_FontId, | ||
2124 | add_I2(cPos, init_I2(indent, lineHeight_Text(font))), | ||
2125 | metaFg, | ||
2126 | range_String(&d->meta)); | ||
2127 | } | ||
2128 | } | 2175 | } |
2129 | 2176 | ||
2130 | iBeginDefineSubclass(SidebarWidget, Widget) | 2177 | iBeginDefineSubclass(SidebarWidget, Widget) |
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 | |||
54 | void setWidth_SidebarWidget (iSidebarWidget *, float widthAsGaps); | 54 | void setWidth_SidebarWidget (iSidebarWidget *, float widthAsGaps); |
55 | iBool setButtonFont_SidebarWidget (iSidebarWidget *, int font); | 55 | iBool setButtonFont_SidebarWidget (iSidebarWidget *, int font); |
56 | void setClosedFolders_SidebarWidget (iSidebarWidget *, const iIntSet *closedFolders); | 56 | void setClosedFolders_SidebarWidget (iSidebarWidget *, const iIntSet *closedFolders); |
57 | void setMidHeight_SidebarWidget (iSidebarWidget *, int midHeight); /* phone layout */ | ||
57 | 58 | ||
58 | enum iSidebarMode mode_SidebarWidget (const iSidebarWidget *); | 59 | enum iSidebarMode mode_SidebarWidget (const iSidebarWidget *); |
59 | enum iFeedsMode feedsMode_SidebarWidget (const iSidebarWidget *); | 60 | enum iFeedsMode feedsMode_SidebarWidget (const iSidebarWidget *); |
diff --git a/src/ui/text.c b/src/ui/text.c index 4a4b3776..7bb418eb 100644 --- a/src/ui/text.c +++ b/src/ui/text.c | |||
@@ -390,14 +390,19 @@ static void deinitCache_Text_(iText *d) { | |||
390 | SDL_DestroyTexture(d->cache); | 390 | SDL_DestroyTexture(d->cache); |
391 | } | 391 | } |
392 | 392 | ||
393 | iRegExp *makeAnsiEscapePattern_Text(void) { | ||
394 | return new_RegExp("[[()][?]?([0-9;AB]*?)([ABCDEFGHJKSTfhilmn])", 0); | ||
395 | } | ||
396 | |||
393 | void init_Text(iText *d, SDL_Renderer *render) { | 397 | void init_Text(iText *d, SDL_Renderer *render) { |
394 | iText *oldActive = activeText_; | 398 | iText *oldActive = activeText_; |
395 | activeText_ = d; | 399 | activeText_ = d; |
396 | init_Array(&d->fonts, sizeof(iFont)); | 400 | init_Array(&d->fonts, sizeof(iFont)); |
397 | d->contentFontSize = contentScale_Text_; | 401 | d->contentFontSize = contentScale_Text_; |
398 | d->ansiEscape = new_RegExp("[[()][?]?([0-9;AB]*?)([ABCDEFGHJKSTfhilmn])", 0); | 402 | d->ansiEscape = makeAnsiEscapePattern_Text(); |
399 | d->baseFontId = -1; | 403 | d->baseFontId = -1; |
400 | d->baseFgColorId = -1; | 404 | d->baseFgColorId = -1; |
405 | d->missingGlyphs = iFalse; | ||
401 | d->render = render; | 406 | d->render = render; |
402 | /* A grayscale palette for rasterized glyphs. */ { | 407 | /* A grayscale palette for rasterized glyphs. */ { |
403 | SDL_Color colors[256]; | 408 | SDL_Color colors[256]; |
@@ -448,6 +453,10 @@ void setAnsiFlags_Text(int ansiFlags) { | |||
448 | activeText_->ansiFlags = ansiFlags; | 453 | activeText_->ansiFlags = ansiFlags; |
449 | } | 454 | } |
450 | 455 | ||
456 | int ansiFlags_Text(void) { | ||
457 | return activeText_->ansiFlags; | ||
458 | } | ||
459 | |||
451 | void setDocumentFontSize_Text(iText *d, float fontSizeFactor) { | 460 | void setDocumentFontSize_Text(iText *d, float fontSizeFactor) { |
452 | fontSizeFactor *= contentScale_Text_; | 461 | fontSizeFactor *= contentScale_Text_; |
453 | iAssert(fontSizeFactor > 0); | 462 | iAssert(fontSizeFactor > 0); |
@@ -642,6 +651,37 @@ static iBool isControl_Char_(iChar c) { | |||
642 | /*----------------------------------------------------------------------------------------------*/ | 651 | /*----------------------------------------------------------------------------------------------*/ |
643 | 652 | ||
644 | iDeclareType(AttributedRun) | 653 | iDeclareType(AttributedRun) |
654 | |||
655 | enum iScript { | ||
656 | unspecified_Script, | ||
657 | arabic_Script, | ||
658 | bengali_Script, | ||
659 | devanagari_Script, | ||
660 | han_Script, | ||
661 | hiragana_Script, | ||
662 | katakana_Script, | ||
663 | oriya_Script, | ||
664 | tamil_Script, | ||
665 | max_Script | ||
666 | }; | ||
667 | |||
668 | iLocalDef iBool isCJK_Script_(enum iScript d) { | ||
669 | return d == han_Script || d == hiragana_Script || d == katakana_Script; | ||
670 | } | ||
671 | |||
672 | #if defined (LAGRANGE_ENABLE_HARFBUZZ) | ||
673 | static const hb_script_t hbScripts_[max_Script] = { | ||
674 | 0, | ||
675 | HB_SCRIPT_ARABIC, | ||
676 | HB_SCRIPT_BENGALI, | ||
677 | HB_SCRIPT_DEVANAGARI, | ||
678 | HB_SCRIPT_HAN, | ||
679 | HB_SCRIPT_HIRAGANA, | ||
680 | HB_SCRIPT_KATAKANA, | ||
681 | HB_SCRIPT_ORIYA, | ||
682 | HB_SCRIPT_TAMIL, | ||
683 | }; | ||
684 | #endif | ||
645 | 685 | ||
646 | struct Impl_AttributedRun { | 686 | struct Impl_AttributedRun { |
647 | iRangei logical; /* UTF-32 codepoint indices in the logical-order text */ | 687 | iRangei logical; /* UTF-32 codepoint indices in the logical-order text */ |
@@ -651,8 +691,7 @@ struct Impl_AttributedRun { | |||
651 | iColor bgColor_; /* any RGB color; A > 0 */ | 691 | iColor bgColor_; /* any RGB color; A > 0 */ |
652 | struct { | 692 | struct { |
653 | uint8_t isLineBreak : 1; | 693 | uint8_t isLineBreak : 1; |
654 | // uint8_t isRTL : 1; | 694 | uint8_t script : 7; /* if script detected */ |
655 | uint8_t isArabic : 1; /* Arabic script detected */ | ||
656 | } flags; | 695 | } flags; |
657 | }; | 696 | }; |
658 | 697 | ||
@@ -744,7 +783,7 @@ static void finishRun_AttributedText_(iAttributedText *d, iAttributedRun *run, i | |||
744 | #endif | 783 | #endif |
745 | pushBack_Array(&d->runs, &finishedRun); | 784 | pushBack_Array(&d->runs, &finishedRun); |
746 | run->flags.isLineBreak = iFalse; | 785 | run->flags.isLineBreak = iFalse; |
747 | run->flags.isArabic = iFalse; | 786 | run->flags.script = unspecified_Script; |
748 | } | 787 | } |
749 | run->logical.start = endAt; | 788 | run->logical.start = endAt; |
750 | } | 789 | } |
@@ -789,6 +828,7 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh | |||
789 | pushBack_Array(&d->logicalToSourceOffset, &(int){ ch - d->source.start }); | 828 | pushBack_Array(&d->logicalToSourceOffset, &(int){ ch - d->source.start }); |
790 | ch += len; | 829 | ch += len; |
791 | } | 830 | } |
831 | iBool bidiOk = iFalse; | ||
792 | #if defined (LAGRANGE_ENABLE_FRIBIDI) | 832 | #if defined (LAGRANGE_ENABLE_FRIBIDI) |
793 | /* Use FriBidi to reorder the codepoints. */ | 833 | /* Use FriBidi to reorder the codepoints. */ |
794 | resize_Array(&d->visual, length); | 834 | resize_Array(&d->visual, length); |
@@ -796,25 +836,25 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh | |||
796 | resize_Array(&d->visualToLogical, length); | 836 | resize_Array(&d->visualToLogical, length); |
797 | d->bidiLevels = length ? malloc(length) : NULL; | 837 | d->bidiLevels = length ? malloc(length) : NULL; |
798 | FriBidiParType baseDir = (FriBidiParType) FRIBIDI_TYPE_ON; | 838 | FriBidiParType baseDir = (FriBidiParType) FRIBIDI_TYPE_ON; |
799 | /* TODO: If this returns zero (error occurred), act like everything is LTR. */ | 839 | bidiOk = fribidi_log2vis(constData_Array(&d->logical), |
800 | fribidi_log2vis(constData_Array(&d->logical), | 840 | (FriBidiStrIndex) length, |
801 | length, | 841 | &baseDir, |
802 | &baseDir, | 842 | data_Array(&d->visual), |
803 | data_Array(&d->visual), | 843 | data_Array(&d->logicalToVisual), |
804 | data_Array(&d->logicalToVisual), | 844 | data_Array(&d->visualToLogical), |
805 | data_Array(&d->visualToLogical), | 845 | (FriBidiLevel *) d->bidiLevels) > 0; |
806 | (FriBidiLevel *) d->bidiLevels); | ||
807 | d->isBaseRTL = (overrideBaseDir == 0 ? FRIBIDI_IS_RTL(baseDir) : (overrideBaseDir < 0)); | 846 | d->isBaseRTL = (overrideBaseDir == 0 ? FRIBIDI_IS_RTL(baseDir) : (overrideBaseDir < 0)); |
808 | #else | ||
809 | /* 1:1 mapping. */ | ||
810 | setCopy_Array(&d->visual, &d->logical); | ||
811 | resize_Array(&d->logicalToVisual, length); | ||
812 | for (size_t i = 0; i < length; i++) { | ||
813 | set_Array(&d->logicalToVisual, i, &(int){ i }); | ||
814 | } | ||
815 | setCopy_Array(&d->visualToLogical, &d->logicalToVisual); | ||
816 | d->isBaseRTL = iFalse; | ||
817 | #endif | 847 | #endif |
848 | if (!bidiOk) { | ||
849 | /* 1:1 mapping. */ | ||
850 | setCopy_Array(&d->visual, &d->logical); | ||
851 | resize_Array(&d->logicalToVisual, length); | ||
852 | for (size_t i = 0; i < length; i++) { | ||
853 | set_Array(&d->logicalToVisual, i, &(int){ i }); | ||
854 | } | ||
855 | setCopy_Array(&d->visualToLogical, &d->logicalToVisual); | ||
856 | d->isBaseRTL = iFalse; | ||
857 | } | ||
818 | } | 858 | } |
819 | /* The mapping needs to include the terminating NULL position. */ { | 859 | /* The mapping needs to include the terminating NULL position. */ { |
820 | pushBack_Array(&d->logicalToSourceOffset, &(int){ d->source.end - d->source.start }); | 860 | pushBack_Array(&d->logicalToSourceOffset, &(int){ d->source.end - d->source.start }); |
@@ -828,7 +868,6 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh | |||
828 | .font = d->font, | 868 | .font = d->font, |
829 | }; | 869 | }; |
830 | const int *logToSource = constData_Array(&d->logicalToSourceOffset); | 870 | const int *logToSource = constData_Array(&d->logicalToSourceOffset); |
831 | const int * logToVis = constData_Array(&d->logicalToVisual); | ||
832 | const iChar * logicalText = constData_Array(&d->logical); | 871 | const iChar * logicalText = constData_Array(&d->logical); |
833 | iBool isRTL = d->isBaseRTL; | 872 | iBool isRTL = d->isBaseRTL; |
834 | int numNonSpace = 0; | 873 | int numNonSpace = 0; |
@@ -868,17 +907,34 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh | |||
868 | /* Note: This styling is hardcoded to match `typesetOneLine_RunTypesetter_()`. */ | 907 | /* Note: This styling is hardcoded to match `typesetOneLine_RunTypesetter_()`. */ |
869 | if (ansi & allowFontStyle_AnsiFlag && equal_Rangecc(sequence, "1")) { | 908 | if (ansi & allowFontStyle_AnsiFlag && equal_Rangecc(sequence, "1")) { |
870 | run.attrib.bold = iTrue; | 909 | run.attrib.bold = iTrue; |
910 | run.attrib.regular = iFalse; | ||
911 | run.attrib.light = iFalse; | ||
871 | if (d->baseFgColorId == tmParagraph_ColorId) { | 912 | if (d->baseFgColorId == tmParagraph_ColorId) { |
872 | setFgColor_AttributedRun_(&run, tmFirstParagraph_ColorId); | 913 | setFgColor_AttributedRun_(&run, tmFirstParagraph_ColorId); |
873 | } | 914 | } |
874 | attribFont = font_Text_(fontWithStyle_Text(fontId_Text_(d->baseFont), | 915 | attribFont = font_Text_(fontWithStyle_Text(fontId_Text_(d->baseFont), |
875 | bold_FontStyle)); | 916 | bold_FontStyle)); |
876 | } | 917 | } |
918 | else if (ansi & allowFontStyle_AnsiFlag && equal_Rangecc(sequence, "2")) { | ||
919 | run.attrib.light = iTrue; | ||
920 | run.attrib.regular = iFalse; | ||
921 | run.attrib.bold = iFalse; | ||
922 | attribFont = font_Text_(fontWithStyle_Text(fontId_Text_(d->baseFont), | ||
923 | light_FontStyle)); | ||
924 | } | ||
877 | else if (ansi & allowFontStyle_AnsiFlag && equal_Rangecc(sequence, "3")) { | 925 | else if (ansi & allowFontStyle_AnsiFlag && equal_Rangecc(sequence, "3")) { |
878 | run.attrib.italic = iTrue; | 926 | run.attrib.italic = iTrue; |
879 | attribFont = font_Text_(fontWithStyle_Text(fontId_Text_(d->baseFont), | 927 | attribFont = font_Text_(fontWithStyle_Text(fontId_Text_(d->baseFont), |
880 | italic_FontStyle)); | 928 | italic_FontStyle)); |
881 | } | 929 | } |
930 | else if (ansi & allowFontStyle_AnsiFlag && equal_Rangecc(sequence, "10")) { | ||
931 | run.attrib.regular = iTrue; | ||
932 | run.attrib.bold = iFalse; | ||
933 | run.attrib.light = iFalse; | ||
934 | run.attrib.italic = iFalse; | ||
935 | attribFont = font_Text_(fontWithStyle_Text(fontId_Text_(d->baseFont), | ||
936 | regular_FontStyle)); | ||
937 | } | ||
882 | else if (ansi & allowFontStyle_AnsiFlag && equal_Rangecc(sequence, "11")) { | 938 | else if (ansi & allowFontStyle_AnsiFlag && equal_Rangecc(sequence, "11")) { |
883 | run.attrib.monospace = iTrue; | 939 | run.attrib.monospace = iTrue; |
884 | setFgColor_AttributedRun_(&run, tmPreformatted_ColorId); | 940 | setFgColor_AttributedRun_(&run, tmPreformatted_ColorId); |
@@ -886,7 +942,9 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh | |||
886 | monospace_FontId)); | 942 | monospace_FontId)); |
887 | } | 943 | } |
888 | else if (equal_Rangecc(sequence, "0")) { | 944 | else if (equal_Rangecc(sequence, "0")) { |
945 | run.attrib.regular = iFalse; | ||
889 | run.attrib.bold = iFalse; | 946 | run.attrib.bold = iFalse; |
947 | run.attrib.light = iFalse; | ||
890 | run.attrib.italic = iFalse; | 948 | run.attrib.italic = iFalse; |
891 | run.attrib.monospace = iFalse; | 949 | run.attrib.monospace = iFalse; |
892 | attribFont = run.font = d->baseFont; | 950 | attribFont = run.font = d->baseFont; |
@@ -957,16 +1015,44 @@ static void prepare_AttributedText_(iAttributedText *d, int overrideBaseDir, iCh | |||
957 | (int)logicalText[pos]); | 1015 | (int)logicalText[pos]); |
958 | #endif | 1016 | #endif |
959 | } | 1017 | } |
1018 | /* Detect the script. */ | ||
960 | #if defined (LAGRANGE_ENABLE_FRIBIDI) | 1019 | #if defined (LAGRANGE_ENABLE_FRIBIDI) |
961 | if (fribidi_get_bidi_type(ch) == FRIBIDI_TYPE_AL) { | 1020 | if (fribidi_get_bidi_type(ch) == FRIBIDI_TYPE_AL) { |
962 | run.flags.isArabic = iTrue; /* Arabic letter */ | 1021 | run.flags.script = arabic_Script; |
963 | } | 1022 | } |
1023 | else | ||
964 | #endif | 1024 | #endif |
1025 | { | ||
1026 | const char *scr = script_Char(ch); | ||
1027 | // printf("Char %08x %lc => %s\n", ch, (int) ch, scr); | ||
1028 | if (!iCmpStr(scr, "Bengali")) { | ||
1029 | run.flags.script = bengali_Script; | ||
1030 | } | ||
1031 | else if (!iCmpStr(scr, "Devanagari")) { | ||
1032 | run.flags.script = devanagari_Script; | ||
1033 | } | ||
1034 | else if (!iCmpStr(scr, "Han")) { | ||
1035 | run.flags.script = han_Script; | ||
1036 | } | ||
1037 | else if (!iCmpStr(scr, "Hiragana")) { | ||
1038 | run.flags.script = hiragana_Script; | ||
1039 | } | ||
1040 | else if (!iCmpStr(scr, "Katakana")) { | ||
1041 | run.flags.script = katakana_Script; | ||
1042 | } | ||
1043 | else if (!iCmpStr(scr, "Oriya")) { | ||
1044 | run.flags.script = oriya_Script; | ||
1045 | } | ||
1046 | else if (!iCmpStr(scr, "Tamil")) { | ||
1047 | run.flags.script = tamil_Script; | ||
1048 | } | ||
1049 | } | ||
965 | } | 1050 | } |
966 | if (!isEmpty_Range(&run.logical)) { | 1051 | if (!isEmpty_Range(&run.logical)) { |
967 | pushBack_Array(&d->runs, &run); | 1052 | pushBack_Array(&d->runs, &run); |
968 | } | 1053 | } |
969 | #if 0 | 1054 | #if 0 |
1055 | const int *logToVis = constData_Array(&d->logicalToVisual); | ||
970 | printf("[AttributedText] %zu runs:\n", size_Array(&d->runs)); | 1056 | printf("[AttributedText] %zu runs:\n", size_Array(&d->runs)); |
971 | iConstForEach(Array, i, &d->runs) { | 1057 | iConstForEach(Array, i, &d->runs) { |
972 | const iAttributedRun *run = i.value; | 1058 | const iAttributedRun *run = i.value; |
@@ -1021,10 +1107,10 @@ struct Impl_RasterGlyph { | |||
1021 | iRect rect; | 1107 | iRect rect; |
1022 | }; | 1108 | }; |
1023 | 1109 | ||
1024 | static void cacheGlyphs_Font_(iFont *d, const iArray *glyphIndices) { | 1110 | static void cacheGlyphs_Font_(iFont *d, const uint32_t *glyphIndices, size_t numGlyphIndices) { |
1025 | /* TODO: Make this an object so it can be used sequentially without reallocating buffers. */ | 1111 | /* TODO: Make this an object so it can be used sequentially without reallocating buffers. */ |
1026 | SDL_Surface *buf = NULL; | 1112 | SDL_Surface *buf = NULL; |
1027 | const iInt2 bufSize = init_I2(iMin(512, d->height * iMin(2 * size_Array(glyphIndices), 20)), | 1113 | const iInt2 bufSize = init_I2(iMin(512, d->height * iMin(2 * numGlyphIndices, 20)), |
1028 | d->height * 4 / 3); | 1114 | d->height * 4 / 3); |
1029 | int bufX = 0; | 1115 | int bufX = 0; |
1030 | iArray * rasters = NULL; | 1116 | iArray * rasters = NULL; |
@@ -1033,9 +1119,9 @@ static void cacheGlyphs_Font_(iFont *d, const iArray *glyphIndices) { | |||
1033 | iAssert(isExposed_Window(get_Window())); | 1119 | iAssert(isExposed_Window(get_Window())); |
1034 | /* We'll flush the buffered rasters periodically until everything is cached. */ | 1120 | /* We'll flush the buffered rasters periodically until everything is cached. */ |
1035 | size_t index = 0; | 1121 | size_t index = 0; |
1036 | while (index < size_Array(glyphIndices)) { | 1122 | while (index < numGlyphIndices) { |
1037 | for (; index < size_Array(glyphIndices); index++) { | 1123 | for (; index < numGlyphIndices; index++) { |
1038 | const uint32_t glyphIndex = constValue_Array(glyphIndices, index, uint32_t); | 1124 | const uint32_t glyphIndex = glyphIndices[index]; |
1039 | const int lastCacheBottom = activeText_->cacheBottom; | 1125 | const int lastCacheBottom = activeText_->cacheBottom; |
1040 | iGlyph *glyph = glyphByIndex_Font_(d, glyphIndex); | 1126 | iGlyph *glyph = glyphByIndex_Font_(d, glyphIndex); |
1041 | if (activeText_->cacheBottom < lastCacheBottom) { | 1127 | if (activeText_->cacheBottom < lastCacheBottom) { |
@@ -1137,12 +1223,8 @@ static void cacheGlyphs_Font_(iFont *d, const iArray *glyphIndices) { | |||
1137 | } | 1223 | } |
1138 | } | 1224 | } |
1139 | 1225 | ||
1140 | static void cacheSingleGlyph_Font_(iFont *d, uint32_t glyphIndex) { | 1226 | iLocalDef void cacheSingleGlyph_Font_(iFont *d, uint32_t glyphIndex) { |
1141 | iArray indices; | 1227 | cacheGlyphs_Font_(d, &glyphIndex, 1); |
1142 | init_Array(&indices, sizeof(uint32_t)); | ||
1143 | pushBack_Array(&indices, &glyphIndex); | ||
1144 | cacheGlyphs_Font_(d, &indices); | ||
1145 | deinit_Array(&indices); | ||
1146 | } | 1228 | } |
1147 | 1229 | ||
1148 | static void cacheTextGlyphs_Font_(iFont *d, const iRangecc text) { | 1230 | static void cacheTextGlyphs_Font_(iFont *d, const iRangecc text) { |
@@ -1171,7 +1253,7 @@ static void cacheTextGlyphs_Font_(iFont *d, const iRangecc text) { | |||
1171 | } | 1253 | } |
1172 | deinit_AttributedText(&attrText); | 1254 | deinit_AttributedText(&attrText); |
1173 | /* TODO: Cache glyphs from ALL the fonts we encountered above. */ | 1255 | /* TODO: Cache glyphs from ALL the fonts we encountered above. */ |
1174 | cacheGlyphs_Font_(d, &glyphIndices); | 1256 | cacheGlyphs_Font_(d, constData_Array(&glyphIndices), size_Array(&glyphIndices)); |
1175 | deinit_Array(&glyphIndices); | 1257 | deinit_Array(&glyphIndices); |
1176 | } | 1258 | } |
1177 | 1259 | ||
@@ -1380,8 +1462,9 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) { | |||
1380 | } | 1462 | } |
1381 | hb_buffer_set_content_type(buf->hb, HB_BUFFER_CONTENT_TYPE_UNICODE); | 1463 | hb_buffer_set_content_type(buf->hb, HB_BUFFER_CONTENT_TYPE_UNICODE); |
1382 | hb_buffer_set_direction(buf->hb, HB_DIRECTION_LTR); /* visual */ | 1464 | hb_buffer_set_direction(buf->hb, HB_DIRECTION_LTR); /* visual */ |
1383 | if (run->flags.isArabic) { | 1465 | const hb_script_t script = hbScripts_[run->flags.script]; |
1384 | hb_buffer_set_script(buf->hb, HB_SCRIPT_ARABIC); | 1466 | if (script) { |
1467 | hb_buffer_set_script(buf->hb, script); | ||
1385 | } | 1468 | } |
1386 | } | 1469 | } |
1387 | if (isMonospaced) { | 1470 | if (isMonospaced) { |
@@ -1405,6 +1488,7 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) { | |||
1405 | iBool isFirst = iTrue; | 1488 | iBool isFirst = iTrue; |
1406 | const iBool checkHitPoint = wrap && !isEqual_I2(wrap->hitPoint, zero_I2()); | 1489 | const iBool checkHitPoint = wrap && !isEqual_I2(wrap->hitPoint, zero_I2()); |
1407 | const iBool checkHitChar = wrap && wrap->hitChar; | 1490 | const iBool checkHitChar = wrap && wrap->hitChar; |
1491 | size_t numWrapLines = 0; | ||
1408 | while (!isEmpty_Range(&wrapRuns)) { | 1492 | while (!isEmpty_Range(&wrapRuns)) { |
1409 | if (isFirst) { | 1493 | if (isFirst) { |
1410 | isFirst = iFalse; | 1494 | isFirst = iFalse; |
@@ -1469,8 +1553,11 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) { | |||
1469 | const float xOffset = run->font->xScale * buf->glyphPos[i].x_offset; | 1553 | const float xOffset = run->font->xScale * buf->glyphPos[i].x_offset; |
1470 | const float xAdvance = run->font->xScale * buf->glyphPos[i].x_advance; | 1554 | const float xAdvance = run->font->xScale * buf->glyphPos[i].x_advance; |
1471 | const iChar ch = logicalText[logPos]; | 1555 | const iChar ch = logicalText[logPos]; |
1556 | const enum iWrapTextMode wrapMode = isCJK_Script_(run->flags.script) | ||
1557 | ? anyCharacter_WrapTextMode | ||
1558 | : args->wrap->mode; | ||
1472 | iAssert(xAdvance >= 0); | 1559 | iAssert(xAdvance >= 0); |
1473 | if (args->wrap->mode == word_WrapTextMode) { | 1560 | if (wrapMode == word_WrapTextMode) { |
1474 | /* When word wrapping, only consider certain places breakable. */ | 1561 | /* When word wrapping, only consider certain places breakable. */ |
1475 | if ((prevCh == '-' || prevCh == '/') && !isPunct_Char(ch)) { | 1562 | if ((prevCh == '-' || prevCh == '/') && !isPunct_Char(ch)) { |
1476 | safeBreakPos = logPos; | 1563 | safeBreakPos = logPos; |
@@ -1515,7 +1602,7 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) { | |||
1515 | wrapPosRange.end = safeBreakPos; | 1602 | wrapPosRange.end = safeBreakPos; |
1516 | } | 1603 | } |
1517 | else { | 1604 | else { |
1518 | if (args->wrap->mode == word_WrapTextMode && run->logical.start > wrapPosRange.start) { | 1605 | if (wrapMode == word_WrapTextMode && run->logical.start > wrapPosRange.start) { |
1519 | /* Don't have a word break position, so the whole run needs | 1606 | /* Don't have a word break position, so the whole run needs |
1520 | to be cut. */ | 1607 | to be cut. */ |
1521 | wrapPosRange.end = run->logical.start; | 1608 | wrapPosRange.end = run->logical.start; |
@@ -1529,7 +1616,7 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) { | |||
1529 | breakRunIndex = runIndex; | 1616 | breakRunIndex = runIndex; |
1530 | } | 1617 | } |
1531 | wrapResumePos = wrapPosRange.end; | 1618 | wrapResumePos = wrapPosRange.end; |
1532 | if (args->wrap->mode != anyCharacter_WrapTextMode) { | 1619 | if (wrapMode != anyCharacter_WrapTextMode) { |
1533 | while (wrapResumePos < textLen && isSpace_Char(logicalText[wrapResumePos])) { | 1620 | while (wrapResumePos < textLen && isSpace_Char(logicalText[wrapResumePos])) { |
1534 | wrapResumePos++; /* skip space */ | 1621 | wrapResumePos++; /* skip space */ |
1535 | } | 1622 | } |
@@ -1634,6 +1721,10 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) { | |||
1634 | iRound(wrapAdvance))) { | 1721 | iRound(wrapAdvance))) { |
1635 | willAbortDueToWrap = iTrue; | 1722 | willAbortDueToWrap = iTrue; |
1636 | } | 1723 | } |
1724 | numWrapLines++; | ||
1725 | if (wrap && wrap->maxLines && numWrapLines == wrap->maxLines) { | ||
1726 | willAbortDueToWrap = iTrue; | ||
1727 | } | ||
1637 | wrapAttrib = lastAttrib; | 1728 | wrapAttrib = lastAttrib; |
1638 | xCursor = origin; | 1729 | xCursor = origin; |
1639 | /* We have determined a possible wrap position and alignment for the work runs, | 1730 | /* We have determined a possible wrap position and alignment for the work runs, |
@@ -1664,12 +1755,13 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) { | |||
1664 | /* Already handled this part of the run. */ | 1755 | /* Already handled this part of the run. */ |
1665 | continue; | 1756 | continue; |
1666 | } | 1757 | } |
1667 | const float xOffset = run->font->xScale * buf->glyphPos[i].x_offset; | 1758 | const float xOffset = run->font->xScale * buf->glyphPos[i].x_offset; |
1668 | const float yOffset = run->font->yScale * buf->glyphPos[i].y_offset; | 1759 | float yOffset = run->font->yScale * buf->glyphPos[i].y_offset; |
1669 | const float xAdvance = run->font->xScale * buf->glyphPos[i].x_advance; | 1760 | const float xAdvance = run->font->xScale * buf->glyphPos[i].x_advance; |
1670 | const float yAdvance = run->font->yScale * buf->glyphPos[i].y_advance; | 1761 | const float yAdvance = run->font->yScale * buf->glyphPos[i].y_advance; |
1671 | const iGlyph *glyph = glyphByIndex_Font_(run->font, glyphId); | 1762 | const iGlyph *glyph = glyphByIndex_Font_(run->font, glyphId); |
1672 | if (logicalText[logPos] == '\t') { | 1763 | const iChar ch = logicalText[logPos]; |
1764 | if (ch == '\t') { | ||
1673 | #if 0 | 1765 | #if 0 |
1674 | if (mode & draw_RunMode) { | 1766 | if (mode & draw_RunMode) { |
1675 | /* Tab indicator. */ | 1767 | /* Tab indicator. */ |
@@ -1688,6 +1780,13 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) { | |||
1688 | } | 1780 | } |
1689 | const float xf = xCursor + xOffset; | 1781 | const float xf = xCursor + xOffset; |
1690 | const int hoff = enableHalfPixelGlyphs_Text ? (xf - ((int) xf) > 0.5f ? 1 : 0) : 0; | 1782 | const int hoff = enableHalfPixelGlyphs_Text ? (xf - ((int) xf) > 0.5f ? 1 : 0) : 0; |
1783 | if (ch == 0x3001 || ch == 0x3002) { | ||
1784 | /* Vertical misalignment?? */ | ||
1785 | if (yOffset == 0.0f) { | ||
1786 | /* Move down to baseline. Why doesn't HarfBuzz do this? */ | ||
1787 | yOffset = glyph->d[hoff].y + glyph->rect[hoff].size.y + glyph->d[hoff].y / 4; | ||
1788 | } | ||
1789 | } | ||
1691 | /* Output position for the glyph. */ | 1790 | /* Output position for the glyph. */ |
1692 | SDL_Rect dst = { orig.x + xCursor + xOffset + glyph->d[hoff].x, | 1791 | SDL_Rect dst = { orig.x + xCursor + xOffset + glyph->d[hoff].x, |
1693 | orig.y + yCursor - yOffset + glyph->font->baseline + glyph->d[hoff].y, | 1792 | orig.y + yCursor - yOffset + glyph->font->baseline + glyph->d[hoff].y, |
@@ -2231,6 +2330,7 @@ void init_TextBuf(iTextBuf *d, iWrapText *wrapText, int font, int color) { | |||
2231 | SDL_Texture *oldTarget = SDL_GetRenderTarget(render); | 2330 | SDL_Texture *oldTarget = SDL_GetRenderTarget(render); |
2232 | const iInt2 oldOrigin = origin_Paint; | 2331 | const iInt2 oldOrigin = origin_Paint; |
2233 | origin_Paint = zero_I2(); | 2332 | origin_Paint = zero_I2(); |
2333 | setBaseAttributes_Text(font, color); | ||
2234 | SDL_SetRenderTarget(render, d->texture); | 2334 | SDL_SetRenderTarget(render, d->texture); |
2235 | SDL_SetRenderDrawBlendMode(render, SDL_BLENDMODE_NONE); | 2335 | SDL_SetRenderDrawBlendMode(render, SDL_BLENDMODE_NONE); |
2236 | SDL_SetRenderDrawColor(render, 255, 255, 255, 0); | 2336 | SDL_SetRenderDrawColor(render, 255, 255, 255, 0); |
@@ -2241,6 +2341,7 @@ void init_TextBuf(iTextBuf *d, iWrapText *wrapText, int font, int color) { | |||
2241 | SDL_SetRenderTarget(render, oldTarget); | 2341 | SDL_SetRenderTarget(render, oldTarget); |
2242 | origin_Paint = oldOrigin; | 2342 | origin_Paint = oldOrigin; |
2243 | SDL_SetTextureBlendMode(d->texture, SDL_BLENDMODE_BLEND); | 2343 | SDL_SetTextureBlendMode(d->texture, SDL_BLENDMODE_BLEND); |
2344 | setBaseAttributes_Text(-1, -1); | ||
2244 | } | 2345 | } |
2245 | } | 2346 | } |
2246 | 2347 | ||
diff --git a/src/ui/text.h b/src/ui/text.h index 63499484..c8bb6f85 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. */ | |||
29 | 29 | ||
30 | #include "fontpack.h" | 30 | #include "fontpack.h" |
31 | 31 | ||
32 | iDeclareType(RegExp) | ||
33 | |||
32 | /* Content sizes: regular (1x) -> medium (1.2x) -> big (1.33x) -> large (1.67x) -> huge (2x) */ | 34 | /* Content sizes: regular (1x) -> medium (1.2x) -> big (1.33x) -> large (1.67x) -> huge (2x) */ |
33 | 35 | ||
34 | #define FONT_ID(name, style, size) ((name) + ((style) * max_FontSize) + (size)) | 36 | #define FONT_ID(name, style, size) ((name) + ((style) * max_FontSize) + (size)) |
@@ -159,6 +161,7 @@ enum iAnsiFlag { | |||
159 | void setOpacity_Text (float opacity); | 161 | void setOpacity_Text (float opacity); |
160 | void setBaseAttributes_Text (int fontId, int fgColorId); /* current "normal" text attributes */ | 162 | void setBaseAttributes_Text (int fontId, int fgColorId); /* current "normal" text attributes */ |
161 | void setAnsiFlags_Text (int ansiFlags); | 163 | void setAnsiFlags_Text (int ansiFlags); |
164 | int ansiFlags_Text (void); | ||
162 | 165 | ||
163 | void cache_Text (int fontId, iRangecc text); /* pre-render glyphs */ | 166 | void cache_Text (int fontId, iRangecc text); /* pre-render glyphs */ |
164 | 167 | ||
@@ -190,7 +193,9 @@ struct Impl_TextAttrib { | |||
190 | int16_t fgColorId; | 193 | int16_t fgColorId; |
191 | int16_t bgColorId; | 194 | int16_t bgColorId; |
192 | struct { | 195 | struct { |
196 | uint16_t regular : 1; | ||
193 | uint16_t bold : 1; | 197 | uint16_t bold : 1; |
198 | uint16_t light : 1; | ||
194 | uint16_t italic : 1; | 199 | uint16_t italic : 1; |
195 | uint16_t monospace : 1; | 200 | uint16_t monospace : 1; |
196 | uint16_t isBaseRTL : 1; | 201 | uint16_t isBaseRTL : 1; |
@@ -202,6 +207,7 @@ struct Impl_WrapText { | |||
202 | /* arguments */ | 207 | /* arguments */ |
203 | iRangecc text; | 208 | iRangecc text; |
204 | int maxWidth; | 209 | int maxWidth; |
210 | size_t maxLines; /* 0: unlimited */ | ||
205 | enum iWrapTextMode mode; | 211 | enum iWrapTextMode mode; |
206 | iBool (*wrapFunc)(iWrapText *, iRangecc wrappedText, iTextAttrib attrib, int origin, | 212 | iBool (*wrapFunc)(iWrapText *, iRangecc wrappedText, iTextAttrib attrib, int origin, |
207 | int advance); | 213 | int advance); |
@@ -229,6 +235,8 @@ enum iTextBlockMode { quadrants_TextBlockMode, shading_TextBlockMode }; | |||
229 | iString * renderBlockChars_Text (const iBlock *fontData, int height, enum iTextBlockMode, | 235 | iString * renderBlockChars_Text (const iBlock *fontData, int height, enum iTextBlockMode, |
230 | const iString *text); | 236 | const iString *text); |
231 | 237 | ||
238 | iRegExp * makeAnsiEscapePattern_Text (void); | ||
239 | |||
232 | /*-----------------------------------------------------------------------------------------------*/ | 240 | /*-----------------------------------------------------------------------------------------------*/ |
233 | 241 | ||
234 | iDeclareType(TextBuf) | 242 | iDeclareType(TextBuf) |
@@ -239,6 +247,5 @@ struct Impl_TextBuf { | |||
239 | iInt2 size; | 247 | iInt2 size; |
240 | }; | 248 | }; |
241 | 249 | ||
242 | iTextBuf * newRange_TextBuf (int font, int color, iRangecc text); | 250 | iTextBuf * newRange_TextBuf(int font, int color, iRangecc text); |
243 | |||
244 | void draw_TextBuf (const iTextBuf *, iInt2 pos, int color); | 251 | void draw_TextBuf (const iTextBuf *, iInt2 pos, int color); |
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) { | |||
118 | if (match_RegExp(activeText_->ansiEscape, chPos, args->text.end - chPos, &m)) { | 118 | if (match_RegExp(activeText_->ansiEscape, chPos, args->text.end - chPos, &m)) { |
119 | if (mode & draw_RunMode && ~mode & permanentColorFlag_RunMode) { | 119 | if (mode & draw_RunMode && ~mode & permanentColorFlag_RunMode) { |
120 | /* Change the color. */ | 120 | /* Change the color. */ |
121 | iColor clr; | 121 | iColor clr = get_Color(args->color); |
122 | ansiColors_Color(capturedRange_RegExpMatch(&m, 1), tmParagraph_ColorId, | 122 | ansiColors_Color(capturedRange_RegExpMatch(&m, 1), |
123 | activeText_->baseFgColorId, | ||
123 | none_ColorId, &clr, NULL); | 124 | none_ColorId, &clr, NULL); |
124 | SDL_SetTextureColorMod(activeText_->cache, clr.r, clr.g, clr.b); | 125 | SDL_SetTextureColorMod(activeText_->cache, clr.r, clr.g, clr.b); |
125 | if (args->mode & fillBackground_RunMode) { | 126 | if (args->mode & fillBackground_RunMode) { |
diff --git a/src/ui/touch.c b/src/ui/touch.c index d6846572..20ccf7b8 100644 --- a/src/ui/touch.c +++ b/src/ui/touch.c | |||
@@ -111,6 +111,7 @@ struct Impl_TouchState { | |||
111 | double momFrictionPerStep; | 111 | double momFrictionPerStep; |
112 | double lastMomTime; | 112 | double lastMomTime; |
113 | iInt2 currentTouchPos; /* for emulating SDL_GetMouseState() */ | 113 | iInt2 currentTouchPos; /* for emulating SDL_GetMouseState() */ |
114 | iInt2 latestLongPressStartPos; | ||
114 | }; | 115 | }; |
115 | 116 | ||
116 | static iTouchState *touchState_(void) { | 117 | static iTouchState *touchState_(void) { |
@@ -260,6 +261,11 @@ static iFloat3 gestureVector_Touch_(const iTouch *d) { | |||
260 | return sub_F3(d->pos[0], d->pos[lastIndex]); | 261 | return sub_F3(d->pos[0], d->pos[lastIndex]); |
261 | } | 262 | } |
262 | 263 | ||
264 | static uint32_t gestureSpan_Touch_(const iTouch *d) { | ||
265 | const size_t lastIndex = iMin(d->posCount - 1, lastIndex_Touch_); | ||
266 | return d->posTime[0] - d->posTime[lastIndex]; | ||
267 | } | ||
268 | |||
263 | static void update_TouchState_(void *ptr) { | 269 | static void update_TouchState_(void *ptr) { |
264 | iWindow *win = get_Window(); | 270 | iWindow *win = get_Window(); |
265 | const iWidget *oldHover = win->hover; | 271 | const iWidget *oldHover = win->hover; |
@@ -308,6 +314,7 @@ static void update_TouchState_(void *ptr) { | |||
308 | } | 314 | } |
309 | if (!touch->isTapAndHold && nowTime - touch->startTime >= longPressSpanMs_ && | 315 | if (!touch->isTapAndHold && nowTime - touch->startTime >= longPressSpanMs_ && |
310 | touch->affinity) { | 316 | touch->affinity) { |
317 | touchState_()->latestLongPressStartPos = initF3_I2(touch->pos[0]); | ||
311 | dispatchClick_Touch_(touch, SDL_BUTTON_RIGHT); | 318 | dispatchClick_Touch_(touch, SDL_BUTTON_RIGHT); |
312 | touch->isTapAndHold = iTrue; | 319 | touch->isTapAndHold = iTrue; |
313 | touch->hasMoved = iFalse; | 320 | touch->hasMoved = iFalse; |
@@ -593,9 +600,18 @@ iBool processEvent_Touch(const SDL_Event *ev) { | |||
593 | divvf_F3(&touch->accum, 6); | 600 | divvf_F3(&touch->accum, 6); |
594 | divfv_I2(&pixels, 6); | 601 | divfv_I2(&pixels, 6); |
595 | /* Allow scrolling a scrollable widget. */ | 602 | /* Allow scrolling a scrollable widget. */ |
596 | iWidget *flow = findOverflowScrollable_Widget(touch->affinity); | 603 | if (touch->affinity && touch->affinity->flags2 & slidingSheetDraggable_WidgetFlag2) { |
597 | if (flow) { | 604 | extern iWidgetClass Class_SidebarWidget; /* The only type of sliding sheet for now. */ |
598 | touch->affinity = flow; | 605 | iWidget *slider = findParentClass_Widget(touch->affinity, &Class_SidebarWidget); |
606 | if (slider) { | ||
607 | touch->affinity = slider; | ||
608 | } | ||
609 | } | ||
610 | else { | ||
611 | iWidget *flow = findOverflowScrollable_Widget(touch->affinity); | ||
612 | if (flow) { | ||
613 | touch->affinity = flow; | ||
614 | } | ||
599 | } | 615 | } |
600 | } | 616 | } |
601 | else { | 617 | else { |
@@ -621,11 +637,13 @@ iBool processEvent_Touch(const SDL_Event *ev) { | |||
621 | if (touch->axis == y_TouchAxis) { | 637 | if (touch->axis == y_TouchAxis) { |
622 | pixels.x = 0; | 638 | pixels.x = 0; |
623 | } | 639 | } |
624 | // printf("%p (%s) py: %i wy: %f acc: %f edge: %d\n", | 640 | #if 0 |
625 | // touch->affinity, | 641 | printf("%p (%s) py: %i wy: %f acc: %f edge: %d\n", |
626 | // class_Widget(touch->affinity)->name, | 642 | touch->affinity, |
627 | // pixels.y, y_F3(amount), y_F3(touch->accum), | 643 | class_Widget(touch->affinity)->name, |
628 | // touch->edge); | 644 | pixels.y, y_F3(amount), y_F3(touch->accum), |
645 | touch->edge); | ||
646 | #endif | ||
629 | if (pixels.x || pixels.y) { | 647 | if (pixels.x || pixels.y) { |
630 | //setFocus_Widget(NULL); | 648 | //setFocus_Widget(NULL); |
631 | dispatchMotion_Touch_(touch->startPos /*pos[0]*/, 0); | 649 | dispatchMotion_Touch_(touch->startPos /*pos[0]*/, 0); |
@@ -661,11 +679,14 @@ iBool processEvent_Touch(const SDL_Event *ev) { | |||
661 | #endif | 679 | #endif |
662 | if (touch->edge && !isStationary_Touch_(touch)) { | 680 | if (touch->edge && !isStationary_Touch_(touch)) { |
663 | const iFloat3 gesture = gestureVector_Touch_(touch); | 681 | const iFloat3 gesture = gestureVector_Touch_(touch); |
682 | const uint32_t duration = gestureSpan_Touch_(touch); | ||
664 | const float pixel = window->pixelRatio; | 683 | const float pixel = window->pixelRatio; |
665 | const int moveDir = x_F3(gesture) < -pixel ? -1 : x_F3(gesture) > pixel ? +1 : 0; | 684 | const int moveDir = x_F3(gesture) < -pixel ? -1 : x_F3(gesture) > pixel ? +1 : 0; |
666 | const int didAbort = (touch->edge == left_TouchEdge && moveDir < 0) || | 685 | const int didAbort = (touch->edge == left_TouchEdge && moveDir < 0) || |
667 | (touch->edge == right_TouchEdge && moveDir > 0); | 686 | (touch->edge == right_TouchEdge && moveDir > 0); |
668 | postCommandf_App("edgeswipe.ended abort:%d side:%d id:%llu", didAbort, touch->edge, touch->id); | 687 | postCommandf_App("edgeswipe.ended abort:%d side:%d id:%llu speed:%d", didAbort, |
688 | touch->edge, touch->id, | ||
689 | (int) (duration > 0 ? length_F3(gesture) / (duration / 1000.0f) : 0)); | ||
669 | remove_ArrayIterator(&i); | 690 | remove_ArrayIterator(&i); |
670 | continue; | 691 | continue; |
671 | } | 692 | } |
@@ -689,8 +710,8 @@ iBool processEvent_Touch(const SDL_Event *ev) { | |||
689 | } | 710 | } |
690 | /* Edge swipes do not generate momentum. */ | 711 | /* Edge swipes do not generate momentum. */ |
691 | const size_t lastIndex = iMin(touch->posCount - 1, lastIndex_Touch_); | 712 | const size_t lastIndex = iMin(touch->posCount - 1, lastIndex_Touch_); |
692 | const uint32_t duration = nowTime - touch->startTime; | ||
693 | const iFloat3 gestureVector = sub_F3(pos, touch->pos[lastIndex]); | 713 | const iFloat3 gestureVector = sub_F3(pos, touch->pos[lastIndex]); |
714 | const uint32_t duration = nowTime - touch->startTime; | ||
694 | iFloat3 velocity = zero_F3(); | 715 | iFloat3 velocity = zero_F3(); |
695 | #if 0 | 716 | #if 0 |
696 | if (touch->edge && fabsf(2 * x_F3(gestureVector)) > fabsf(y_F3(gestureVector)) && | 717 | if (touch->edge && fabsf(2 * x_F3(gestureVector)) > fabsf(y_F3(gestureVector)) && |
@@ -805,10 +826,24 @@ void widgetDestroyed_Touch(iWidget *widget) { | |||
805 | } | 826 | } |
806 | } | 827 | } |
807 | 828 | ||
829 | void transferAffinity_Touch(iWidget *src, iWidget *dst) { | ||
830 | iTouchState *d = touchState_(); | ||
831 | iForEach(Array, i, d->touches) { | ||
832 | iTouch *touch = i.value; | ||
833 | if (touch->affinity == src) { | ||
834 | touch->affinity = dst; | ||
835 | } | ||
836 | } | ||
837 | } | ||
838 | |||
808 | iInt2 latestPosition_Touch(void) { | 839 | iInt2 latestPosition_Touch(void) { |
809 | return touchState_()->currentTouchPos; | 840 | return touchState_()->currentTouchPos; |
810 | } | 841 | } |
811 | 842 | ||
843 | iInt2 latestTapPosition_Touch(void) { | ||
844 | return touchState_()->latestLongPressStartPos; | ||
845 | } | ||
846 | |||
812 | iBool isHovering_Touch(void) { | 847 | iBool isHovering_Touch(void) { |
813 | iTouchState *d = touchState_(); | 848 | iTouchState *d = touchState_(); |
814 | if (numFingers_Touch() == 1) { | 849 | if (numFingers_Touch() == 1) { |
diff --git a/src/ui/touch.h b/src/ui/touch.h index e048224a..15c6da1f 100644 --- a/src/ui/touch.h +++ b/src/ui/touch.h | |||
@@ -36,10 +36,12 @@ enum iWidgetTouchMode { | |||
36 | iBool processEvent_Touch (const SDL_Event *); | 36 | iBool processEvent_Touch (const SDL_Event *); |
37 | void update_Touch (void); | 37 | void update_Touch (void); |
38 | 38 | ||
39 | float stopWidgetMomentum_Touch (const iWidget *widget); | 39 | float stopWidgetMomentum_Touch (const iWidget *widget); /* pixels per second */ |
40 | enum iWidgetTouchMode widgetMode_Touch (const iWidget *widget); | 40 | enum iWidgetTouchMode widgetMode_Touch (const iWidget *widget); |
41 | void widgetDestroyed_Touch (iWidget *widget); | 41 | void widgetDestroyed_Touch (iWidget *widget); |
42 | void transferAffinity_Touch (iWidget *src, iWidget *dst); | ||
42 | 43 | ||
43 | iInt2 latestPosition_Touch (void); /* valid during processing of current event */ | 44 | iInt2 latestPosition_Touch (void); /* valid during processing of current event */ |
45 | iInt2 latestTapPosition_Touch (void); | ||
44 | iBool isHovering_Touch (void); /* stationary touch or a long-press drag ongoing */ | 46 | iBool isHovering_Touch (void); /* stationary touch or a long-press drag ongoing */ |
45 | size_t numFingers_Touch (void); | 47 | size_t numFingers_Touch (void); |
diff --git a/src/ui/uploadwidget.c b/src/ui/uploadwidget.c index bad00071..ed448768 100644 --- a/src/ui/uploadwidget.c +++ b/src/ui/uploadwidget.c | |||
@@ -56,8 +56,10 @@ struct Impl_UploadWidget { | |||
56 | iDocumentWidget *viewer; | 56 | iDocumentWidget *viewer; |
57 | iGmRequest * request; | 57 | iGmRequest * request; |
58 | iLabelWidget * info; | 58 | iLabelWidget * info; |
59 | iInputWidget * path; | ||
59 | iInputWidget * mime; | 60 | iInputWidget * mime; |
60 | iInputWidget * token; | 61 | iInputWidget * token; |
62 | iLabelWidget * ident; | ||
61 | iInputWidget * input; | 63 | iInputWidget * input; |
62 | iLabelWidget * filePathLabel; | 64 | iLabelWidget * filePathLabel; |
63 | iLabelWidget * fileSizeLabel; | 65 | iLabelWidget * fileSizeLabel; |
@@ -91,17 +93,19 @@ static void updateProgress_UploadWidget_(iGmRequest *request, size_t current, si | |||
91 | static void updateInputMaxHeight_UploadWidget_(iUploadWidget *d) { | 93 | static void updateInputMaxHeight_UploadWidget_(iUploadWidget *d) { |
92 | iWidget *w = as_Widget(d); | 94 | iWidget *w = as_Widget(d); |
93 | /* Calculate how many lines fits vertically in the view. */ | 95 | /* Calculate how many lines fits vertically in the view. */ |
94 | const iInt2 inputPos = topLeft_Rect(bounds_Widget(as_Widget(d->input))); | 96 | const iInt2 inputPos = topLeft_Rect(bounds_Widget(as_Widget(d->input))); |
95 | const int footerHeight = isUsingPanelLayout_Mobile() ? 0 : | 97 | int footerHeight = 0; |
96 | (height_Widget(d->token) + | 98 | if (!isUsingPanelLayout_Mobile()) { |
97 | height_Widget(findChild_Widget(w, "dialogbuttons")) + | 99 | footerHeight = (height_Widget(d->token) + |
98 | 12 * gap_UI); | 100 | height_Widget(findChild_Widget(w, "dialogbuttons")) + |
99 | const int avail = bottom_Rect(safeRect_Root(w->root)) - footerHeight - | 101 | 12 * gap_UI); |
100 | get_MainWindow()->keyboardHeight; | 102 | } |
101 | setLineLimits_InputWidget(d->input, | 103 | const int avail = bottom_Rect(visibleRect_Root(w->root)) - footerHeight - inputPos.y; |
102 | minLines_InputWidget(d->input), | 104 | /* On desktop, retain the previously set minLines value. */ |
103 | iMaxi(minLines_InputWidget(d->input), | 105 | int minLines = isUsingPanelLayout_Mobile() ? 1 : minLines_InputWidget(d->input); |
104 | (avail - inputPos.y) / lineHeight_Text(font_InputWidget(d->input)))); | 106 | int maxLines = iMaxi(minLines, avail / lineHeight_Text(font_InputWidget(d->input))); |
107 | /* On mobile, the height is fixed to the available space. */ | ||
108 | setLineLimits_InputWidget(d->input, isUsingPanelLayout_Mobile() ? maxLines : minLines, maxLines); | ||
105 | } | 109 | } |
106 | 110 | ||
107 | static const iGmIdentity *titanIdentityForUrl_(const iString *url) { | 111 | static const iGmIdentity *titanIdentityForUrl_(const iString *url) { |
@@ -123,9 +127,15 @@ static const iArray *makeIdentityItems_UploadWidget_(const iUploadWidget *d) { | |||
123 | pushBack_Array(items, &(iMenuItem){ "---" }); | 127 | pushBack_Array(items, &(iMenuItem){ "---" }); |
124 | iConstForEach(PtrArray, i, listIdentities_GmCerts(certs_App(), NULL, NULL)) { | 128 | iConstForEach(PtrArray, i, listIdentities_GmCerts(certs_App(), NULL, NULL)) { |
125 | const iGmIdentity *id = i.ptr; | 129 | const iGmIdentity *id = i.ptr; |
130 | iString *str = collect_String(copy_String(name_GmIdentity(id))); | ||
131 | prependCStr_String(str, "\x1b[1m"); | ||
132 | if (!isEmpty_String(&id->notes)) { | ||
133 | appendFormat_String( | ||
134 | str, "\x1b[0m\n%s%s", escape_Color(uiTextDim_ColorId), cstr_String(&id->notes)); | ||
135 | } | ||
126 | pushBack_Array( | 136 | pushBack_Array( |
127 | items, | 137 | items, |
128 | &(iMenuItem){ cstr_String(name_GmIdentity(id)), 0, 0, | 138 | &(iMenuItem){ cstr_String(str), 0, 0, |
129 | format_CStr("upload.setid fp:%s", | 139 | format_CStr("upload.setid fp:%s", |
130 | cstrCollect_String(hexEncode_Block(&id->fingerprint))) }); | 140 | cstrCollect_String(hexEncode_Block(&id->fingerprint))) }); |
131 | } | 141 | } |
@@ -182,15 +192,20 @@ void init_UploadWidget(iUploadWidget *d) { | |||
182 | }; | 192 | }; |
183 | initPanels_Mobile(w, NULL, (iMenuItem[]){ | 193 | initPanels_Mobile(w, NULL, (iMenuItem[]){ |
184 | { "title id:heading.upload" }, | 194 | { "title id:heading.upload" }, |
185 | { "label id:upload.info" }, | 195 | { "heading id:upload.url" }, |
196 | { format_CStr("label id:upload.info font:%d", | ||
197 | deviceType_App() == phone_AppDeviceType ? uiLabelBig_FontId : uiLabelMedium_FontId) }, | ||
198 | { "input id:upload.path hint:hint.upload.path noheading:1 url:1 text:" }, | ||
199 | { "heading text:${heading.upload.id}" }, | ||
200 | { "dropdown id:upload.id icon:0x1f464 text:", 0, 0, constData_Array(makeIdentityItems_UploadWidget_(d)) }, | ||
201 | { "input id:upload.token hint:hint.upload.token.long icon:0x1f516 text:" }, | ||
202 | { "heading id:upload.content" }, | ||
186 | { "panel id:dlg.upload.text icon:0x1f5b9 noscroll:1", 0, 0, (const void *) textItems }, | 203 | { "panel id:dlg.upload.text icon:0x1f5b9 noscroll:1", 0, 0, (const void *) textItems }, |
187 | { "panel id:dlg.upload.file icon:0x1f4c1", 0, 0, (const void *) fileItems }, | 204 | { "panel id:dlg.upload.file icon:0x1f4c1", 0, 0, (const void *) fileItems }, |
188 | { "padding" }, | ||
189 | { "dropdown id:upload.id icon:0x1f464", 0, 0, constData_Array(makeIdentityItems_UploadWidget_(d)) }, | ||
190 | { "input id:upload.token hint:hint.upload.token icon:0x1f511" }, | ||
191 | { NULL } | 205 | { NULL } |
192 | }, actions, iElemCount(actions)); | 206 | }, actions, iElemCount(actions)); |
193 | d->info = findChild_Widget(w, "upload.info"); | 207 | d->info = findChild_Widget(w, "upload.info"); |
208 | d->path = findChild_Widget(w, "upload.path"); | ||
194 | d->input = findChild_Widget(w, "upload.text"); | 209 | d->input = findChild_Widget(w, "upload.text"); |
195 | d->filePathLabel = findChild_Widget(w, "upload.filepathlabel"); | 210 | d->filePathLabel = findChild_Widget(w, "upload.filepathlabel"); |
196 | d->fileSizeLabel = findChild_Widget(w, "upload.filesizelabel"); | 211 | d->fileSizeLabel = findChild_Widget(w, "upload.filesizelabel"); |
@@ -200,17 +215,28 @@ void init_UploadWidget(iUploadWidget *d) { | |||
200 | if (isPortraitPhone_App()) { | 215 | if (isPortraitPhone_App()) { |
201 | enableUploadButton_UploadWidget_(d, iFalse); | 216 | enableUploadButton_UploadWidget_(d, iFalse); |
202 | } | 217 | } |
218 | iWidget *title = findChild_Widget(w, "heading.upload.text"); | ||
219 | iLabelWidget *menu = new_LabelWidget(midEllipsis_Icon, "upload.editmenu.open"); | ||
220 | setTextColor_LabelWidget(menu, uiTextAction_ColorId); | ||
221 | setFont_LabelWidget(menu, uiLabelBigBold_FontId); | ||
222 | addChildFlags_Widget(title, iClob(menu), frameless_WidgetFlag | moveToParentRightEdge_WidgetFlag); | ||
203 | } | 223 | } |
204 | else { | 224 | else { |
205 | useSheetStyle_Widget(w); | 225 | useSheetStyle_Widget(w); |
206 | setFlags_Widget(w, overflowScrollable_WidgetFlag, iFalse); | 226 | setFlags_Widget(w, overflowScrollable_WidgetFlag, iFalse); |
207 | addChildFlags_Widget(w, | 227 | addDialogTitle_Widget(w, "${heading.upload}", NULL); |
208 | iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.upload}", NULL)), | 228 | iWidget *headings, *values; |
209 | frameless_WidgetFlag); | 229 | /* URL path. */ { |
210 | d->info = addChildFlags_Widget(w, iClob(new_LabelWidget("", NULL)), | 230 | iWidget *page = makeTwoColumns_Widget(&headings, &values); |
211 | frameless_WidgetFlag | resizeToParentWidth_WidgetFlag | | 231 | d->path = new_InputWidget(0); |
212 | fixedHeight_WidgetFlag); | 232 | addTwoColumnDialogInputField_Widget( |
213 | setWrap_LabelWidget(d->info, iTrue); | 233 | headings, values, "", "upload.path", iClob(d->path)); |
234 | d->info = (iLabelWidget *) lastChild_Widget(headings); | ||
235 | setFont_LabelWidget(d->info, uiContent_FontId); | ||
236 | setTextColor_LabelWidget(d->info, uiInputTextFocused_ColorId); | ||
237 | addChild_Widget(w, iClob(page)); | ||
238 | addChild_Widget(w, iClob(makePadding_Widget(gap_UI))); | ||
239 | } | ||
214 | /* Tabs for input data. */ | 240 | /* Tabs for input data. */ |
215 | iWidget *tabs = makeTabs_Widget(w); | 241 | iWidget *tabs = makeTabs_Widget(w); |
216 | /* Make the tabs support vertical expansion based on content. */ { | 242 | /* Make the tabs support vertical expansion based on content. */ { |
@@ -220,7 +246,6 @@ void init_UploadWidget(iUploadWidget *d) { | |||
220 | setFlags_Widget(tabPages, resizeHeightOfChildren_WidgetFlag, iFalse); | 246 | setFlags_Widget(tabPages, resizeHeightOfChildren_WidgetFlag, iFalse); |
221 | setFlags_Widget(tabPages, arrangeHeight_WidgetFlag, iTrue); | 247 | setFlags_Widget(tabPages, arrangeHeight_WidgetFlag, iTrue); |
222 | } | 248 | } |
223 | iWidget *headings, *values; | ||
224 | setBackgroundColor_Widget(findChild_Widget(tabs, "tabs.buttons"), uiBackgroundSidebar_ColorId); | 249 | setBackgroundColor_Widget(findChild_Widget(tabs, "tabs.buttons"), uiBackgroundSidebar_ColorId); |
225 | setId_Widget(tabs, "upload.tabs"); | 250 | setId_Widget(tabs, "upload.tabs"); |
226 | /* Text input. */ { | 251 | /* Text input. */ { |
@@ -233,7 +258,8 @@ void init_UploadWidget(iUploadWidget *d) { | |||
233 | appendFramelessTabPage_Widget(tabs, iClob(page), "${heading.upload.text}", '1', 0); | 258 | appendFramelessTabPage_Widget(tabs, iClob(page), "${heading.upload.text}", '1', 0); |
234 | } | 259 | } |
235 | /* File content. */ { | 260 | /* File content. */ { |
236 | appendTwoColumnTabPage_Widget(tabs, "${heading.upload.file}", '2', &headings, &values); | 261 | iWidget *page = appendTwoColumnTabPage_Widget(tabs, "${heading.upload.file}", '2', &headings, &values); |
262 | setBackgroundColor_Widget(page, uiBackgroundSidebar_ColorId); | ||
237 | addChildFlags_Widget(headings, iClob(new_LabelWidget("${upload.file.name}", NULL)), frameless_WidgetFlag); | 263 | addChildFlags_Widget(headings, iClob(new_LabelWidget("${upload.file.name}", NULL)), frameless_WidgetFlag); |
238 | d->filePathLabel = addChildFlags_Widget(values, iClob(new_LabelWidget(uiTextAction_ColorEscape "${upload.file.drophere}", NULL)), frameless_WidgetFlag); | 264 | d->filePathLabel = addChildFlags_Widget(values, iClob(new_LabelWidget(uiTextAction_ColorEscape "${upload.file.drophere}", NULL)), frameless_WidgetFlag); |
239 | addChildFlags_Widget(headings, iClob(new_LabelWidget("${upload.file.size}", NULL)), frameless_WidgetFlag); | 265 | addChildFlags_Widget(headings, iClob(new_LabelWidget("${upload.file.size}", NULL)), frameless_WidgetFlag); |
@@ -245,19 +271,20 @@ void init_UploadWidget(iUploadWidget *d) { | |||
245 | /* Identity and Token. */ { | 271 | /* Identity and Token. */ { |
246 | addChild_Widget(w, iClob(makePadding_Widget(gap_UI))); | 272 | addChild_Widget(w, iClob(makePadding_Widget(gap_UI))); |
247 | iWidget *page = makeTwoColumns_Widget(&headings, &values); | 273 | iWidget *page = makeTwoColumns_Widget(&headings, &values); |
248 | /* Token. */ | ||
249 | d->token = addTwoColumnDialogInputField_Widget( | ||
250 | headings, values, "${upload.token}", "upload.token", iClob(new_InputWidget(0))); | ||
251 | setHint_InputWidget(d->token, "${hint.upload.token}"); | ||
252 | setFixedSize_Widget(as_Widget(d->token), init_I2(50 * gap_UI, -1)); | ||
253 | /* Identity. */ | 274 | /* Identity. */ |
254 | const iArray * identItems = makeIdentityItems_UploadWidget_(d); | 275 | const iArray * identItems = makeIdentityItems_UploadWidget_(d); |
255 | const iMenuItem *items = constData_Array(identItems); | 276 | const iMenuItem *items = constData_Array(identItems); |
256 | const size_t numItems = size_Array(identItems); | 277 | const size_t numItems = size_Array(identItems); |
257 | iLabelWidget * ident = makeMenuButton_LabelWidget("${upload.id}", items, numItems); | 278 | d->ident = makeMenuButton_LabelWidget("${upload.id}", items, numItems); |
258 | setTextCStr_LabelWidget(ident, items[findWidestLabel_MenuItem(items, numItems)].label); | 279 | setTextCStr_LabelWidget(d->ident, items[findWidestLabel_MenuItem(items, numItems)].label); |
280 | //setFixedSize_Widget(as_Widget(d->ident), init_I2(50 * gap_UI, )); | ||
259 | addChild_Widget(headings, iClob(makeHeading_Widget("${upload.id}"))); | 281 | addChild_Widget(headings, iClob(makeHeading_Widget("${upload.id}"))); |
260 | setId_Widget(addChildFlags_Widget(values, iClob(ident), alignLeft_WidgetFlag), "upload.id"); | 282 | setId_Widget(addChildFlags_Widget(values, iClob(d->ident), alignLeft_WidgetFlag), "upload.id"); |
283 | /* Token. */ | ||
284 | d->token = addTwoColumnDialogInputField_Widget( | ||
285 | headings, values, "${upload.token}", "upload.token", iClob(new_InputWidget(0))); | ||
286 | setHint_InputWidget(d->token, "${hint.upload.token}"); | ||
287 | setFixedSize_Widget(as_Widget(d->token), init_I2(50 * gap_UI, -1)); | ||
261 | addChild_Widget(w, iClob(page)); | 288 | addChild_Widget(w, iClob(page)); |
262 | } | 289 | } |
263 | /* Buttons. */ { | 290 | /* Buttons. */ { |
@@ -272,7 +299,12 @@ void init_UploadWidget(iUploadWidget *d) { | |||
272 | } | 299 | } |
273 | resizeToLargestPage_Widget(tabs); | 300 | resizeToLargestPage_Widget(tabs); |
274 | arrange_Widget(w); | 301 | arrange_Widget(w); |
302 | setFixedSize_Widget(as_Widget(d->path), init_I2(width_Widget(tabs) - width_Widget(d->info), -1)); | ||
303 | setFixedSize_Widget(as_Widget(d->mime), init_I2(width_Widget(tabs) - 3 * gap_UI - | ||
304 | left_Rect(parent_Widget(d->mime)->rect), -1)); | ||
275 | setFixedSize_Widget(as_Widget(d->token), init_I2(width_Widget(tabs) - left_Rect(parent_Widget(d->token)->rect), -1)); | 305 | setFixedSize_Widget(as_Widget(d->token), init_I2(width_Widget(tabs) - left_Rect(parent_Widget(d->token)->rect), -1)); |
306 | setFixedSize_Widget(as_Widget(d->ident), init_I2(width_Widget(d->token), | ||
307 | lineHeight_Text(uiLabel_FontId) + 2 * gap_UI)); | ||
276 | setFlags_Widget(as_Widget(d->token), expand_WidgetFlag, iTrue); | 308 | setFlags_Widget(as_Widget(d->token), expand_WidgetFlag, iTrue); |
277 | setFocus_Widget(as_Widget(d->input)); | 309 | setFocus_Widget(as_Widget(d->input)); |
278 | } | 310 | } |
@@ -339,8 +371,24 @@ static void setUrlPort_UploadWidget_(iUploadWidget *d, const iString *url, uint1 | |||
339 | appendRange_String(&d->url, (iRangecc){ parts.scheme.end, parts.host.end }); | 371 | appendRange_String(&d->url, (iRangecc){ parts.scheme.end, parts.host.end }); |
340 | appendFormat_String(&d->url, ":%u", overridePort ? overridePort : titanPortForUrl_(url)); | 372 | appendFormat_String(&d->url, ":%u", overridePort ? overridePort : titanPortForUrl_(url)); |
341 | appendRange_String(&d->url, (iRangecc){ parts.path.start, constEnd_String(url) }); | 373 | appendRange_String(&d->url, (iRangecc){ parts.path.start, constEnd_String(url) }); |
342 | setText_LabelWidget(d->info, &d->url); | 374 | const iRangecc siteRoot = urlRoot_String(&d->url); |
343 | arrange_Widget(as_Widget(d)); | 375 | setTextCStr_LabelWidget(d->info, cstr_Rangecc((iRangecc){ urlHost_String(&d->url).start, |
376 | siteRoot.end })); | ||
377 | /* From root onwards, the URL is editable. */ | ||
378 | setTextCStr_InputWidget(d->path, | ||
379 | cstr_Rangecc((iRangecc){ siteRoot.end, constEnd_String(&d->url) })); | ||
380 | if (!cmp_String(text_InputWidget(d->path), "/")) { | ||
381 | setTextCStr_InputWidget(d->path, ""); /* might as well show the hint */ | ||
382 | } | ||
383 | if (isUsingPanelLayout_Mobile()) { | ||
384 | arrange_Widget(as_Widget(d)); /* a wrapped label */ | ||
385 | } | ||
386 | else { | ||
387 | setFixedSize_Widget(as_Widget(d->path), | ||
388 | init_I2(width_Widget(findChild_Widget(as_Widget(d), "upload.tabs")) - | ||
389 | width_Widget(d->info), | ||
390 | -1)); | ||
391 | } | ||
344 | } | 392 | } |
345 | 393 | ||
346 | void setUrl_UploadWidget(iUploadWidget *d, const iString *url) { | 394 | void setUrl_UploadWidget(iUploadWidget *d, const iString *url) { |
@@ -353,6 +401,10 @@ void setResponseViewer_UploadWidget(iUploadWidget *d, iDocumentWidget *doc) { | |||
353 | d->viewer = doc; | 401 | d->viewer = doc; |
354 | } | 402 | } |
355 | 403 | ||
404 | void setText_UploadWidget(iUploadWidget *d, const iString *text) { | ||
405 | setText_InputWidget(findChild_Widget(as_Widget(d), "upload.text"), text); | ||
406 | } | ||
407 | |||
356 | static iWidget *acceptButton_UploadWidget_(iUploadWidget *d) { | 408 | static iWidget *acceptButton_UploadWidget_(iUploadWidget *d) { |
357 | return lastChild_Widget(findChild_Widget(as_Widget(d), "dialogbuttons")); | 409 | return lastChild_Widget(findChild_Widget(as_Widget(d), "dialogbuttons")); |
358 | } | 410 | } |
@@ -396,6 +448,18 @@ static void showOrHideUploadButton_UploadWidget_(iUploadWidget *d) { | |||
396 | } | 448 | } |
397 | } | 449 | } |
398 | 450 | ||
451 | static const iString *requestUrl_UploadWidget_(const iUploadWidget *d) { | ||
452 | const iRangecc siteRoot = urlRoot_String(&d->url); | ||
453 | iString *reqUrl = collectNew_String(); | ||
454 | setRange_String(reqUrl, (iRangecc){ constBegin_String(&d->url), siteRoot.end }); | ||
455 | const iString *path = text_InputWidget(d->path); | ||
456 | if (!startsWith_String(path, "/")) { | ||
457 | appendCStr_String(reqUrl, "/"); | ||
458 | } | ||
459 | append_String(reqUrl, path); | ||
460 | return reqUrl; | ||
461 | } | ||
462 | |||
399 | static iBool processEvent_UploadWidget_(iUploadWidget *d, const SDL_Event *ev) { | 463 | static iBool processEvent_UploadWidget_(iUploadWidget *d, const SDL_Event *ev) { |
400 | iWidget *w = as_Widget(d); | 464 | iWidget *w = as_Widget(d); |
401 | const char *cmd = command_UserEvent(ev); | 465 | const char *cmd = command_UserEvent(ev); |
@@ -405,8 +469,22 @@ static iBool processEvent_UploadWidget_(iUploadWidget *d, const SDL_Event *ev) { | |||
405 | } | 469 | } |
406 | else if (equal_Command(cmd, "panel.changed")) { | 470 | else if (equal_Command(cmd, "panel.changed")) { |
407 | showOrHideUploadButton_UploadWidget_(d); | 471 | showOrHideUploadButton_UploadWidget_(d); |
472 | if (currentPanelIndex_Mobile(w) == 0) { | ||
473 | setFocus_Widget(as_Widget(d->input)); | ||
474 | } | ||
475 | else { | ||
476 | setFocus_Widget(NULL); | ||
477 | } | ||
478 | refresh_Widget(d->input); | ||
479 | return iFalse; | ||
480 | } | ||
481 | #if defined (iPlatformAppleMobile) | ||
482 | else if (deviceType_App() != desktop_AppDeviceType && equal_Command(cmd, "menu.opened")) { | ||
483 | setFocus_Widget(NULL); /* overlaid text fields! */ | ||
484 | refresh_Widget(d->input); | ||
408 | return iFalse; | 485 | return iFalse; |
409 | } | 486 | } |
487 | #endif | ||
410 | else if (equal_Command(cmd, "upload.cancel")) { | 488 | else if (equal_Command(cmd, "upload.cancel")) { |
411 | setupSheetTransition_Mobile(w, iFalse); | 489 | setupSheetTransition_Mobile(w, iFalse); |
412 | destroy_Widget(w); | 490 | destroy_Widget(w); |
@@ -444,6 +522,43 @@ static iBool processEvent_UploadWidget_(iUploadWidget *d, const SDL_Event *ev) { | |||
444 | updateIdentityDropdown_UploadWidget_(d); | 522 | updateIdentityDropdown_UploadWidget_(d); |
445 | return iTrue; | 523 | return iTrue; |
446 | } | 524 | } |
525 | if (isCommand_Widget(w, ev, "upload.editmenu.open")) { | ||
526 | setFocus_Widget(NULL); | ||
527 | refresh_Widget(as_Widget(d->input)); | ||
528 | iWidget *editMenu = makeMenu_Widget(root_Widget(w), (iMenuItem[]){ | ||
529 | { select_Icon " ${menu.selectall}", 0, 0, "upload.text.selectall" }, | ||
530 | { export_Icon " ${menu.upload.export}", 0, 0, "upload.text.export" }, | ||
531 | { "---" }, | ||
532 | { delete_Icon " " uiTextCaution_ColorEscape "${menu.upload.delete}", 0, 0, "upload.text.delete" } | ||
533 | }, 4); | ||
534 | openMenu_Widget(editMenu, topLeft_Rect(bounds_Widget(as_Widget(d->input)))); | ||
535 | return iTrue; | ||
536 | } | ||
537 | if (isCommand_UserEvent(ev, "upload.text.export")) { | ||
538 | #if defined (iPlatformAppleMobile) | ||
539 | openTextActivityView_iOS(text_InputWidget(d->input)); | ||
540 | #endif | ||
541 | return iTrue; | ||
542 | } | ||
543 | if (isCommand_UserEvent(ev, "upload.text.delete")) { | ||
544 | if (argLabel_Command(command_UserEvent(ev), "confirmed")) { | ||
545 | setTextCStr_InputWidget(d->input, ""); | ||
546 | setFocus_Widget(as_Widget(d->input)); | ||
547 | } | ||
548 | else { | ||
549 | openMenu_Widget(makeMenu_Widget(root_Widget(w), (iMenuItem[]){ | ||
550 | { delete_Icon " " uiTextCaution_ColorEscape "${menu.upload.delete.confirm}", 0, 0, | ||
551 | "upload.text.delete confirmed:1" } | ||
552 | }, 1), zero_I2()); | ||
553 | } | ||
554 | return iTrue; | ||
555 | } | ||
556 | if (isCommand_UserEvent(ev, "upload.text.selectall")) { | ||
557 | setFocus_Widget(as_Widget(d->input)); | ||
558 | refresh_Widget(as_Widget(d->input)); | ||
559 | postCommand_Widget(d->input, "input.selectall"); | ||
560 | return iTrue; | ||
561 | } | ||
447 | if (isCommand_Widget(w, ev, "upload.accept")) { | 562 | if (isCommand_Widget(w, ev, "upload.accept")) { |
448 | iBool isText; | 563 | iBool isText; |
449 | iWidget *tabs = findChild_Widget(w, "upload.tabs"); | 564 | iWidget *tabs = findChild_Widget(w, "upload.tabs"); |
@@ -464,7 +579,7 @@ static iBool processEvent_UploadWidget_(iUploadWidget *d, const SDL_Event *ev) { | |||
464 | d->request = new_GmRequest(certs_App()); | 579 | d->request = new_GmRequest(certs_App()); |
465 | setSendProgressFunc_GmRequest(d->request, updateProgress_UploadWidget_); | 580 | setSendProgressFunc_GmRequest(d->request, updateProgress_UploadWidget_); |
466 | setUserData_Object(d->request, d); | 581 | setUserData_Object(d->request, d); |
467 | setUrl_GmRequest(d->request, &d->url); | 582 | setUrl_GmRequest(d->request, requestUrl_UploadWidget_(d)); |
468 | const iString *site = collectNewRange_String(urlRoot_String(&d->url)); | 583 | const iString *site = collectNewRange_String(urlRoot_String(&d->url)); |
469 | switch (d->idMode) { | 584 | switch (d->idMode) { |
470 | case none_UploadIdentity: | 585 | case none_UploadIdentity: |
@@ -540,10 +655,15 @@ static iBool processEvent_UploadWidget_(iUploadWidget *d, const SDL_Event *ev) { | |||
540 | return iTrue; | 655 | return iTrue; |
541 | } | 656 | } |
542 | else if (isCommand_Widget(w, ev, "input.resized")) { | 657 | else if (isCommand_Widget(w, ev, "input.resized")) { |
543 | resizeToLargestPage_Widget(findChild_Widget(w, "upload.tabs")); | 658 | if (!isUsingPanelLayout_Mobile()) { |
544 | arrange_Widget(w); | 659 | resizeToLargestPage_Widget(findChild_Widget(w, "upload.tabs")); |
545 | refresh_Widget(w); | 660 | arrange_Widget(w); |
546 | return iTrue; | 661 | refresh_Widget(w); |
662 | return iTrue; | ||
663 | } | ||
664 | else { | ||
665 | refresh_Widget(as_Widget(d->input)); | ||
666 | } | ||
547 | } | 667 | } |
548 | else if (isCommand_Widget(w, ev, "upload.pickfile")) { | 668 | else if (isCommand_Widget(w, ev, "upload.pickfile")) { |
549 | #if defined (iPlatformAppleMobile) | 669 | #if defined (iPlatformAppleMobile) |
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) | |||
31 | 31 | ||
32 | void setUrl_UploadWidget (iUploadWidget *, const iString *url); | 32 | void setUrl_UploadWidget (iUploadWidget *, const iString *url); |
33 | void setResponseViewer_UploadWidget (iUploadWidget *, iDocumentWidget *doc); | 33 | void setResponseViewer_UploadWidget (iUploadWidget *, iDocumentWidget *doc); |
34 | void setText_UploadWidget (iUploadWidget *, const iString *text); | ||
diff --git a/src/ui/util.c b/src/ui/util.c index 912e1d37..31907721 100644 --- a/src/ui/util.c +++ b/src/ui/util.c | |||
@@ -479,12 +479,15 @@ void init_SmoothScroll(iSmoothScroll *d, iWidget *owner, iSmoothScrollNotifyFunc | |||
479 | reset_SmoothScroll(d); | 479 | reset_SmoothScroll(d); |
480 | d->widget = owner; | 480 | d->widget = owner; |
481 | d->notify = notify; | 481 | d->notify = notify; |
482 | d->pullActionTriggered = 0; | ||
483 | d->flags = 0; | ||
482 | } | 484 | } |
483 | 485 | ||
484 | void reset_SmoothScroll(iSmoothScroll *d) { | 486 | void reset_SmoothScroll(iSmoothScroll *d) { |
485 | init_Anim(&d->pos, 0); | 487 | init_Anim(&d->pos, 0); |
486 | d->max = 0; | 488 | d->max = 0; |
487 | d->overscroll = (deviceType_App() != desktop_AppDeviceType ? 100 * gap_UI : 0); | 489 | d->overscroll = (deviceType_App() != desktop_AppDeviceType ? 100 * gap_UI : 0); |
490 | d->pullActionTriggered = 0; | ||
488 | } | 491 | } |
489 | 492 | ||
490 | void setMax_SmoothScroll(iSmoothScroll *d, int max) { | 493 | void setMax_SmoothScroll(iSmoothScroll *d, int max) { |
@@ -518,6 +521,29 @@ iBool isFinished_SmoothScroll(const iSmoothScroll *d) { | |||
518 | return isFinished_Anim(&d->pos); | 521 | return isFinished_Anim(&d->pos); |
519 | } | 522 | } |
520 | 523 | ||
524 | iLocalDef int pullActionThreshold_SmoothScroll_(const iSmoothScroll *d) { | ||
525 | return d->overscroll * 6 / 10; | ||
526 | } | ||
527 | |||
528 | float pullActionPos_SmoothScroll(const iSmoothScroll *d) { | ||
529 | if (d->pullActionTriggered >= 1) { | ||
530 | return 1.0f; | ||
531 | } | ||
532 | float pos = overscroll_SmoothScroll_(d); | ||
533 | if (pos >= 0.0f) { | ||
534 | return 0.0f; | ||
535 | } | ||
536 | pos = -pos / (float) pullActionThreshold_SmoothScroll_(d); | ||
537 | return iMin(pos, 1.0f); | ||
538 | } | ||
539 | |||
540 | static void checkPullAction_SmoothScroll_(iSmoothScroll *d) { | ||
541 | if (d->pullActionTriggered == 1 && d->widget) { | ||
542 | postCommand_Widget(d->widget, "pullaction"); | ||
543 | d->pullActionTriggered = 2; /* pending handling */ | ||
544 | } | ||
545 | } | ||
546 | |||
521 | void moveSpan_SmoothScroll(iSmoothScroll *d, int offset, uint32_t span) { | 547 | void moveSpan_SmoothScroll(iSmoothScroll *d, int offset, uint32_t span) { |
522 | #if !defined (iPlatformMobile) | 548 | #if !defined (iPlatformMobile) |
523 | if (!prefs_App()->smoothScrolling) { | 549 | if (!prefs_App()->smoothScrolling) { |
@@ -525,16 +551,19 @@ void moveSpan_SmoothScroll(iSmoothScroll *d, int offset, uint32_t span) { | |||
525 | } | 551 | } |
526 | #endif | 552 | #endif |
527 | int destY = targetValue_Anim(&d->pos) + offset; | 553 | int destY = targetValue_Anim(&d->pos) + offset; |
554 | if (d->flags & pullDownAction_SmoothScrollFlag && destY < -pullActionThreshold_SmoothScroll_(d)) { | ||
555 | if (d->pullActionTriggered == 0) { | ||
556 | d->pullActionTriggered = iTrue; | ||
557 | #if defined (iPlatformAppleMobile) | ||
558 | playHapticEffect_iOS(tap_HapticEffect); | ||
559 | #endif | ||
560 | } | ||
561 | } | ||
528 | if (destY < -d->overscroll) { | 562 | if (destY < -d->overscroll) { |
529 | destY = -d->overscroll; | 563 | destY = -d->overscroll; |
530 | } | 564 | } |
531 | if (d->max > 0) { | 565 | if (destY >= d->max + d->overscroll) { |
532 | if (destY >= d->max + d->overscroll) { | 566 | destY = d->max + d->overscroll; |
533 | destY = d->max + d->overscroll; | ||
534 | } | ||
535 | } | ||
536 | else { | ||
537 | destY = 0; | ||
538 | } | 567 | } |
539 | if (span) { | 568 | if (span) { |
540 | setValueEased_Anim(&d->pos, destY, span); | 569 | setValueEased_Anim(&d->pos, destY, span); |
@@ -552,6 +581,7 @@ void moveSpan_SmoothScroll(iSmoothScroll *d, int offset, uint32_t span) { | |||
552 | // printf("remaining: %f dur: %d\n", remaining, duration); | 581 | // printf("remaining: %f dur: %d\n", remaining, duration); |
553 | d->pos.bounce = (osDelta < 0 ? -1 : 1) * | 582 | d->pos.bounce = (osDelta < 0 ? -1 : 1) * |
554 | iMini(5 * d->overscroll, remaining * remaining * 0.00005f); | 583 | iMini(5 * d->overscroll, remaining * remaining * 0.00005f); |
584 | checkPullAction_SmoothScroll_(d); | ||
555 | } | 585 | } |
556 | } | 586 | } |
557 | if (d->notify) { | 587 | if (d->notify) { |
@@ -570,6 +600,7 @@ iBool processEvent_SmoothScroll(iSmoothScroll *d, const SDL_Event *ev) { | |||
570 | moveSpan_SmoothScroll(d, -osDelta, 100 * sqrt(iAbs(osDelta) / gap_UI)); | 600 | moveSpan_SmoothScroll(d, -osDelta, 100 * sqrt(iAbs(osDelta) / gap_UI)); |
571 | d->pos.flags = easeOut_AnimFlag | muchSofter_AnimFlag; | 601 | d->pos.flags = easeOut_AnimFlag | muchSofter_AnimFlag; |
572 | } | 602 | } |
603 | checkPullAction_SmoothScroll_(d); | ||
573 | return iTrue; | 604 | return iTrue; |
574 | } | 605 | } |
575 | return iFalse; | 606 | return iFalse; |
@@ -620,7 +651,7 @@ static iBool isCommandIgnoredByMenus_(const char *cmd) { | |||
620 | if (equal_Command(cmd, "window.focus.lost") || | 651 | if (equal_Command(cmd, "window.focus.lost") || |
621 | equal_Command(cmd, "window.focus.gained")) return iTrue; | 652 | equal_Command(cmd, "window.focus.gained")) return iTrue; |
622 | /* TODO: Perhaps a common way of indicating which commands are notifications and should not | 653 | /* TODO: Perhaps a common way of indicating which commands are notifications and should not |
623 | be reacted to by menus? */ | 654 | be reacted to by menus?! */ |
624 | return equal_Command(cmd, "media.updated") || | 655 | return equal_Command(cmd, "media.updated") || |
625 | equal_Command(cmd, "media.player.update") || | 656 | equal_Command(cmd, "media.player.update") || |
626 | startsWith_CStr(cmd, "feeds.update.") || | 657 | startsWith_CStr(cmd, "feeds.update.") || |
@@ -640,13 +671,16 @@ static iBool isCommandIgnoredByMenus_(const char *cmd) { | |||
640 | equal_Command(cmd, "window.reload.update") || | 671 | equal_Command(cmd, "window.reload.update") || |
641 | equal_Command(cmd, "window.mouse.exited") || | 672 | equal_Command(cmd, "window.mouse.exited") || |
642 | equal_Command(cmd, "window.mouse.entered") || | 673 | equal_Command(cmd, "window.mouse.entered") || |
674 | equal_Command(cmd, "input.backup") || | ||
675 | equal_Command(cmd, "input.ended") || | ||
676 | equal_Command(cmd, "focus.lost") || | ||
643 | (equal_Command(cmd, "mouse.clicked") && !arg_Command(cmd)); /* button released */ | 677 | (equal_Command(cmd, "mouse.clicked") && !arg_Command(cmd)); /* button released */ |
644 | } | 678 | } |
645 | 679 | ||
646 | static iLabelWidget *parentMenuButton_(const iWidget *menu) { | 680 | static iLabelWidget *parentMenuButton_(const iWidget *menu) { |
647 | if (isInstance_Object(menu->parent, &Class_LabelWidget)) { | 681 | if (isInstance_Object(menu->parent, &Class_LabelWidget)) { |
648 | iLabelWidget *button = (iLabelWidget *) menu->parent; | 682 | iLabelWidget *button = (iLabelWidget *) menu->parent; |
649 | if (!cmp_String(command_LabelWidget(button), "menu.open")) { | 683 | if (equal_Command(cstr_String(command_LabelWidget(button)), "menu.open")) { |
650 | return button; | 684 | return button; |
651 | } | 685 | } |
652 | } | 686 | } |
@@ -671,6 +705,21 @@ static iBool menuHandler_(iWidget *menu, const char *cmd) { | |||
671 | closeMenu_Widget(menu); | 705 | closeMenu_Widget(menu); |
672 | return iTrue; | 706 | return iTrue; |
673 | } | 707 | } |
708 | if (equal_Command(cmd, "cancel") && pointerLabel_Command(cmd, "menu") == menu) { | ||
709 | return iFalse; | ||
710 | } | ||
711 | if (equal_Command(cmd, "contextclick") && pointer_Command(cmd) == menu) { | ||
712 | return iFalse; | ||
713 | } | ||
714 | if (deviceType_App() == phone_AppDeviceType && equal_Command(cmd, "keyboard.changed") && | ||
715 | arg_Command(cmd) == 0) { | ||
716 | /* May need to reposition the menu. */ | ||
717 | menu->rect.pos = windowToLocal_Widget( | ||
718 | menu, | ||
719 | init_I2(left_Rect(bounds_Widget(menu)), | ||
720 | bottom_Rect(safeRect_Root(menu->root)) - menu->rect.size.y)); | ||
721 | return iFalse; | ||
722 | } | ||
674 | if (!isCommandIgnoredByMenus_(cmd)) { | 723 | if (!isCommandIgnoredByMenus_(cmd)) { |
675 | closeMenu_Widget(menu); | 724 | closeMenu_Widget(menu); |
676 | } | 725 | } |
@@ -733,13 +782,16 @@ void makeMenuItems_Widget(iWidget *menu, const iMenuItem *items, size_t n) { | |||
733 | noBackground_WidgetFlag | frameless_WidgetFlag | alignLeft_WidgetFlag | | 782 | noBackground_WidgetFlag | frameless_WidgetFlag | alignLeft_WidgetFlag | |
734 | drawKey_WidgetFlag | itemFlags); | 783 | drawKey_WidgetFlag | itemFlags); |
735 | setWrap_LabelWidget(label, isInfo); | 784 | setWrap_LabelWidget(label, isInfo); |
785 | if (!isInfo) { | ||
736 | haveIcons |= checkIcon_LabelWidget(label); | 786 | haveIcons |= checkIcon_LabelWidget(label); |
737 | updateSize_LabelWidget(label); /* drawKey was set */ | 787 | } |
738 | setFlags_Widget(as_Widget(label), disabled_WidgetFlag, isDisabled); | 788 | setFlags_Widget(as_Widget(label), disabled_WidgetFlag, isDisabled); |
739 | if (isInfo) { | 789 | if (isInfo) { |
740 | setFlags_Widget(as_Widget(label), fixedHeight_WidgetFlag, iTrue); /* wrap changes height */ | 790 | setFlags_Widget(as_Widget(label), resizeToParentWidth_WidgetFlag | |
791 | fixedHeight_WidgetFlag, iTrue); /* wrap changes height */ | ||
741 | setTextColor_LabelWidget(label, uiTextAction_ColorId); | 792 | setTextColor_LabelWidget(label, uiTextAction_ColorId); |
742 | } | 793 | } |
794 | updateSize_LabelWidget(label); /* drawKey was set */ | ||
743 | } | 795 | } |
744 | } | 796 | } |
745 | if (deviceType_App() == phone_AppDeviceType) { | 797 | if (deviceType_App() == phone_AppDeviceType) { |
@@ -861,12 +913,8 @@ iWidget *makeMenu_Widget(iWidget *parent, const iMenuItem *items, size_t n) { | |||
861 | setFlags_Widget(menu, | 913 | setFlags_Widget(menu, |
862 | keepOnTop_WidgetFlag | collapse_WidgetFlag | hidden_WidgetFlag | | 914 | keepOnTop_WidgetFlag | collapse_WidgetFlag | hidden_WidgetFlag | |
863 | arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag | | 915 | arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag | |
864 | resizeChildrenToWidestChild_WidgetFlag | overflowScrollable_WidgetFlag | | 916 | resizeChildrenToWidestChild_WidgetFlag | overflowScrollable_WidgetFlag, |
865 | (isPortraitPhone_App() ? drawBackgroundToVerticalSafeArea_WidgetFlag : 0), | ||
866 | iTrue); | 917 | iTrue); |
867 | if (!isPortraitPhone_App()) { | ||
868 | setFrameColor_Widget(menu, uiBackgroundSelected_ColorId); | ||
869 | } | ||
870 | makeMenuItems_Widget(menu, items, n); | 918 | makeMenuItems_Widget(menu, items, n); |
871 | addChild_Widget(parent, menu); | 919 | addChild_Widget(parent, menu); |
872 | iRelease(menu); /* owned by parent now */ | 920 | iRelease(menu); /* owned by parent now */ |
@@ -884,6 +932,7 @@ void openMenu_Widget(iWidget *d, iInt2 windowCoord) { | |||
884 | 932 | ||
885 | static void updateMenuItemFonts_Widget_(iWidget *d) { | 933 | static void updateMenuItemFonts_Widget_(iWidget *d) { |
886 | const iBool isPortraitPhone = (deviceType_App() == phone_AppDeviceType && isPortrait_App()); | 934 | const iBool isPortraitPhone = (deviceType_App() == phone_AppDeviceType && isPortrait_App()); |
935 | const iBool isMobile = (deviceType_App() != desktop_AppDeviceType); | ||
887 | const iBool isSlidePanel = (flags_Widget(d) & horizontalOffset_WidgetFlag) != 0; | 936 | const iBool isSlidePanel = (flags_Widget(d) & horizontalOffset_WidgetFlag) != 0; |
888 | iForEach(ObjectList, i, children_Widget(d)) { | 937 | iForEach(ObjectList, i, children_Widget(d)) { |
889 | if (isInstance_Object(i.object, &Class_LabelWidget)) { | 938 | if (isInstance_Object(i.object, &Class_LabelWidget)) { |
@@ -892,16 +941,16 @@ static void updateMenuItemFonts_Widget_(iWidget *d) { | |||
892 | if (isWrapped_LabelWidget(label)) { | 941 | if (isWrapped_LabelWidget(label)) { |
893 | continue; | 942 | continue; |
894 | } | 943 | } |
895 | if (deviceType_App() == desktop_AppDeviceType) { | 944 | switch (deviceType_App()) { |
945 | case desktop_AppDeviceType: | ||
896 | setFont_LabelWidget(label, isCaution ? uiLabelBold_FontId : uiLabel_FontId); | 946 | setFont_LabelWidget(label, isCaution ? uiLabelBold_FontId : uiLabel_FontId); |
897 | } | 947 | break; |
898 | else if (isPortraitPhone) { | 948 | case tablet_AppDeviceType: |
899 | if (!isSlidePanel) { | 949 | setFont_LabelWidget(label, isCaution ? uiLabelMediumBold_FontId : uiLabelMedium_FontId); |
950 | break; | ||
951 | case phone_AppDeviceType: | ||
900 | setFont_LabelWidget(label, isCaution ? uiLabelBigBold_FontId : uiLabelBig_FontId); | 952 | setFont_LabelWidget(label, isCaution ? uiLabelBigBold_FontId : uiLabelBig_FontId); |
901 | } | 953 | break; |
902 | } | ||
903 | else { | ||
904 | setFont_LabelWidget(label, isCaution ? uiContentBold_FontId : uiContent_FontId); | ||
905 | } | 954 | } |
906 | } | 955 | } |
907 | else if (childCount_Widget(i.object)) { | 956 | else if (childCount_Widget(i.object)) { |
@@ -1024,20 +1073,28 @@ void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, int menuOpenFlags) { | |||
1024 | #else | 1073 | #else |
1025 | const iRect rootRect = rect_Root(d->root); | 1074 | const iRect rootRect = rect_Root(d->root); |
1026 | const iInt2 rootSize = rootRect.size; | 1075 | const iInt2 rootSize = rootRect.size; |
1027 | const iBool isPortraitPhone = (deviceType_App() == phone_AppDeviceType && isPortrait_App()); | 1076 | const iBool isPhone = (deviceType_App() == phone_AppDeviceType); |
1077 | const iBool isPortraitPhone = (isPhone && isPortrait_App()); | ||
1028 | const iBool isSlidePanel = (flags_Widget(d) & horizontalOffset_WidgetFlag) != 0; | 1078 | const iBool isSlidePanel = (flags_Widget(d) & horizontalOffset_WidgetFlag) != 0; |
1029 | if (postCommands) { | 1079 | if (postCommands) { |
1030 | postCommand_App("cancel"); /* dismiss any other menus */ | 1080 | postCommandf_App("cancel menu:%p", d); /* dismiss any other menus */ |
1031 | } | 1081 | } |
1032 | /* Menu closes when commands are emitted, so handle any pending ones beforehand. */ | 1082 | /* Menu closes when commands are emitted, so handle any pending ones beforehand. */ |
1033 | processEvents_App(postedEventsOnly_AppEventMode); | 1083 | processEvents_App(postedEventsOnly_AppEventMode); |
1034 | setFlags_Widget(d, hidden_WidgetFlag, iFalse); | 1084 | setFlags_Widget(d, hidden_WidgetFlag, iFalse); |
1035 | setFlags_Widget(d, commandOnMouseMiss_WidgetFlag, iTrue); | 1085 | setFlags_Widget(d, commandOnMouseMiss_WidgetFlag, iTrue); |
1036 | setFlags_Widget(findChild_Widget(d, "menu.cancel"), disabled_WidgetFlag, iFalse); | 1086 | setFlags_Widget(findChild_Widget(d, "menu.cancel"), disabled_WidgetFlag, iFalse); |
1087 | if (!isPortraitPhone) { | ||
1088 | setFrameColor_Widget(d, uiBackgroundSelected_ColorId); | ||
1089 | } | ||
1090 | else { | ||
1091 | setFrameColor_Widget(d, none_ColorId); | ||
1092 | } | ||
1037 | arrange_Widget(d); /* need to know the height */ | 1093 | arrange_Widget(d); /* need to know the height */ |
1038 | iBool allowOverflow = iFalse; | 1094 | iBool allowOverflow = iFalse; |
1039 | /* A vertical offset determined by a possible selected label in the menu. */ | 1095 | /* A vertical offset determined by a possible selected label in the menu. */ |
1040 | if (windowCoord.y < rootSize.y - lineHeight_Text(uiNormal_FontSize) * 3) { | 1096 | if (deviceType_App() == desktop_AppDeviceType && |
1097 | windowCoord.y < rootSize.y - lineHeight_Text(uiNormal_FontSize) * 3) { | ||
1041 | iConstForEach(ObjectList, child, children_Widget(d)) { | 1098 | iConstForEach(ObjectList, child, children_Widget(d)) { |
1042 | const iWidget *item = constAs_Widget(child.object); | 1099 | const iWidget *item = constAs_Widget(child.object); |
1043 | if (flags_Widget(item) & selected_WidgetFlag) { | 1100 | if (flags_Widget(item) & selected_WidgetFlag) { |
@@ -1104,22 +1161,35 @@ void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, int menuOpenFlags) { | |||
1104 | } | 1161 | } |
1105 | #endif | 1162 | #endif |
1106 | raise_Widget(d); | 1163 | raise_Widget(d); |
1107 | if (isPortraitPhone) { | 1164 | if (deviceType_App() != desktop_AppDeviceType) { |
1108 | setFlags_Widget(d, arrangeWidth_WidgetFlag | resizeChildrenToWidestChild_WidgetFlag, iFalse); | 1165 | setFlags_Widget(d, arrangeWidth_WidgetFlag | resizeChildrenToWidestChild_WidgetFlag, |
1109 | setFlags_Widget(d, resizeWidthOfChildren_WidgetFlag | drawBackgroundToBottom_WidgetFlag, iTrue); | 1166 | !isPhone); |
1110 | if (!isSlidePanel) { | 1167 | setFlags_Widget(d, |
1111 | setFlags_Widget(d, borderTop_WidgetFlag, iTrue); | 1168 | resizeWidthOfChildren_WidgetFlag | drawBackgroundToBottom_WidgetFlag | |
1169 | drawBackgroundToVerticalSafeArea_WidgetFlag, | ||
1170 | isPhone); | ||
1171 | if (isPhone) { | ||
1172 | setFlags_Widget(d, borderTop_WidgetFlag, !isSlidePanel && isPortrait_App()); /* menu is otherwise frameless */ | ||
1173 | setFixedSize_Widget(d, init_I2(iMin(rootSize.x, rootSize.y), -1)); | ||
1174 | } | ||
1175 | else { | ||
1176 | d->rect.size.x = 0; | ||
1112 | } | 1177 | } |
1113 | d->rect.size.x = rootSize.x; | ||
1114 | } | 1178 | } |
1115 | updateMenuItemFonts_Widget_(d); | 1179 | updateMenuItemFonts_Widget_(d); |
1116 | arrange_Widget(d); | 1180 | arrange_Widget(d); |
1117 | if (isPortraitPhone) { | 1181 | if (!isSlidePanel) { |
1182 | /* LAYOUT BUG: Height of wrapped menu items is incorrect with a single arrange! */ | ||
1183 | arrange_Widget(d); | ||
1184 | } | ||
1185 | if (deviceType_App() == phone_AppDeviceType) { | ||
1118 | if (isSlidePanel) { | 1186 | if (isSlidePanel) { |
1119 | d->rect.pos = zero_I2(); | 1187 | d->rect.pos = zero_I2(); |
1120 | } | 1188 | } |
1121 | else { | 1189 | else { |
1122 | d->rect.pos = init_I2(0, rootSize.y); | 1190 | d->rect.pos = windowToLocal_Widget(d, |
1191 | init_I2(rootSize.x / 2 - d->rect.size.x / 2, | ||
1192 | rootSize.y)); | ||
1123 | } | 1193 | } |
1124 | } | 1194 | } |
1125 | else if (menuOpenFlags & center_MenuOpenFlags) { | 1195 | else if (menuOpenFlags & center_MenuOpenFlags) { |
@@ -1143,6 +1213,9 @@ void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, int menuOpenFlags) { | |||
1143 | leftExcess += l; | 1213 | leftExcess += l; |
1144 | rightExcess += r; | 1214 | rightExcess += r; |
1145 | } | 1215 | } |
1216 | #elif defined (iPlatformMobile) | ||
1217 | /* Reserve space for the keyboard. */ | ||
1218 | bottomExcess += get_MainWindow()->keyboardHeight; | ||
1146 | #endif | 1219 | #endif |
1147 | if (!allowOverflow) { | 1220 | if (!allowOverflow) { |
1148 | if (bottomExcess > 0 && (!isPortraitPhone || !isSlidePanel)) { | 1221 | if (bottomExcess > 0 && (!isPortraitPhone || !isSlidePanel)) { |
@@ -1203,6 +1276,15 @@ iLabelWidget *findMenuItem_Widget(iWidget *menu, const char *command) { | |||
1203 | return NULL; | 1276 | return NULL; |
1204 | } | 1277 | } |
1205 | 1278 | ||
1279 | iWidget *findUserData_Widget(iWidget *d, void *userData) { | ||
1280 | iForEach(ObjectList, i, children_Widget(d)) { | ||
1281 | if (userData_Object(i.object) == userData) { | ||
1282 | return i.object; | ||
1283 | } | ||
1284 | } | ||
1285 | return NULL; | ||
1286 | } | ||
1287 | |||
1206 | void setMenuItemDisabled_Widget(iWidget *menu, const char *command, iBool disable) { | 1288 | void setMenuItemDisabled_Widget(iWidget *menu, const char *command, iBool disable) { |
1207 | if (flags_Widget(menu) & nativeMenu_WidgetFlag) { | 1289 | if (flags_Widget(menu) & nativeMenu_WidgetFlag) { |
1208 | setDisabled_NativeMenuItem(findNativeMenuItem_Widget(menu, command), disable); | 1290 | setDisabled_NativeMenuItem(findNativeMenuItem_Widget(menu, command), disable); |
@@ -1269,6 +1351,12 @@ const iString *removeMenuItemLabelPrefixes_String(const iString *d) { | |||
1269 | return collect_String(str); | 1351 | return collect_String(str); |
1270 | } | 1352 | } |
1271 | 1353 | ||
1354 | static const iString *replaceNewlinesWithDash_(const iString *str) { | ||
1355 | iString *mod = copy_String(str); | ||
1356 | replace_String(mod, "\n", " "); | ||
1357 | return collect_String(mod); | ||
1358 | } | ||
1359 | |||
1272 | void updateDropdownSelection_LabelWidget(iLabelWidget *dropButton, const char *selectedCommand) { | 1360 | void updateDropdownSelection_LabelWidget(iLabelWidget *dropButton, const char *selectedCommand) { |
1273 | if (!dropButton) { | 1361 | if (!dropButton) { |
1274 | return; | 1362 | return; |
@@ -1279,8 +1367,9 @@ void updateDropdownSelection_LabelWidget(iLabelWidget *dropButton, const char *s | |||
1279 | iMenuItem *item = findNativeMenuItem_Widget(menu, selectedCommand); | 1367 | iMenuItem *item = findNativeMenuItem_Widget(menu, selectedCommand); |
1280 | if (item) { | 1368 | if (item) { |
1281 | setSelected_NativeMenuItem(item, iTrue); | 1369 | setSelected_NativeMenuItem(item, iTrue); |
1282 | updateText_LabelWidget( | 1370 | updateText_LabelWidget(dropButton, |
1283 | dropButton, removeMenuItemLabelPrefixes_String(collectNewCStr_String(item->label))); | 1371 | replaceNewlinesWithDash_(removeMenuItemLabelPrefixes_String( |
1372 | collectNewCStr_String(item->label)))); | ||
1284 | checkIcon_LabelWidget(dropButton); | 1373 | checkIcon_LabelWidget(dropButton); |
1285 | } | 1374 | } |
1286 | return; | 1375 | return; |
@@ -1291,7 +1380,8 @@ void updateDropdownSelection_LabelWidget(iLabelWidget *dropButton, const char *s | |||
1291 | const iBool isSelected = endsWith_String(command_LabelWidget(item), selectedCommand); | 1380 | const iBool isSelected = endsWith_String(command_LabelWidget(item), selectedCommand); |
1292 | setFlags_Widget(as_Widget(item), selected_WidgetFlag, isSelected); | 1381 | setFlags_Widget(as_Widget(item), selected_WidgetFlag, isSelected); |
1293 | if (isSelected) { | 1382 | if (isSelected) { |
1294 | updateText_LabelWidget(dropButton, sourceText_LabelWidget(item)); | 1383 | updateText_LabelWidget(dropButton, |
1384 | replaceNewlinesWithDash_(text_LabelWidget(item))); | ||
1295 | checkIcon_LabelWidget(dropButton); | 1385 | checkIcon_LabelWidget(dropButton); |
1296 | } | 1386 | } |
1297 | } | 1387 | } |
@@ -1342,7 +1432,7 @@ static iBool tabSwitcher_(iWidget *tabs, const char *cmd) { | |||
1342 | if (equal_Command(cmd, "tabs.switch")) { | 1432 | if (equal_Command(cmd, "tabs.switch")) { |
1343 | iWidget *target = pointerLabel_Command(cmd, "page"); | 1433 | iWidget *target = pointerLabel_Command(cmd, "page"); |
1344 | if (!target) { | 1434 | if (!target) { |
1345 | target = findChild_Widget(tabs, cstr_Rangecc(range_Command(cmd, "id"))); | 1435 | target = findChild_Widget(tabs, cstr_Command(cmd, "id")); |
1346 | } | 1436 | } |
1347 | if (!target) return iFalse; | 1437 | if (!target) return iFalse; |
1348 | unfocusFocusInsideTabPage_(currentTabPage_Widget(tabs)); | 1438 | unfocusFocusInsideTabPage_(currentTabPage_Widget(tabs)); |
@@ -1377,7 +1467,7 @@ static iBool tabSwitcher_(iWidget *tabs, const char *cmd) { | |||
1377 | iWidget *nextTabs = findChild_Widget(otherRoot_Window(get_Window(), tabs->root)->widget, | 1467 | iWidget *nextTabs = findChild_Widget(otherRoot_Window(get_Window(), tabs->root)->widget, |
1378 | "doctabs"); | 1468 | "doctabs"); |
1379 | iWidget *nextPages = findChild_Widget(nextTabs, "tabs.pages"); | 1469 | iWidget *nextPages = findChild_Widget(nextTabs, "tabs.pages"); |
1380 | tabIndex = (dir < 0 ? childCount_Widget(nextPages) - 1 : 0); | 1470 | tabIndex = (int) (dir < 0 ? childCount_Widget(nextPages) - 1 : 0); |
1381 | showTabPage_Widget(nextTabs, child_Widget(nextPages, tabIndex)); | 1471 | showTabPage_Widget(nextTabs, child_Widget(nextPages, tabIndex)); |
1382 | postCommand_App("keyroot.next"); | 1472 | postCommand_App("keyroot.next"); |
1383 | } | 1473 | } |
@@ -1602,6 +1692,22 @@ void useSheetStyle_Widget(iWidget *d) { | |||
1602 | iTrue); | 1692 | iTrue); |
1603 | } | 1693 | } |
1604 | 1694 | ||
1695 | static iLabelWidget *addDialogTitle_(iWidget *dlg, const char *text, const char *id) { | ||
1696 | iLabelWidget *label = new_LabelWidget(text, NULL); | ||
1697 | addChildFlags_Widget(dlg, iClob(label), alignLeft_WidgetFlag | frameless_WidgetFlag | | ||
1698 | resizeToParentWidth_WidgetFlag); | ||
1699 | setAllCaps_LabelWidget(label, iTrue); | ||
1700 | setTextColor_LabelWidget(label, uiHeading_ColorId); | ||
1701 | if (id) { | ||
1702 | setId_Widget(as_Widget(label), id); | ||
1703 | } | ||
1704 | return label; | ||
1705 | } | ||
1706 | |||
1707 | iLabelWidget *addDialogTitle_Widget(iWidget *dlg, const char *text, const char *idOrNull) { | ||
1708 | return addDialogTitle_(dlg, text, idOrNull); | ||
1709 | } | ||
1710 | |||
1605 | static void acceptValueInput_(iWidget *dlg) { | 1711 | static void acceptValueInput_(iWidget *dlg) { |
1606 | const iInputWidget *input = findChild_Widget(dlg, "input"); | 1712 | const iInputWidget *input = findChild_Widget(dlg, "input"); |
1607 | if (!isEmpty_String(id_Widget(dlg))) { | 1713 | if (!isEmpty_String(id_Widget(dlg))) { |
@@ -1613,7 +1719,7 @@ static void acceptValueInput_(iWidget *dlg) { | |||
1613 | } | 1719 | } |
1614 | } | 1720 | } |
1615 | 1721 | ||
1616 | static void updateValueInputWidth_(iWidget *dlg) { | 1722 | static void updateValueInputSizing_(iWidget *dlg) { |
1617 | const iRect safeRoot = safeRect_Root(dlg->root); | 1723 | const iRect safeRoot = safeRect_Root(dlg->root); |
1618 | const iInt2 rootSize = safeRoot.size; | 1724 | const iInt2 rootSize = safeRoot.size; |
1619 | iWidget * title = findChild_Widget(dlg, "valueinput.title"); | 1725 | iWidget * title = findChild_Widget(dlg, "valueinput.title"); |
@@ -1623,18 +1729,19 @@ static void updateValueInputWidth_(iWidget *dlg) { | |||
1623 | } | 1729 | } |
1624 | else { | 1730 | else { |
1625 | dlg->rect.size.x = | 1731 | dlg->rect.size.x = |
1626 | iMin(rootSize.x, iMaxi(iMaxi(100 * gap_UI, title->rect.size.x), prompt->rect.size.x)); | 1732 | iMin(rootSize.x, iMaxi(iMaxi(100 * gap_UI, title ? title->rect.size.x : 0), |
1733 | prompt->rect.size.x)); | ||
1627 | } | 1734 | } |
1628 | /* Adjust the maximum number of visible lines. */ | 1735 | /* Adjust the maximum number of visible lines. */ |
1629 | int footer = 6 * gap_UI + get_MainWindow()->keyboardHeight; | 1736 | int footer = 6 * gap_UI; |
1630 | iWidget *buttons = findChild_Widget(dlg, "dialogbuttons"); | 1737 | iWidget *buttons = findChild_Widget(dlg, "dialogbuttons"); |
1631 | if (buttons) { | 1738 | if (buttons && deviceType_App() == desktop_AppDeviceType) { |
1632 | footer += height_Widget(buttons); | 1739 | footer += height_Widget(buttons); |
1633 | } | 1740 | } |
1634 | iInputWidget *input = findChild_Widget(dlg, "input"); | 1741 | iInputWidget *input = findChild_Widget(dlg, "input"); |
1635 | setLineLimits_InputWidget(input, | 1742 | setLineLimits_InputWidget(input, |
1636 | 1, | 1743 | 1, |
1637 | (bottom_Rect(safeRect_Root(dlg->root)) - footer - | 1744 | (bottom_Rect(visibleRect_Root(dlg->root)) - footer - |
1638 | top_Rect(boundsWithoutVisualOffset_Widget(as_Widget(input)))) / | 1745 | top_Rect(boundsWithoutVisualOffset_Widget(as_Widget(input)))) / |
1639 | lineHeight_Text(font_InputWidget(input))); | 1746 | lineHeight_Text(font_InputWidget(input))); |
1640 | } | 1747 | } |
@@ -1643,11 +1750,17 @@ iBool valueInputHandler_(iWidget *dlg, const char *cmd) { | |||
1643 | iWidget *ptr = as_Widget(pointer_Command(cmd)); | 1750 | iWidget *ptr = as_Widget(pointer_Command(cmd)); |
1644 | if (equal_Command(cmd, "window.resized") || equal_Command(cmd, "keyboard.changed")) { | 1751 | if (equal_Command(cmd, "window.resized") || equal_Command(cmd, "keyboard.changed")) { |
1645 | if (isVisible_Widget(dlg)) { | 1752 | if (isVisible_Widget(dlg)) { |
1646 | updateValueInputWidth_(dlg); | 1753 | updateValueInputSizing_(dlg); |
1647 | arrange_Widget(dlg); | 1754 | arrange_Widget(dlg); |
1648 | } | 1755 | } |
1649 | return iFalse; | 1756 | return iFalse; |
1650 | } | 1757 | } |
1758 | if (equal_Command(cmd, "input.resized")) { | ||
1759 | /* BUG: A single arrange here is not sufficient, leaving a big gap between prompt and input. Why? */ | ||
1760 | arrange_Widget(dlg); | ||
1761 | arrange_Widget(dlg); | ||
1762 | return iTrue; | ||
1763 | } | ||
1651 | if (equal_Command(cmd, "input.ended")) { | 1764 | if (equal_Command(cmd, "input.ended")) { |
1652 | if (argLabel_Command(cmd, "enter") && hasParent_Widget(ptr, dlg)) { | 1765 | if (argLabel_Command(cmd, "enter") && hasParent_Widget(ptr, dlg)) { |
1653 | if (arg_Command(cmd)) { | 1766 | if (arg_Command(cmd)) { |
@@ -1663,6 +1776,12 @@ iBool valueInputHandler_(iWidget *dlg, const char *cmd) { | |||
1663 | } | 1776 | } |
1664 | return iFalse; | 1777 | return iFalse; |
1665 | } | 1778 | } |
1779 | else if (equal_Command(cmd, "valueinput.set")) { | ||
1780 | iInputWidget *input = findChild_Widget(dlg, "input"); | ||
1781 | setTextCStr_InputWidget(input, suffixPtr_Command(cmd, "text")); | ||
1782 | validate_InputWidget(input); | ||
1783 | return iTrue; | ||
1784 | } | ||
1666 | else if (equal_Command(cmd, "valueinput.cancel")) { | 1785 | else if (equal_Command(cmd, "valueinput.cancel")) { |
1667 | postCommandf_App("valueinput.cancelled id:%s", cstr_String(id_Widget(dlg))); | 1786 | postCommandf_App("valueinput.cancelled id:%s", cstr_String(id_Widget(dlg))); |
1668 | setId_Widget(dlg, ""); /* no further commands to emit */ | 1787 | setId_Widget(dlg, ""); /* no further commands to emit */ |
@@ -1699,9 +1818,9 @@ iWidget *makeDialogButtons_Widget(const iMenuItem *actions, size_t numActions) { | |||
1699 | addChildFlags_Widget(div, iClob(new_Widget()), expand_WidgetFlag); | 1818 | addChildFlags_Widget(div, iClob(new_Widget()), expand_WidgetFlag); |
1700 | } | 1819 | } |
1701 | int fonts[2] = { uiLabel_FontId, uiLabelBold_FontId }; | 1820 | int fonts[2] = { uiLabel_FontId, uiLabelBold_FontId }; |
1702 | if (deviceType_App() == phone_AppDeviceType) { | 1821 | if (deviceType_App() != desktop_AppDeviceType) { |
1703 | fonts[0] = uiLabelMedium_FontId; | 1822 | fonts[0] = uiLabelBig_FontId; |
1704 | fonts[1] = uiLabelMediumBold_FontId; | 1823 | fonts[1] = uiLabelBigBold_FontId; |
1705 | } | 1824 | } |
1706 | for (size_t i = 0; i < numActions; i++) { | 1825 | for (size_t i = 0; i < numActions; i++) { |
1707 | const char *label = actions[i].label; | 1826 | const char *label = actions[i].label; |
@@ -1743,6 +1862,10 @@ iWidget *makeDialogButtons_Widget(const iMenuItem *actions, size_t numActions) { | |||
1743 | setId_Widget(as_Widget(button), "default"); | 1862 | setId_Widget(as_Widget(button), "default"); |
1744 | } | 1863 | } |
1745 | setFlags_Widget(as_Widget(button), alignLeft_WidgetFlag | drawKey_WidgetFlag, isDefault); | 1864 | setFlags_Widget(as_Widget(button), alignLeft_WidgetFlag | drawKey_WidgetFlag, isDefault); |
1865 | if (deviceType_App() != desktop_AppDeviceType) { | ||
1866 | setFlags_Widget(as_Widget(button), frameless_WidgetFlag | noBackground_WidgetFlag, iTrue); | ||
1867 | setTextColor_LabelWidget(button, uiTextAction_ColorId); | ||
1868 | } | ||
1746 | setFont_LabelWidget(button, isDefault ? fonts[1] : fonts[0]); | 1869 | setFont_LabelWidget(button, isDefault ? fonts[1] : fonts[0]); |
1747 | } | 1870 | } |
1748 | return div; | 1871 | return div; |
@@ -1758,9 +1881,9 @@ iWidget *makeValueInput_Widget(iWidget *parent, const iString *initialValue, con | |||
1758 | if (parent) { | 1881 | if (parent) { |
1759 | addChild_Widget(parent, iClob(dlg)); | 1882 | addChild_Widget(parent, iClob(dlg)); |
1760 | } | 1883 | } |
1761 | setId_Widget( | 1884 | if (deviceType_App() == desktop_AppDeviceType) { /* conserve space on mobile */ |
1762 | addChildFlags_Widget(dlg, iClob(new_LabelWidget(title, NULL)), frameless_WidgetFlag), | 1885 | addDialogTitle_(dlg, title, "valueinput.title"); |
1763 | "valueinput.title"); | 1886 | } |
1764 | iLabelWidget *promptLabel; | 1887 | iLabelWidget *promptLabel; |
1765 | setId_Widget(addChildFlags_Widget( | 1888 | setId_Widget(addChildFlags_Widget( |
1766 | dlg, iClob(promptLabel = new_LabelWidget(prompt, NULL)), frameless_WidgetFlag | 1889 | dlg, iClob(promptLabel = new_LabelWidget(prompt, NULL)), frameless_WidgetFlag |
@@ -1780,27 +1903,38 @@ iWidget *makeValueInput_Widget(iWidget *parent, const iString *initialValue, con | |||
1780 | } | 1903 | } |
1781 | setId_Widget(as_Widget(input), "input"); | 1904 | setId_Widget(as_Widget(input), "input"); |
1782 | addChild_Widget(dlg, iClob(makePadding_Widget(gap_UI))); | 1905 | addChild_Widget(dlg, iClob(makePadding_Widget(gap_UI))); |
1783 | addChild_Widget(dlg, | 1906 | /* On mobile, the actions are laid out a bit differently: buttons on top, on opposite edges. */ |
1784 | iClob(makeDialogButtons_Widget( | 1907 | iArray actions; |
1785 | (iMenuItem[]){ { "${cancel}", SDLK_ESCAPE, 0, "valueinput.cancel" }, | 1908 | init_Array(&actions, sizeof(iMenuItem)); |
1786 | { acceptLabel, | 1909 | pushBack_Array(&actions, &(iMenuItem){ "${cancel}", SDLK_ESCAPE, 0, "valueinput.cancel" }); |
1910 | if (deviceType_App() != desktop_AppDeviceType) { | ||
1911 | pushBack_Array(&actions, &(iMenuItem){ "---" }); | ||
1912 | } | ||
1913 | pushBack_Array(&actions, &(iMenuItem){ | ||
1914 | acceptLabel, | ||
1787 | SDLK_RETURN, | 1915 | SDLK_RETURN, |
1788 | acceptKeyMod_ReturnKeyBehavior(prefs_App()->returnKey), | 1916 | acceptKeyMod_ReturnKeyBehavior(prefs_App()->returnKey), |
1789 | "valueinput.accept" } }, | 1917 | "valueinput.accept" |
1790 | 2))); | 1918 | }); |
1791 | // finalizeSheet_Mobile(dlg); | 1919 | addChildPos_Widget(dlg, |
1920 | iClob(makeDialogButtons_Widget(constData_Array(&actions), | ||
1921 | size_Array(&actions))), | ||
1922 | deviceType_App() != desktop_AppDeviceType ? | ||
1923 | front_WidgetAddPos : back_WidgetAddPos); | ||
1924 | deinit_Array(&actions); | ||
1792 | arrange_Widget(dlg); | 1925 | arrange_Widget(dlg); |
1793 | if (parent) { | 1926 | if (parent) { |
1794 | setFocus_Widget(as_Widget(input)); | 1927 | setFocus_Widget(as_Widget(input)); |
1795 | } | 1928 | } |
1796 | /* Check that the top is in the safe area. */ { | 1929 | /* Check that the top is in the safe area. */ |
1797 | int top = top_Rect(bounds_Widget(dlg)); | 1930 | if (deviceType_App() != desktop_AppDeviceType) { |
1931 | int top = top_Rect(boundsWithoutVisualOffset_Widget(dlg)); | ||
1798 | int delta = top - top_Rect(safeRect_Root(dlg->root)); | 1932 | int delta = top - top_Rect(safeRect_Root(dlg->root)); |
1799 | if (delta < 0) { | 1933 | if (delta < 0) { |
1800 | dlg->rect.pos.y -= delta; | 1934 | dlg->rect.pos.y -= delta; |
1801 | } | 1935 | } |
1802 | } | 1936 | } |
1803 | updateValueInputWidth_(dlg); | 1937 | updateValueInputSizing_(dlg); |
1804 | setupSheetTransition_Mobile(dlg, incoming_TransitionFlag | top_TransitionDir); | 1938 | setupSheetTransition_Mobile(dlg, incoming_TransitionFlag | top_TransitionDir); |
1805 | return dlg; | 1939 | return dlg; |
1806 | } | 1940 | } |
@@ -1808,7 +1942,7 @@ iWidget *makeValueInput_Widget(iWidget *parent, const iString *initialValue, con | |||
1808 | void updateValueInput_Widget(iWidget *d, const char *title, const char *prompt) { | 1942 | void updateValueInput_Widget(iWidget *d, const char *title, const char *prompt) { |
1809 | setTextCStr_LabelWidget(findChild_Widget(d, "valueinput.title"), title); | 1943 | setTextCStr_LabelWidget(findChild_Widget(d, "valueinput.title"), title); |
1810 | setTextCStr_LabelWidget(findChild_Widget(d, "valueinput.prompt"), prompt); | 1944 | setTextCStr_LabelWidget(findChild_Widget(d, "valueinput.prompt"), prompt); |
1811 | updateValueInputWidth_(d); | 1945 | updateValueInputSizing_(d); |
1812 | } | 1946 | } |
1813 | 1947 | ||
1814 | static void updateQuestionWidth_(iWidget *dlg) { | 1948 | static void updateQuestionWidth_(iWidget *dlg) { |
@@ -1894,9 +2028,7 @@ iWidget *makeQuestion_Widget(const char *title, const char *msg, | |||
1894 | } | 2028 | } |
1895 | iWidget *dlg = makeSheet_Widget(""); | 2029 | iWidget *dlg = makeSheet_Widget(""); |
1896 | setCommandHandler_Widget(dlg, messageHandler_); | 2030 | setCommandHandler_Widget(dlg, messageHandler_); |
1897 | setId_Widget( | 2031 | addDialogTitle_(dlg, title, "question.title"); |
1898 | addChildFlags_Widget(dlg, iClob(new_LabelWidget(title, NULL)), frameless_WidgetFlag), | ||
1899 | "question.title"); | ||
1900 | iLabelWidget *msgLabel; | 2032 | iLabelWidget *msgLabel; |
1901 | setId_Widget(addChildFlags_Widget(dlg, | 2033 | setId_Widget(addChildFlags_Widget(dlg, |
1902 | iClob(msgLabel = new_LabelWidget(msg, NULL)), | 2034 | iClob(msgLabel = new_LabelWidget(msg, NULL)), |
@@ -2021,9 +2153,11 @@ iWidget *appendTwoColumnTabPage_Widget(iWidget *tabs, const char *title, int sho | |||
2021 | } | 2153 | } |
2022 | 2154 | ||
2023 | static void makeTwoColumnHeading_(const char *title, iWidget *headings, iWidget *values) { | 2155 | static void makeTwoColumnHeading_(const char *title, iWidget *headings, iWidget *values) { |
2024 | addChildFlags_Widget(headings, | 2156 | setFont_LabelWidget(addChildFlags_Widget(headings, |
2025 | iClob(makeHeading_Widget(format_CStr(uiHeading_ColorEscape "%s", title))), | 2157 | iClob(makeHeading_Widget( |
2026 | ignoreForParentWidth_WidgetFlag); | 2158 | format_CStr(uiHeading_ColorEscape "%s", title))), |
2159 | ignoreForParentWidth_WidgetFlag), | ||
2160 | uiLabelBold_FontId); | ||
2027 | addChild_Widget(values, iClob(makeHeading_Widget(""))); | 2161 | addChild_Widget(values, iClob(makeHeading_Widget(""))); |
2028 | } | 2162 | } |
2029 | 2163 | ||
@@ -2071,28 +2205,6 @@ static const iArray *makeFontItems_(const char *id) { | |||
2071 | 0, | 2205 | 0, |
2072 | format_CStr("!font.set %s:%s", id, cstr_String(&spec->id)) }); | 2206 | format_CStr("!font.set %s:%s", id, cstr_String(&spec->id)) }); |
2073 | } | 2207 | } |
2074 | #if 0 | ||
2075 | const struct { | ||
2076 | const char * name; | ||
2077 | enum iTextFont cfgId; | ||
2078 | } fonts[] = { { "Nunito", nunito_TextFont }, | ||
2079 | { "Source Sans 3", sourceSans3_TextFont }, | ||
2080 | { "Fira Sans", firaSans_TextFont }, | ||
2081 | { "---", -1 }, | ||
2082 | { "Literata", literata_TextFont }, | ||
2083 | { "Tinos", tinos_TextFont }, | ||
2084 | { "---", -1 }, | ||
2085 | { "Iosevka", iosevka_TextFont } }; | ||
2086 | iForIndices(i, fonts) { | ||
2087 | pushBack_Array(items, | ||
2088 | &(iMenuItem){ fonts[i].name, | ||
2089 | 0, | ||
2090 | 0, | ||
2091 | fonts[i].cfgId >= 0 | ||
2092 | ? format_CStr("!%s.set arg:%d", id, fonts[i].cfgId) | ||
2093 | : NULL }); | ||
2094 | } | ||
2095 | #endif | ||
2096 | pushBack_Array(items, &(iMenuItem){ NULL }); /* terminator */ | 2208 | pushBack_Array(items, &(iMenuItem){ NULL }); /* terminator */ |
2097 | return items; | 2209 | return items; |
2098 | } | 2210 | } |
@@ -2108,13 +2220,6 @@ static void addFontButtons_(iWidget *parent, const char *id) { | |||
2108 | addChildFlags_Widget(parent, iClob(button), alignLeft_WidgetFlag); | 2220 | addChildFlags_Widget(parent, iClob(button), alignLeft_WidgetFlag); |
2109 | } | 2221 | } |
2110 | 2222 | ||
2111 | #if 0 | ||
2112 | static int cmp_MenuItem_(const void *e1, const void *e2) { | ||
2113 | const iMenuItem *a = e1, *b = e2; | ||
2114 | return iCmpStr(a->label, b->label); | ||
2115 | } | ||
2116 | #endif | ||
2117 | |||
2118 | void updatePreferencesLayout_Widget(iWidget *prefs) { | 2223 | void updatePreferencesLayout_Widget(iWidget *prefs) { |
2119 | if (!prefs || deviceType_App() != desktop_AppDeviceType) { | 2224 | if (!prefs || deviceType_App() != desktop_AppDeviceType) { |
2120 | return; | 2225 | return; |
@@ -2299,6 +2404,15 @@ iWidget *makePreferences_Widget(void) { | |||
2299 | format_CStr("returnkey.set arg:%d", acceptWithPrimaryMod_ReturnKeyBehavior) }, | 2404 | format_CStr("returnkey.set arg:%d", acceptWithPrimaryMod_ReturnKeyBehavior) }, |
2300 | { NULL } | 2405 | { NULL } |
2301 | }; | 2406 | }; |
2407 | iMenuItem toolbarActionItems[2][max_ToolbarAction]; | ||
2408 | iZap(toolbarActionItems); | ||
2409 | for (int j = 0; j < 2; j++) { | ||
2410 | for (int i = 0; i < sidebar_ToolbarAction; i++) { | ||
2411 | toolbarActionItems[j][i].label = toolbarActions_Mobile[i].label; | ||
2412 | toolbarActionItems[j][i].command = | ||
2413 | format_CStr("toolbar.action.set arg:%d button:%d", i, j); | ||
2414 | } | ||
2415 | } | ||
2302 | iMenuItem docThemes[2][max_GmDocumentTheme + 1]; | 2416 | iMenuItem docThemes[2][max_GmDocumentTheme + 1]; |
2303 | for (int i = 0; i < 2; ++i) { | 2417 | for (int i = 0; i < 2; ++i) { |
2304 | const iBool isDark = (i == 0); | 2418 | const iBool isDark = (i == 0); |
@@ -2390,24 +2504,27 @@ iWidget *makePreferences_Widget(void) { | |||
2390 | }; | 2504 | }; |
2391 | const iMenuItem uiPanelItems[] = { | 2505 | const iMenuItem uiPanelItems[] = { |
2392 | { "title id:heading.prefs.interface" }, | 2506 | { "title id:heading.prefs.interface" }, |
2393 | { "dropdown device:1 id:prefs.returnkey", 0, 0, (const void *) returnKeyBehaviors }, | 2507 | { "dropdown device:0 id:prefs.returnkey", 0, 0, (const void *) returnKeyBehaviors }, |
2394 | { "padding device:1" }, | 2508 | { "padding device:1" }, |
2395 | { "toggle id:prefs.hoverlink" }, | ||
2396 | { "toggle device:2 id:prefs.hidetoolbarscroll" }, | 2509 | { "toggle device:2 id:prefs.hidetoolbarscroll" }, |
2510 | { "heading device:2 id:heading.prefs.toolbaractions" }, | ||
2511 | { "dropdown device:2 id:prefs.toolbaraction1", 0, 0, (const void *) toolbarActionItems[0] }, | ||
2512 | { "dropdown device:2 id:prefs.toolbaraction2", 0, 0, (const void *) toolbarActionItems[1] }, | ||
2397 | { "heading id:heading.prefs.sizing" }, | 2513 | { "heading id:heading.prefs.sizing" }, |
2398 | { "input id:prefs.uiscale maxlen:8" }, | 2514 | { "input id:prefs.uiscale maxlen:8" }, |
2399 | { NULL } | 2515 | { NULL } |
2400 | }; | 2516 | }; |
2401 | const iMenuItem colorPanelItems[] = { | 2517 | const iMenuItem colorPanelItems[] = { |
2402 | { "title id:heading.prefs.colors" }, | 2518 | { "title id:heading.prefs.colors" }, |
2403 | { "heading id:heading.prefs.uitheme" }, | 2519 | #if !defined (iPlatformAndroidMobile) |
2404 | { "toggle id:prefs.ostheme" }, | 2520 | { "toggle id:prefs.ostheme" }, |
2521 | #endif | ||
2405 | { "radio id:prefs.theme", 0, 0, (const void *) themeItems }, | 2522 | { "radio id:prefs.theme", 0, 0, (const void *) themeItems }, |
2406 | { "radio id:prefs.accent", 0, 0, (const void *) accentItems }, | 2523 | { "radio id:prefs.accent", 0, 0, (const void *) accentItems }, |
2407 | { "heading id:heading.prefs.pagecontent" }, | 2524 | { "heading id:heading.prefs.pagecontent" }, |
2408 | { "dropdown id:prefs.doctheme.dark", 0, 0, (const void *) docThemes[0] }, | 2525 | { "dropdown id:prefs.doctheme.dark", 0, 0, (const void *) docThemes[0] }, |
2409 | { "dropdown id:prefs.doctheme.light", 0, 0, (const void *) docThemes[1] }, | 2526 | { "dropdown id:prefs.doctheme.light", 0, 0, (const void *) docThemes[1] }, |
2410 | { "radio id:prefs.saturation", 0, 0, (const void *) satItems }, | 2527 | { "radio horizontal:1 id:prefs.saturation", 0, 0, (const void *) satItems }, |
2411 | { "padding" }, | 2528 | { "padding" }, |
2412 | { "dropdown id:prefs.imagestyle", 0, 0, (const void *) imgStyles }, | 2529 | { "dropdown id:prefs.imagestyle", 0, 0, (const void *) imgStyles }, |
2413 | { NULL } | 2530 | { NULL } |
@@ -2418,18 +2535,23 @@ iWidget *makePreferences_Widget(void) { | |||
2418 | { "dropdown id:prefs.font.body", 0, 0, (const void *) constData_Array(makeFontItems_("body")) }, | 2535 | { "dropdown id:prefs.font.body", 0, 0, (const void *) constData_Array(makeFontItems_("body")) }, |
2419 | { "dropdown id:prefs.font.mono", 0, 0, (const void *) constData_Array(makeFontItems_("mono")) }, | 2536 | { "dropdown id:prefs.font.mono", 0, 0, (const void *) constData_Array(makeFontItems_("mono")) }, |
2420 | { "buttons id:prefs.mono", 0, 0, (const void *) monoFontItems }, | 2537 | { "buttons id:prefs.mono", 0, 0, (const void *) monoFontItems }, |
2421 | { "dropdown id:prefs.font.monodoc", 0, 0, (const void *) constData_Array(makeFontItems_("monodoc")) }, | ||
2422 | { "padding" }, | 2538 | { "padding" }, |
2423 | { "toggle id:prefs.font.smooth" }, | 2539 | { "dropdown id:prefs.font.monodoc", 0, 0, (const void *) constData_Array(makeFontItems_("monodoc")) }, |
2424 | { "padding" }, | 2540 | { "padding" }, |
2425 | { "dropdown id:prefs.font.ui", 0, 0, (const void *) constData_Array(makeFontItems_("ui")) }, | 2541 | { "toggle id:prefs.font.warnmissing" }, |
2542 | { "heading id:prefs.gemtext.ansi" }, | ||
2543 | { "toggle id:prefs.gemtext.ansi.fg" }, | ||
2544 | { "toggle id:prefs.gemtext.ansi.bg" }, | ||
2545 | { "toggle id:prefs.gemtext.ansi.fontstyle" }, | ||
2546 | // { "padding" }, | ||
2547 | // { "dropdown id:prefs.font.ui", 0, 0, (const void *) constData_Array(makeFontItems_("ui")) }, | ||
2426 | { "padding" }, | 2548 | { "padding" }, |
2427 | { "button text:" fontpack_Icon " ${menu.fonts}", 0, 0, "!open url:about:fonts" }, | 2549 | { "button text:" fontpack_Icon " " uiTextAction_ColorEscape "${menu.fonts}", 0, 0, "!open url:about:fonts" }, |
2428 | { NULL } | 2550 | { NULL } |
2429 | }; | 2551 | }; |
2430 | const iMenuItem stylePanelItems[] = { | 2552 | const iMenuItem stylePanelItems[] = { |
2431 | { "title id:heading.prefs.style" }, | 2553 | { "title id:heading.prefs.style" }, |
2432 | { "radio id:prefs.linewidth", 0, 0, (const void *) lineWidthItems }, | 2554 | { "radio horizontal:1 id:prefs.linewidth", 0, 0, (const void *) lineWidthItems }, |
2433 | { "padding" }, | 2555 | { "padding" }, |
2434 | { "input id:prefs.linespacing maxlen:5" }, | 2556 | { "input id:prefs.linespacing maxlen:5" }, |
2435 | { "radio id:prefs.quoteicon", 0, 0, (const void *) quoteItems }, | 2557 | { "radio id:prefs.quoteicon", 0, 0, (const void *) quoteItems }, |
@@ -2458,6 +2580,7 @@ iWidget *makePreferences_Widget(void) { | |||
2458 | }; | 2580 | }; |
2459 | const iMenuItem identityPanelItems[] = { | 2581 | const iMenuItem identityPanelItems[] = { |
2460 | { "title id:sidebar.identities" }, | 2582 | { "title id:sidebar.identities" }, |
2583 | { "certlist" }, | ||
2461 | { NULL } | 2584 | { NULL } |
2462 | }; | 2585 | }; |
2463 | iString *aboutText = collectNew_String(); { | 2586 | iString *aboutText = collectNew_String(); { |
@@ -2466,6 +2589,10 @@ iWidget *makePreferences_Widget(void) { | |||
2466 | appendFormat_String(aboutText, " (" LAGRANGE_IOS_VERSION ") %s" LAGRANGE_IOS_BUILD_DATE, | 2589 | appendFormat_String(aboutText, " (" LAGRANGE_IOS_VERSION ") %s" LAGRANGE_IOS_BUILD_DATE, |
2467 | escape_Color(uiTextDim_ColorId)); | 2590 | escape_Color(uiTextDim_ColorId)); |
2468 | #endif | 2591 | #endif |
2592 | #if defined (iPlatformAndroidMobile) | ||
2593 | appendFormat_String(aboutText, " (" LAGRANGE_ANDROID_VERSION ") %s" LAGRANGE_ANDROID_BUILD_DATE, | ||
2594 | escape_Color(uiTextDim_ColorId)); | ||
2595 | #endif | ||
2469 | } | 2596 | } |
2470 | const iMenuItem aboutPanelItems[] = { | 2597 | const iMenuItem aboutPanelItems[] = { |
2471 | { format_CStr("heading text:%s", cstr_String(aboutText)) }, | 2598 | { format_CStr("heading text:%s", cstr_String(aboutText)) }, |
@@ -2482,7 +2609,7 @@ iWidget *makePreferences_Widget(void) { | |||
2482 | { "title id:heading.settings" }, | 2609 | { "title id:heading.settings" }, |
2483 | { "panel text:" gear_Icon " ${heading.prefs.general}", 0, 0, (const void *) generalPanelItems }, | 2610 | { "panel text:" gear_Icon " ${heading.prefs.general}", 0, 0, (const void *) generalPanelItems }, |
2484 | { "panel icon:0x1f5a7 id:heading.prefs.network", 0, 0, (const void *) networkPanelItems }, | 2611 | { "panel icon:0x1f5a7 id:heading.prefs.network", 0, 0, (const void *) networkPanelItems }, |
2485 | { "panel text:" person_Icon " ${sidebar.identities}", 0, 0, (const void *) identityPanelItems }, | 2612 | { "panel noscroll:1 text:" person_Icon " ${sidebar.identities}", 0, 0, (const void *) identityPanelItems }, |
2486 | { "padding" }, | 2613 | { "padding" }, |
2487 | { "panel icon:0x1f4f1 id:heading.prefs.interface", 0, 0, (const void *) uiPanelItems }, | 2614 | { "panel icon:0x1f4f1 id:heading.prefs.interface", 0, 0, (const void *) uiPanelItems }, |
2488 | { "panel icon:0x1f3a8 id:heading.prefs.colors", 0, 0, (const void *) colorPanelItems }, | 2615 | { "panel icon:0x1f3a8 id:heading.prefs.colors", 0, 0, (const void *) colorPanelItems }, |
@@ -2498,9 +2625,7 @@ iWidget *makePreferences_Widget(void) { | |||
2498 | return dlg; | 2625 | return dlg; |
2499 | } | 2626 | } |
2500 | iWidget *dlg = makeSheet_Widget("prefs"); | 2627 | iWidget *dlg = makeSheet_Widget("prefs"); |
2501 | addChildFlags_Widget(dlg, | 2628 | addDialogTitle_(dlg, "${heading.prefs}", NULL); |
2502 | iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.prefs}", NULL)), | ||
2503 | frameless_WidgetFlag); | ||
2504 | iWidget *tabs = makeTabs_Widget(dlg); | 2629 | iWidget *tabs = makeTabs_Widget(dlg); |
2505 | setBackgroundColor_Widget(findChild_Widget(tabs, "tabs.buttons"), uiBackgroundSidebar_ColorId); | 2630 | setBackgroundColor_Widget(findChild_Widget(tabs, "tabs.buttons"), uiBackgroundSidebar_ColorId); |
2506 | setId_Widget(tabs, "prefs.tabs"); | 2631 | setId_Widget(tabs, "prefs.tabs"); |
@@ -2546,9 +2671,8 @@ iWidget *makePreferences_Widget(void) { | |||
2546 | } | 2671 | } |
2547 | /* User Interface. */ { | 2672 | /* User Interface. */ { |
2548 | appendTwoColumnTabPage_Widget(tabs, "${heading.prefs.interface}", '2', &headings, &values); | 2673 | appendTwoColumnTabPage_Widget(tabs, "${heading.prefs.interface}", '2', &headings, &values); |
2549 | #if defined (LAGRANGE_ENABLE_CUSTOM_FRAME) | 2674 | addDialogToggle_(headings, values, "${prefs.animate}", "prefs.animate"); |
2550 | addDialogToggle_(headings, values, "${prefs.customframe}", "prefs.customframe"); | 2675 | addDialogToggle_(headings, values, "${prefs.blink}", "prefs.blink"); |
2551 | #endif | ||
2552 | addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.returnkey}"))); | 2676 | addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.returnkey}"))); |
2553 | /* Return key behaviors. */ { | 2677 | /* Return key behaviors. */ { |
2554 | iLabelWidget *returnKey = makeMenuButton_LabelWidget( | 2678 | iLabelWidget *returnKey = makeMenuButton_LabelWidget( |
@@ -2562,7 +2686,9 @@ iWidget *makePreferences_Widget(void) { | |||
2562 | setId_Widget(addChildFlags_Widget(values, iClob(returnKey), alignLeft_WidgetFlag), | 2686 | setId_Widget(addChildFlags_Widget(values, iClob(returnKey), alignLeft_WidgetFlag), |
2563 | "prefs.returnkey"); | 2687 | "prefs.returnkey"); |
2564 | } | 2688 | } |
2565 | addDialogToggle_(headings, values, "${prefs.animate}", "prefs.animate"); | 2689 | #if defined (LAGRANGE_ENABLE_CUSTOM_FRAME) |
2690 | addDialogToggle_(headings, values, "${prefs.customframe}", "prefs.customframe"); | ||
2691 | #endif | ||
2566 | makeTwoColumnHeading_("${heading.prefs.scrolling}", headings, values); | 2692 | makeTwoColumnHeading_("${heading.prefs.scrolling}", headings, values); |
2567 | addDialogToggle_(headings, values, "${prefs.smoothscroll}", "prefs.smoothscroll"); | 2693 | addDialogToggle_(headings, values, "${prefs.smoothscroll}", "prefs.smoothscroll"); |
2568 | /* Scroll speeds. */ { | 2694 | /* Scroll speeds. */ { |
@@ -2827,7 +2953,7 @@ static iBool isBookmarkFolder_(void *context, const iBookmark *bm) { | |||
2827 | return isFolder_Bookmark(bm); | 2953 | return isFolder_Bookmark(bm); |
2828 | } | 2954 | } |
2829 | 2955 | ||
2830 | static const iArray *makeBookmarkFolderItems_(void) { | 2956 | static const iArray *makeBookmarkFolderItems_(iBool withNullTerminator) { |
2831 | iArray *folders = new_Array(sizeof(iMenuItem)); | 2957 | iArray *folders = new_Array(sizeof(iMenuItem)); |
2832 | pushBack_Array(folders, &(iMenuItem){ "\u2014", 0, 0, "dlg.bookmark.setfolder arg:0" }); | 2958 | pushBack_Array(folders, &(iMenuItem){ "\u2014", 0, 0, "dlg.bookmark.setfolder arg:0" }); |
2833 | iConstForEach( | 2959 | iConstForEach( |
@@ -2848,6 +2974,9 @@ static const iArray *makeBookmarkFolderItems_(void) { | |||
2848 | 0, | 2974 | 0, |
2849 | format_CStr("dlg.bookmark.setfolder arg:%u", id_Bookmark(bm)) }); | 2975 | format_CStr("dlg.bookmark.setfolder arg:%u", id_Bookmark(bm)) }); |
2850 | } | 2976 | } |
2977 | if (withNullTerminator) { | ||
2978 | pushBack_Array(folders, &(iMenuItem){ NULL }); | ||
2979 | } | ||
2851 | return collect_Array(folders); | 2980 | return collect_Array(folders); |
2852 | } | 2981 | } |
2853 | 2982 | ||
@@ -2856,40 +2985,37 @@ iWidget *makeBookmarkEditor_Widget(void) { | |||
2856 | { "${cancel}", 0, 0, "bmed.cancel" }, | 2985 | { "${cancel}", 0, 0, "bmed.cancel" }, |
2857 | { uiTextCaution_ColorEscape "${dlg.bookmark.save}", SDLK_RETURN, KMOD_PRIMARY, "bmed.accept" } | 2986 | { uiTextCaution_ColorEscape "${dlg.bookmark.save}", SDLK_RETURN, KMOD_PRIMARY, "bmed.accept" } |
2858 | }; | 2987 | }; |
2988 | iWidget *dlg = NULL; | ||
2859 | if (isUsingPanelLayout_Mobile()) { | 2989 | if (isUsingPanelLayout_Mobile()) { |
2990 | const iArray *folderItems = makeBookmarkFolderItems_(iTrue); | ||
2860 | const iMenuItem items[] = { | 2991 | const iMenuItem items[] = { |
2861 | { "title id:bmed.heading text:${heading.bookmark.edit}" }, | 2992 | { "title id:bmed.heading text:${heading.bookmark.edit}" }, |
2862 | { "heading id:dlg.bookmark.url" }, | 2993 | { "heading id:dlg.bookmark.url" }, |
2863 | { "input id:bmed.url url:1 noheading:1" }, | 2994 | { "input id:bmed.url url:1 noheading:1" }, |
2864 | { "padding" }, | 2995 | { "padding" }, |
2865 | { "input id:bmed.title text:${dlg.bookmark.title}" }, | 2996 | { "input id:bmed.title text:${dlg.bookmark.title}" }, |
2866 | { "input id:bmed.tags text:${dlg.bookmark.tags}" }, | 2997 | { "dropdown id:bmed.folder text:${dlg.bookmark.folder}", 0, 0, (const void *) constData_Array(folderItems) }, |
2998 | { "padding" }, | ||
2867 | { "input id:bmed.icon maxlen:1 text:${dlg.bookmark.icon}" }, | 2999 | { "input id:bmed.icon maxlen:1 text:${dlg.bookmark.icon}" }, |
3000 | { "input id:bmed.tags text:${dlg.bookmark.tags}" }, | ||
2868 | { "heading text:${heading.bookmark.tags}" }, | 3001 | { "heading text:${heading.bookmark.tags}" }, |
2869 | { "toggle id:bmed.tag.home text:${bookmark.tag.home}" }, | 3002 | { "toggle id:bmed.tag.home text:${bookmark.tag.home}" }, |
2870 | { "toggle id:bmed.tag.remote text:${bookmark.tag.remote}" }, | 3003 | { "toggle id:bmed.tag.remote text:${bookmark.tag.remote}" }, |
2871 | { "toggle id:bmed.tag.linksplit text:${bookmark.tag.linksplit}" }, | 3004 | { "toggle id:bmed.tag.linksplit text:${bookmark.tag.linksplit}" }, |
2872 | { NULL } | 3005 | { NULL } |
2873 | }; | 3006 | }; |
2874 | iWidget *dlg = makePanels_Mobile("bmed", items, actions, iElemCount(actions)); | 3007 | dlg = makePanels_Mobile("bmed", items, actions, iElemCount(actions)); |
2875 | setupSheetTransition_Mobile(dlg, iTrue); | 3008 | setupSheetTransition_Mobile(dlg, iTrue); |
2876 | return dlg; | ||
2877 | } | 3009 | } |
2878 | iWidget *dlg = makeSheet_Widget("bmed"); | 3010 | else { |
2879 | setId_Widget(addChildFlags_Widget( | 3011 | dlg = makeSheet_Widget("bmed"); |
2880 | dlg, | 3012 | addDialogTitle_(dlg, "${heading.bookmark.edit}", "bmed.heading"); |
2881 | iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.bookmark.edit}", NULL)), | ||
2882 | frameless_WidgetFlag), | ||
2883 | "bmed.heading"); | ||
2884 | iWidget *headings, *values; | 3013 | iWidget *headings, *values; |
2885 | addChild_Widget(dlg, iClob(makeTwoColumns_Widget(&headings, &values))); | 3014 | addChild_Widget(dlg, iClob(makeTwoColumns_Widget(&headings, &values))); |
2886 | iInputWidget *inputs[4]; | 3015 | iInputWidget *inputs[4]; |
2887 | addDialogInputWithHeading_(headings, values, "${dlg.bookmark.title}", "bmed.title", iClob(inputs[0] = new_InputWidget(0))); | ||
2888 | addDialogInputWithHeading_(headings, values, "${dlg.bookmark.url}", "bmed.url", iClob(inputs[1] = new_InputWidget(0))); | ||
2889 | setUrlContent_InputWidget(inputs[1], iTrue); | ||
2890 | /* Folder to add to. */ { | 3016 | /* Folder to add to. */ { |
2891 | addChild_Widget(headings, iClob(makeHeading_Widget("${dlg.bookmark.folder}"))); | 3017 | addChild_Widget(headings, iClob(makeHeading_Widget("${dlg.bookmark.folder}"))); |
2892 | const iArray *folderItems = makeBookmarkFolderItems_(); | 3018 | const iArray *folderItems = makeBookmarkFolderItems_(iFalse); |
2893 | iLabelWidget *folderButton; | 3019 | iLabelWidget *folderButton; |
2894 | setId_Widget(addChildFlags_Widget(values, | 3020 | setId_Widget(addChildFlags_Widget(values, |
2895 | iClob(folderButton = makeMenuButton_LabelWidget( | 3021 | iClob(folderButton = makeMenuButton_LabelWidget( |
@@ -2897,11 +3023,10 @@ iWidget *makeBookmarkEditor_Widget(void) { | |||
2897 | constData_Array(folderItems), | 3023 | constData_Array(folderItems), |
2898 | size_Array(folderItems))), alignLeft_WidgetFlag), | 3024 | size_Array(folderItems))), alignLeft_WidgetFlag), |
2899 | "bmed.folder"); | 3025 | "bmed.folder"); |
2900 | const uint32_t recentFolderId = recentFolder_Bookmarks(bookmarks_App()); | ||
2901 | updateDropdownSelection_LabelWidget( | ||
2902 | folderButton, format_CStr(" arg:%u", recentFolderId)); | ||
2903 | setUserData_Object(folderButton, get_Bookmarks(bookmarks_App(), recentFolderId)); | ||
2904 | } | 3026 | } |
3027 | addDialogInputWithHeading_(headings, values, "${dlg.bookmark.title}", "bmed.title", iClob(inputs[0] = new_InputWidget(0))); | ||
3028 | addDialogInputWithHeading_(headings, values, "${dlg.bookmark.url}", "bmed.url", iClob(inputs[1] = new_InputWidget(0))); | ||
3029 | setUrlContent_InputWidget(inputs[1], iTrue); | ||
2905 | addDialogInputWithHeading_(headings, values, "${dlg.bookmark.tags}", "bmed.tags", iClob(inputs[2] = new_InputWidget(0))); | 3030 | addDialogInputWithHeading_(headings, values, "${dlg.bookmark.tags}", "bmed.tags", iClob(inputs[2] = new_InputWidget(0))); |
2906 | addDialogInputWithHeading_(headings, values, "${dlg.bookmark.icon}", "bmed.icon", iClob(inputs[3] = new_InputWidget(1))); | 3031 | addDialogInputWithHeading_(headings, values, "${dlg.bookmark.icon}", "bmed.icon", iClob(inputs[3] = new_InputWidget(1))); |
2907 | /* Buttons for special tags. */ | 3032 | /* Buttons for special tags. */ |
@@ -2921,6 +3046,12 @@ iWidget *makeBookmarkEditor_Widget(void) { | |||
2921 | addChild_Widget(dlg, iClob(makeDialogButtons_Widget(actions, iElemCount(actions)))); | 3046 | addChild_Widget(dlg, iClob(makeDialogButtons_Widget(actions, iElemCount(actions)))); |
2922 | addChild_Widget(get_Root()->widget, iClob(dlg)); | 3047 | addChild_Widget(get_Root()->widget, iClob(dlg)); |
2923 | setupSheetTransition_Mobile(dlg, iTrue); | 3048 | setupSheetTransition_Mobile(dlg, iTrue); |
3049 | } | ||
3050 | /* Use a recently accessed folder as the default. */ | ||
3051 | const uint32_t recentFolderId = recentFolder_Bookmarks(bookmarks_App()); | ||
3052 | iLabelWidget *folderDrop = findChild_Widget(dlg, "bmed.folder"); | ||
3053 | updateDropdownSelection_LabelWidget(folderDrop, format_CStr(" arg:%u", recentFolderId)); | ||
3054 | setUserData_Object(folderDrop, get_Bookmarks(bookmarks_App(), recentFolderId)); | ||
2924 | return dlg; | 3055 | return dlg; |
2925 | } | 3056 | } |
2926 | 3057 | ||
@@ -2945,16 +3076,16 @@ static iBool handleBookmarkCreationCommands_SidebarWidget_(iWidget *editor, cons | |||
2945 | const uint32_t id = add_Bookmarks(bookmarks_App(), url, title, tags, first_String(icon)); | 3076 | const uint32_t id = add_Bookmarks(bookmarks_App(), url, title, tags, first_String(icon)); |
2946 | iBookmark * bm = get_Bookmarks(bookmarks_App(), id); | 3077 | iBookmark * bm = get_Bookmarks(bookmarks_App(), id); |
2947 | if (!isEmpty_String(icon)) { | 3078 | if (!isEmpty_String(icon)) { |
2948 | addTagIfMissing_Bookmark(bm, userIcon_BookmarkTag); | 3079 | bm->flags |= userIcon_BookmarkFlag; |
2949 | } | 3080 | } |
2950 | if (isSelected_Widget(findChild_Widget(editor, "bmed.tag.home"))) { | 3081 | if (isSelected_Widget(findChild_Widget(editor, "bmed.tag.home"))) { |
2951 | addTag_Bookmark(bm, homepage_BookmarkTag); | 3082 | bm->flags |= homepage_BookmarkFlag; |
2952 | } | 3083 | } |
2953 | if (isSelected_Widget(findChild_Widget(editor, "bmed.tag.remote"))) { | 3084 | if (isSelected_Widget(findChild_Widget(editor, "bmed.tag.remote"))) { |
2954 | addTag_Bookmark(bm, remoteSource_BookmarkTag); | 3085 | bm->flags |= remoteSource_BookmarkFlag; |
2955 | } | 3086 | } |
2956 | if (isSelected_Widget(findChild_Widget(editor, "bmed.tag.linksplit"))) { | 3087 | if (isSelected_Widget(findChild_Widget(editor, "bmed.tag.linksplit"))) { |
2957 | addTag_Bookmark(bm, linkSplit_BookmarkTag); | 3088 | bm->flags |= linkSplit_BookmarkFlag; |
2958 | } | 3089 | } |
2959 | bm->parentId = folder ? id_Bookmark(folder) : 0; | 3090 | bm->parentId = folder ? id_Bookmark(folder) : 0; |
2960 | setRecentFolder_Bookmarks(bookmarks_App(), bm->parentId); | 3091 | setRecentFolder_Bookmarks(bookmarks_App(), bm->parentId); |
@@ -3020,9 +3151,9 @@ static iBool handleFeedSettingCommands_(iWidget *dlg, const char *cmd) { | |||
3020 | iBookmark *bm = get_Bookmarks(bookmarks_App(), id); | 3151 | iBookmark *bm = get_Bookmarks(bookmarks_App(), id); |
3021 | iAssert(bm); | 3152 | iAssert(bm); |
3022 | set_String(&bm->title, feedTitle); | 3153 | set_String(&bm->title, feedTitle); |
3023 | addOrRemoveTag_Bookmark(bm, subscribed_BookmarkTag, iTrue); | 3154 | bm->flags |= subscribed_BookmarkFlag; |
3024 | addOrRemoveTag_Bookmark(bm, headings_BookmarkTag, headings); | 3155 | iChangeFlags(bm->flags, headings_BookmarkFlag, headings); |
3025 | addOrRemoveTag_Bookmark(bm, ignoreWeb_BookmarkTag, ignoreWeb); | 3156 | iChangeFlags(bm->flags, ignoreWeb_BookmarkFlag, ignoreWeb); |
3026 | postCommand_App("bookmarks.changed"); | 3157 | postCommand_App("bookmarks.changed"); |
3027 | setupSheetTransition_Mobile(dlg, iFalse); | 3158 | setupSheetTransition_Mobile(dlg, iFalse); |
3028 | destroy_Widget(dlg); | 3159 | destroy_Widget(dlg); |
@@ -3051,15 +3182,14 @@ iWidget *makeFeedSettings_Widget(uint32_t bookmarkId) { | |||
3051 | { format_CStr("title id:feedcfg.heading text:%s", headingText) }, | 3182 | { format_CStr("title id:feedcfg.heading text:%s", headingText) }, |
3052 | { "input id:feedcfg.title text:${dlg.feed.title}" }, | 3183 | { "input id:feedcfg.title text:${dlg.feed.title}" }, |
3053 | { "radio id:dlg.feed.entrytype", 0, 0, (const void *) typeItems }, | 3184 | { "radio id:dlg.feed.entrytype", 0, 0, (const void *) typeItems }, |
3185 | { "padding" }, | ||
3054 | { "toggle id:feedcfg.ignoreweb text:${dlg.feed.ignoreweb}" }, | 3186 | { "toggle id:feedcfg.ignoreweb text:${dlg.feed.ignoreweb}" }, |
3055 | { NULL } | 3187 | { NULL } |
3056 | }, actions, iElemCount(actions)); | 3188 | }, actions, iElemCount(actions)); |
3057 | } | 3189 | } |
3058 | else { | 3190 | else { |
3059 | dlg = makeSheet_Widget("feedcfg"); | 3191 | dlg = makeSheet_Widget("feedcfg"); |
3060 | setId_Widget( | 3192 | addDialogTitle_(dlg, headingText, "feedcfg.heading"); |
3061 | addChildFlags_Widget(dlg, iClob(new_LabelWidget(headingText, NULL)), frameless_WidgetFlag), | ||
3062 | "feedcfg.heading"); | ||
3063 | iWidget *headings, *values; | 3193 | iWidget *headings, *values; |
3064 | addChild_Widget(dlg, iClob(makeTwoColumns_Widget(&headings, &values))); | 3194 | addChild_Widget(dlg, iClob(makeTwoColumns_Widget(&headings, &values))); |
3065 | iInputWidget *input = new_InputWidget(0); | 3195 | iInputWidget *input = new_InputWidget(0); |
@@ -3085,13 +3215,13 @@ iWidget *makeFeedSettings_Widget(uint32_t bookmarkId) { | |||
3085 | setText_InputWidget(findChild_Widget(dlg, "feedcfg.title"), | 3215 | setText_InputWidget(findChild_Widget(dlg, "feedcfg.title"), |
3086 | bm ? &bm->title : feedTitle_DocumentWidget(document_App())); | 3216 | bm ? &bm->title : feedTitle_DocumentWidget(document_App())); |
3087 | setFlags_Widget(findChild_Widget(dlg, | 3217 | setFlags_Widget(findChild_Widget(dlg, |
3088 | hasTag_Bookmark(bm, headings_BookmarkTag) | 3218 | bm && bm->flags & headings_BookmarkFlag |
3089 | ? "feedcfg.type.headings" | 3219 | ? "feedcfg.type.headings" |
3090 | : "feedcfg.type.gemini"), | 3220 | : "feedcfg.type.gemini"), |
3091 | selected_WidgetFlag, | 3221 | selected_WidgetFlag, |
3092 | iTrue); | 3222 | iTrue); |
3093 | setToggle_Widget(findChild_Widget(dlg, "feedcfg.ignoreweb"), | 3223 | setToggle_Widget(findChild_Widget(dlg, "feedcfg.ignoreweb"), |
3094 | hasTag_Bookmark(bm, ignoreWeb_BookmarkTag)); | 3224 | bm && bm->flags & ignoreWeb_BookmarkFlag); |
3095 | setCommandHandler_Widget(dlg, handleFeedSettingCommands_); | 3225 | setCommandHandler_Widget(dlg, handleFeedSettingCommands_); |
3096 | } | 3226 | } |
3097 | setupSheetTransition_Mobile(dlg, incoming_TransitionFlag); | 3227 | setupSheetTransition_Mobile(dlg, incoming_TransitionFlag); |
@@ -3138,11 +3268,7 @@ iWidget *makeIdentityCreation_Widget(void) { | |||
3138 | } | 3268 | } |
3139 | else { | 3269 | else { |
3140 | dlg = makeSheet_Widget("ident"); | 3270 | dlg = makeSheet_Widget("ident"); |
3141 | setId_Widget(addChildFlags_Widget( | 3271 | addDialogTitle_(dlg, "${heading.newident}", "ident.heading"); |
3142 | dlg, | ||
3143 | iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.newident}", NULL)), | ||
3144 | frameless_WidgetFlag), | ||
3145 | "ident.heading"); | ||
3146 | iWidget *page = new_Widget(); | 3272 | iWidget *page = new_Widget(); |
3147 | addChildFlags_Widget( | 3273 | addChildFlags_Widget( |
3148 | dlg, iClob(new_LabelWidget("${dlg.newident.rsa.selfsign}", NULL)), frameless_WidgetFlag); | 3274 | dlg, iClob(new_LabelWidget("${dlg.newident.rsa.selfsign}", NULL)), frameless_WidgetFlag); |
@@ -3226,7 +3352,7 @@ static const iMenuItem languages[] = { | |||
3226 | static iBool translationHandler_(iWidget *dlg, const char *cmd) { | 3352 | static iBool translationHandler_(iWidget *dlg, const char *cmd) { |
3227 | iUnused(dlg); | 3353 | iUnused(dlg); |
3228 | if (equal_Command(cmd, "xlt.lang")) { | 3354 | if (equal_Command(cmd, "xlt.lang")) { |
3229 | const iMenuItem *langItem = &languages[languageIndex_CStr(cstr_Rangecc(range_Command(cmd, "id")))]; | 3355 | const iMenuItem *langItem = &languages[languageIndex_CStr(cstr_Command(cmd, "id"))]; |
3230 | iWidget *widget = pointer_Command(cmd); | 3356 | iWidget *widget = pointer_Command(cmd); |
3231 | iLabelWidget *drop; | 3357 | iLabelWidget *drop; |
3232 | if (flags_Widget(widget) & nativeMenu_WidgetFlag) { | 3358 | if (flags_Widget(widget) & nativeMenu_WidgetFlag) { |
@@ -3246,7 +3372,7 @@ const char *languageId_String(const iString *menuItemLabel) { | |||
3246 | iForIndices(i, languages) { | 3372 | iForIndices(i, languages) { |
3247 | if (!languages[i].label) break; | 3373 | if (!languages[i].label) break; |
3248 | if (!cmp_String(menuItemLabel, translateCStr_Lang(languages[i].label))) { | 3374 | if (!cmp_String(menuItemLabel, translateCStr_Lang(languages[i].label))) { |
3249 | return cstr_Rangecc(range_Command(languages[i].command, "id")); | 3375 | return cstr_Command(languages[i].command, "id"); |
3250 | } | 3376 | } |
3251 | } | 3377 | } |
3252 | return ""; | 3378 | return ""; |
@@ -3280,10 +3406,7 @@ iWidget *makeTranslation_Widget(iWidget *parent) { | |||
3280 | dlg = makeSheet_Widget("xlt"); | 3406 | dlg = makeSheet_Widget("xlt"); |
3281 | setFlags_Widget(dlg, keepOnTop_WidgetFlag, iFalse); | 3407 | setFlags_Widget(dlg, keepOnTop_WidgetFlag, iFalse); |
3282 | dlg->minSize.x = 70 * gap_UI; | 3408 | dlg->minSize.x = 70 * gap_UI; |
3283 | addChildFlags_Widget( | 3409 | addDialogTitle_(dlg, "${heading.translate}", NULL); |
3284 | dlg, | ||
3285 | iClob(new_LabelWidget(uiHeading_ColorEscape "${heading.translate}", NULL)), | ||
3286 | frameless_WidgetFlag); | ||
3287 | addChild_Widget(dlg, iClob(makePadding_Widget(lineHeight_Text(uiLabel_FontId)))); | 3410 | addChild_Widget(dlg, iClob(makePadding_Widget(lineHeight_Text(uiLabel_FontId)))); |
3288 | iWidget *headings, *values; | 3411 | iWidget *headings, *values; |
3289 | iWidget *page; | 3412 | iWidget *page; |
@@ -3316,20 +3439,13 @@ iWidget *makeTranslation_Widget(iWidget *parent) { | |||
3316 | addChild_Widget(dlg, iClob(makeDialogButtons_Widget(actions, iElemCount(actions)))); | 3439 | addChild_Widget(dlg, iClob(makeDialogButtons_Widget(actions, iElemCount(actions)))); |
3317 | addChild_Widget(parent, iClob(dlg)); | 3440 | addChild_Widget(parent, iClob(dlg)); |
3318 | arrange_Widget(dlg); | 3441 | arrange_Widget(dlg); |
3442 | arrange_Widget(dlg); /* TODO: Augh, another layout bug: two arranges required. */ | ||
3319 | } | 3443 | } |
3320 | /* Update choices. */ | 3444 | /* Update choices. */ |
3321 | updateDropdownSelection_LabelWidget(findChild_Widget(dlg, "xlt.from"), | 3445 | updateDropdownSelection_LabelWidget(findChild_Widget(dlg, "xlt.from"), |
3322 | languages[prefs_App()->langFrom].command); | 3446 | languages[prefs_App()->langFrom].command); |
3323 | updateDropdownSelection_LabelWidget(findChild_Widget(dlg, "xlt.to"), | 3447 | updateDropdownSelection_LabelWidget(findChild_Widget(dlg, "xlt.to"), |
3324 | languages[prefs_App()->langTo].command); | 3448 | languages[prefs_App()->langTo].command); |
3325 | // updateText_LabelWidget( | ||
3326 | // findChild_Widget(dlg, "xlt.from"), | ||
3327 | // text_LabelWidget(child_Widget(findChild_Widget(findChild_Widget(dlg, "xlt.from"), "menu"), | ||
3328 | // prefs_App()->langFrom))); | ||
3329 | // updateText_LabelWidget( | ||
3330 | // findChild_Widget(dlg, "xlt.to"), | ||
3331 | // text_LabelWidget(child_Widget(findChild_Widget(findChild_Widget(dlg, "xlt.to"), "menu"), | ||
3332 | // prefs_App()->langTo))); | ||
3333 | setCommandHandler_Widget(dlg, translationHandler_); | 3449 | setCommandHandler_Widget(dlg, translationHandler_); |
3334 | setupSheetTransition_Mobile(dlg, iTrue); | 3450 | setupSheetTransition_Mobile(dlg, iTrue); |
3335 | return dlg; | 3451 | return dlg; |
diff --git a/src/ui/util.h b/src/ui/util.h index d13d751b..98ce784c 100644 --- a/src/ui/util.h +++ b/src/ui/util.h | |||
@@ -47,16 +47,31 @@ iLocalDef iBool isMetricsChange_UserEvent(const SDL_Event *d) { | |||
47 | } | 47 | } |
48 | 48 | ||
49 | enum iMouseWheelFlag { | 49 | enum iMouseWheelFlag { |
50 | perPixel_MouseWheelFlag = iBit(9), /* e.g., trackpad or finger scroll; applied to `direction` */ | 50 | /* Note: A future version of SDL may support per-pixel scrolling, but 2.0.x doesn't. */ |
51 | perPixel_MouseWheelFlag = iBit(9), /* e.g., trackpad or finger scroll; applied to `direction` */ | ||
52 | inertia_MouseWheelFlag = iBit(10), | ||
53 | scrollFinished_MouseWheelFlag = iBit(11), | ||
51 | }; | 54 | }; |
52 | 55 | ||
53 | /* Note: A future version of SDL may support per-pixel scrolling, but 2.0.x doesn't. */ | ||
54 | iLocalDef void setPerPixel_MouseWheelEvent(SDL_MouseWheelEvent *ev, iBool set) { | 56 | iLocalDef void setPerPixel_MouseWheelEvent(SDL_MouseWheelEvent *ev, iBool set) { |
55 | iChangeFlags(ev->direction, perPixel_MouseWheelFlag, set); | 57 | iChangeFlags(ev->direction, perPixel_MouseWheelFlag, set); |
56 | } | 58 | } |
59 | iLocalDef void setInertia_MouseWheelEvent(SDL_MouseWheelEvent *ev, iBool set) { | ||
60 | iChangeFlags(ev->direction, inertia_MouseWheelFlag, set); | ||
61 | } | ||
62 | iLocalDef void setScrollFinished_MouseWheelEvent(SDL_MouseWheelEvent *ev, iBool set) { | ||
63 | iChangeFlags(ev->direction, scrollFinished_MouseWheelFlag, set); | ||
64 | } | ||
65 | |||
57 | iLocalDef iBool isPerPixel_MouseWheelEvent(const SDL_MouseWheelEvent *ev) { | 66 | iLocalDef iBool isPerPixel_MouseWheelEvent(const SDL_MouseWheelEvent *ev) { |
58 | return (ev->direction & perPixel_MouseWheelFlag) != 0; | 67 | return (ev->direction & perPixel_MouseWheelFlag) != 0; |
59 | } | 68 | } |
69 | iLocalDef iBool isInertia_MouseWheelEvent(const SDL_MouseWheelEvent *ev) { | ||
70 | return (ev->direction & inertia_MouseWheelFlag) != 0; | ||
71 | } | ||
72 | iLocalDef iBool isScrollFinished_MouseWheelEvent(const SDL_MouseWheelEvent *ev) { | ||
73 | return (ev->direction & scrollFinished_MouseWheelFlag) != 0; | ||
74 | } | ||
60 | 75 | ||
61 | iInt2 coord_MouseWheelEvent (const SDL_MouseWheelEvent *); | 76 | iInt2 coord_MouseWheelEvent (const SDL_MouseWheelEvent *); |
62 | 77 | ||
@@ -179,11 +194,18 @@ iDeclareType(SmoothScroll) | |||
179 | 194 | ||
180 | typedef void (*iSmoothScrollNotifyFunc)(iAnyObject *, int offset, uint32_t span); | 195 | typedef void (*iSmoothScrollNotifyFunc)(iAnyObject *, int offset, uint32_t span); |
181 | 196 | ||
197 | enum iSmoothScrollFlags { | ||
198 | pullDownAction_SmoothScrollFlag = iBit(1), | ||
199 | pullUpAction_SmoothScrollFlag = iBit(2), | ||
200 | }; | ||
201 | |||
182 | struct Impl_SmoothScroll { | 202 | struct Impl_SmoothScroll { |
183 | iAnim pos; | 203 | iAnim pos; |
184 | int max; | 204 | int max; |
185 | int overscroll; | 205 | int overscroll; |
186 | iWidget *widget; | 206 | iWidget *widget; |
207 | int flags; | ||
208 | int pullActionTriggered; | ||
187 | iSmoothScrollNotifyFunc notify; | 209 | iSmoothScrollNotifyFunc notify; |
188 | }; | 210 | }; |
189 | 211 | ||
@@ -197,6 +219,7 @@ iBool processEvent_SmoothScroll (iSmoothScroll *, const SDL_Event *ev); | |||
197 | 219 | ||
198 | float pos_SmoothScroll (const iSmoothScroll *); | 220 | float pos_SmoothScroll (const iSmoothScroll *); |
199 | iBool isFinished_SmoothScroll (const iSmoothScroll *); | 221 | iBool isFinished_SmoothScroll (const iSmoothScroll *); |
222 | float pullActionPos_SmoothScroll (const iSmoothScroll *); /* 0...1 */ | ||
200 | 223 | ||
201 | /*-----------------------------------------------------------------------------------------------*/ | 224 | /*-----------------------------------------------------------------------------------------------*/ |
202 | 225 | ||
@@ -249,7 +272,8 @@ void setMenuItemDisabled_Widget (iWidget *menu, const char *comm | |||
249 | void setMenuItemDisabledByIndex_Widget(iWidget *menu, size_t index, iBool disable); | 272 | void setMenuItemDisabledByIndex_Widget(iWidget *menu, size_t index, iBool disable); |
250 | void setMenuItemLabel_Widget (iWidget *menu, const char *command, const char *newLabel); | 273 | void setMenuItemLabel_Widget (iWidget *menu, const char *command, const char *newLabel); |
251 | void setMenuItemLabelByIndex_Widget (iWidget *menu, size_t index, const char *newLabel); | 274 | void setMenuItemLabelByIndex_Widget (iWidget *menu, size_t index, const char *newLabel); |
252 | void setNativeMenuItems_Widget (iWidget *, const iMenuItem *items, size_t n); | 275 | void setNativeMenuItems_Widget (iWidget *menu, const iMenuItem *items, size_t n); |
276 | iWidget * findUserData_Widget (iWidget *, void *userData); | ||
253 | 277 | ||
254 | int checkContextMenu_Widget (iWidget *, const SDL_Event *ev); /* see macro below */ | 278 | int checkContextMenu_Widget (iWidget *, const SDL_Event *ev); /* see macro below */ |
255 | 279 | ||
@@ -293,6 +317,7 @@ iWidget * makeTwoColumns_Widget (iWidget **headings, iWidget **values); | |||
293 | 317 | ||
294 | iLabelWidget *dialogAcceptButton_Widget (const iWidget *); | 318 | iLabelWidget *dialogAcceptButton_Widget (const iWidget *); |
295 | 319 | ||
320 | iLabelWidget *addDialogTitle_Widget (iWidget *, const char *text, const char *idOrNull); | ||
296 | iInputWidget *addTwoColumnDialogInputField_Widget(iWidget *headings, iWidget *values, | 321 | iInputWidget *addTwoColumnDialogInputField_Widget(iWidget *headings, iWidget *values, |
297 | const char *labelText, const char *inputId, | 322 | const char *labelText, const char *inputId, |
298 | iInputWidget *input); | 323 | iInputWidget *input); |
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 | |||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | 21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ |
22 | 22 | ||
23 | #include "visbuf.h" | 23 | #include "visbuf.h" |
24 | #include "paint.h" | ||
24 | #include "window.h" | 25 | #include "window.h" |
25 | #include "util.h" | 26 | #include "util.h" |
26 | 27 | ||
@@ -224,6 +225,8 @@ void draw_VisBuf(const iVisBuf *d, const iInt2 topLeft, const iRangei yClipBound | |||
224 | continue; /* Outside the clipping area. */ | 225 | continue; /* Outside the clipping area. */ |
225 | #endif | 226 | #endif |
226 | } | 227 | } |
228 | dst.x += origin_Paint.x; | ||
229 | dst.y += origin_Paint.y; | ||
227 | #if defined (DEBUG_SCALE) | 230 | #if defined (DEBUG_SCALE) |
228 | dst.w *= DEBUG_SCALE; | 231 | dst.w *= DEBUG_SCALE; |
229 | dst.h *= DEBUG_SCALE; | 232 | dst.h *= DEBUG_SCALE; |
diff --git a/src/ui/widget.c b/src/ui/widget.c index cedda461..9f67b1c7 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. */ | |||
31 | #include "util.h" | 31 | #include "util.h" |
32 | #include "window.h" | 32 | #include "window.h" |
33 | 33 | ||
34 | #include "labelwidget.h" | ||
35 | |||
34 | #include <the_Foundation/ptrarray.h> | 36 | #include <the_Foundation/ptrarray.h> |
35 | #include <the_Foundation/ptrset.h> | 37 | #include <the_Foundation/ptrset.h> |
36 | #include <SDL_mouse.h> | 38 | #include <SDL_mouse.h> |
@@ -122,7 +124,9 @@ void init_Widget(iWidget *d) { | |||
122 | init_String(&d->id); | 124 | init_String(&d->id); |
123 | d->root = get_Root(); /* never changes after this */ | 125 | d->root = get_Root(); /* never changes after this */ |
124 | d->flags = 0; | 126 | d->flags = 0; |
127 | d->flags2 = 0; | ||
125 | d->rect = zero_Rect(); | 128 | d->rect = zero_Rect(); |
129 | d->oldSize = zero_I2(); | ||
126 | d->minSize = zero_I2(); | 130 | d->minSize = zero_I2(); |
127 | d->sizeRef = NULL; | 131 | d->sizeRef = NULL; |
128 | d->offsetRef = NULL; | 132 | d->offsetRef = NULL; |
@@ -139,8 +143,11 @@ void init_Widget(iWidget *d) { | |||
139 | static void visualOffsetAnimation_Widget_(void *ptr) { | 143 | static void visualOffsetAnimation_Widget_(void *ptr) { |
140 | iWidget *d = ptr; | 144 | iWidget *d = ptr; |
141 | postRefresh_App(); | 145 | postRefresh_App(); |
146 | d->root->didAnimateVisualOffsets = iTrue; | ||
147 | // printf("'%s' visoffanim: fin:%d val:%f\n", cstr_String(&d->id), | ||
148 | // isFinished_Anim(&d->visualOffset), value_Anim(&d->visualOffset)); fflush(stdout); | ||
142 | if (!isFinished_Anim(&d->visualOffset)) { | 149 | if (!isFinished_Anim(&d->visualOffset)) { |
143 | addTicker_App(visualOffsetAnimation_Widget_, ptr); | 150 | addTickerRoot_App(visualOffsetAnimation_Widget_, d->root, ptr); |
144 | } | 151 | } |
145 | else { | 152 | else { |
146 | d->flags &= ~visualOffset_WidgetFlag; | 153 | d->flags &= ~visualOffset_WidgetFlag; |
@@ -269,10 +276,12 @@ void setMinSize_Widget(iWidget *d, iInt2 minSize) { | |||
269 | } | 276 | } |
270 | 277 | ||
271 | void setPadding_Widget(iWidget *d, int left, int top, int right, int bottom) { | 278 | void setPadding_Widget(iWidget *d, int left, int top, int right, int bottom) { |
272 | d->padding[0] = left; | 279 | if (d) { |
273 | d->padding[1] = top; | 280 | d->padding[0] = left; |
274 | d->padding[2] = right; | 281 | d->padding[1] = top; |
275 | d->padding[3] = bottom; | 282 | d->padding[2] = right; |
283 | d->padding[3] = bottom; | ||
284 | } | ||
276 | } | 285 | } |
277 | 286 | ||
278 | iWidget *root_Widget(const iWidget *d) { | 287 | iWidget *root_Widget(const iWidget *d) { |
@@ -414,9 +423,10 @@ static iBool setWidth_Widget_(iWidget *d, int width) { | |||
414 | if (d->rect.size.x != width) { | 423 | if (d->rect.size.x != width) { |
415 | d->rect.size.x = width; | 424 | d->rect.size.x = width; |
416 | TRACE(d, "width has changed to %d", width); | 425 | TRACE(d, "width has changed to %d", width); |
417 | if (class_Widget(d)->sizeChanged) { | 426 | // if (~d->flags2 & undefinedWidth_WidgetFlag2 && class_Widget(d)->sizeChanged) { |
418 | class_Widget(d)->sizeChanged(d); | 427 | // class_Widget(d)->sizeChanged(d); |
419 | } | 428 | // } |
429 | // d->flags2 &= ~undefinedWidth_WidgetFlag2; | ||
420 | return iTrue; | 430 | return iTrue; |
421 | } | 431 | } |
422 | } | 432 | } |
@@ -437,9 +447,10 @@ static iBool setHeight_Widget_(iWidget *d, int height) { | |||
437 | if (d->rect.size.y != height) { | 447 | if (d->rect.size.y != height) { |
438 | d->rect.size.y = height; | 448 | d->rect.size.y = height; |
439 | TRACE(d, "height has changed to %d", height); | 449 | TRACE(d, "height has changed to %d", height); |
440 | if (class_Widget(d)->sizeChanged) { | 450 | // if (~d->flags2 & undefinedHeight_WidgetFlag2 && class_Widget(d)->sizeChanged) { |
441 | class_Widget(d)->sizeChanged(d); | 451 | // class_Widget(d)->sizeChanged(d); |
442 | } | 452 | // } |
453 | // d->flags2 &= ~undefinedHeight_WidgetFlag2; | ||
443 | return iTrue; | 454 | return iTrue; |
444 | } | 455 | } |
445 | } | 456 | } |
@@ -836,6 +847,13 @@ static void arrange_Widget_(iWidget *d) { | |||
836 | } | 847 | } |
837 | 848 | ||
838 | static void resetArrangement_Widget_(iWidget *d) { | 849 | static void resetArrangement_Widget_(iWidget *d) { |
850 | d->oldSize = d->rect.size; | ||
851 | if (d->flags & resizeToParentWidth_WidgetFlag) { | ||
852 | d->rect.size.x = 0; | ||
853 | } | ||
854 | if (d->flags & resizeToParentHeight_WidgetFlag) { | ||
855 | d->rect.size.y = 0; | ||
856 | } | ||
839 | iForEach(ObjectList, i, children_Widget(d)) { | 857 | iForEach(ObjectList, i, children_Widget(d)) { |
840 | iWidget *child = as_Widget(i.object); | 858 | iWidget *child = as_Widget(i.object); |
841 | resetArrangement_Widget_(child); | 859 | resetArrangement_Widget_(child); |
@@ -847,6 +865,14 @@ static void resetArrangement_Widget_(iWidget *d) { | |||
847 | ~child->flags & fixedWidth_WidgetFlag) { | 865 | ~child->flags & fixedWidth_WidgetFlag) { |
848 | child->rect.size.x = 0; | 866 | child->rect.size.x = 0; |
849 | } | 867 | } |
868 | if (d->flags & resizeChildrenToWidestChild_WidgetFlag) { | ||
869 | if (isInstance_Object(child, &Class_LabelWidget)) { | ||
870 | updateSize_LabelWidget((iLabelWidget *) child); | ||
871 | } | ||
872 | else { | ||
873 | child->rect.size.x = 0; | ||
874 | } | ||
875 | } | ||
850 | if (d->flags & arrangeVertical_WidgetFlag) { | 876 | if (d->flags & arrangeVertical_WidgetFlag) { |
851 | child->rect.pos.y = 0; | 877 | child->rect.pos.y = 0; |
852 | } | 878 | } |
@@ -858,6 +884,15 @@ static void resetArrangement_Widget_(iWidget *d) { | |||
858 | } | 884 | } |
859 | } | 885 | } |
860 | 886 | ||
887 | static void notifySizeChanged_Widget_(iWidget *d) { | ||
888 | if (class_Widget(d)->sizeChanged && !isEqual_I2(d->rect.size, d->oldSize)) { | ||
889 | class_Widget(d)->sizeChanged(d); | ||
890 | } | ||
891 | iForEach(ObjectList, child, d->children) { | ||
892 | notifySizeChanged_Widget_(child.object); | ||
893 | } | ||
894 | } | ||
895 | |||
861 | void arrange_Widget(iWidget *d) { | 896 | void arrange_Widget(iWidget *d) { |
862 | if (d) { | 897 | if (d) { |
863 | #if !defined (NDEBUG) | 898 | #if !defined (NDEBUG) |
@@ -867,6 +902,8 @@ void arrange_Widget(iWidget *d) { | |||
867 | #endif | 902 | #endif |
868 | resetArrangement_Widget_(d); /* back to initial default sizes */ | 903 | resetArrangement_Widget_(d); /* back to initial default sizes */ |
869 | arrange_Widget_(d); | 904 | arrange_Widget_(d); |
905 | notifySizeChanged_Widget_(d); | ||
906 | d->root->didChangeArrangement = iTrue; | ||
870 | } | 907 | } |
871 | } | 908 | } |
872 | 909 | ||
@@ -884,6 +921,14 @@ int visualOffsetByReference_Widget(const iWidget *d) { | |||
884 | // const float factor = width_Widget(d) / (float) size_Root(d->root).x; | 921 | // const float factor = width_Widget(d) / (float) size_Root(d->root).x; |
885 | const int invOff = width_Widget(d) - iRound(value_Anim(&child->visualOffset)); | 922 | const int invOff = width_Widget(d) - iRound(value_Anim(&child->visualOffset)); |
886 | offX -= invOff / 4; | 923 | offX -= invOff / 4; |
924 | #if 0 | ||
925 | if (invOff) { | ||
926 | printf(" [%p] %s (%p, fin:%d visoff:%d drag:%d): invOff %d\n", d, cstr_String(&child->id), child, | ||
927 | isFinished_Anim(&child->visualOffset), | ||
928 | (child->flags & visualOffset_WidgetFlag) != 0, | ||
929 | (child->flags & dragged_WidgetFlag) != 0, invOff); fflush(stdout); | ||
930 | } | ||
931 | #endif | ||
887 | } | 932 | } |
888 | } | 933 | } |
889 | return offX; | 934 | return offX; |
@@ -1183,6 +1228,9 @@ iBool scrollOverflow_Widget(iWidget *d, int delta) { | |||
1183 | bounds.pos.y = iMin(bounds.pos.y, validPosRange.end); | 1228 | bounds.pos.y = iMin(bounds.pos.y, validPosRange.end); |
1184 | } | 1229 | } |
1185 | // printf("range: %d ... %d\n", range.start, range.end); | 1230 | // printf("range: %d ... %d\n", range.start, range.end); |
1231 | if (delta) { | ||
1232 | d->root->didChangeArrangement = iTrue; /* ensure that widgets update if needed */ | ||
1233 | } | ||
1186 | } | 1234 | } |
1187 | else { | 1235 | else { |
1188 | bounds.pos.y = iClamp(bounds.pos.y, validPosRange.start, validPosRange.end); | 1236 | bounds.pos.y = iClamp(bounds.pos.y, validPosRange.start, validPosRange.end); |
@@ -1370,7 +1418,8 @@ void drawLayerEffects_Widget(const iWidget *d) { | |||
1370 | shadowBorder = iFalse; | 1418 | shadowBorder = iFalse; |
1371 | } | 1419 | } |
1372 | } | 1420 | } |
1373 | const iBool isFaded = fadeBackground && ~d->flags & noFadeBackground_WidgetFlag; | 1421 | const iBool isFaded = (fadeBackground && ~d->flags & noFadeBackground_WidgetFlag) || |
1422 | (d->flags2 & fadeBackground_WidgetFlag2); | ||
1374 | if (shadowBorder && ~d->flags & noShadowBorder_WidgetFlag) { | 1423 | if (shadowBorder && ~d->flags & noShadowBorder_WidgetFlag) { |
1375 | iPaint p; | 1424 | iPaint p; |
1376 | init_Paint(&p); | 1425 | init_Paint(&p); |
@@ -1381,9 +1430,19 @@ void drawLayerEffects_Widget(const iWidget *d) { | |||
1381 | init_Paint(&p); | 1430 | init_Paint(&p); |
1382 | p.alpha = 0x50; | 1431 | p.alpha = 0x50; |
1383 | if (flags_Widget(d) & (visualOffset_WidgetFlag | dragged_WidgetFlag)) { | 1432 | if (flags_Widget(d) & (visualOffset_WidgetFlag | dragged_WidgetFlag)) { |
1384 | const float area = d->rect.size.x * d->rect.size.y; | 1433 | const float area = d->rect.size.x * d->rect.size.y; |
1434 | const float rootArea = area_Rect(rect_Root(d->root)); | ||
1385 | const float visibleArea = area_Rect(intersect_Rect(bounds_Widget(d), rect_Root(d->root))); | 1435 | const float visibleArea = area_Rect(intersect_Rect(bounds_Widget(d), rect_Root(d->root))); |
1386 | p.alpha *= (area > 0 ? visibleArea / area : 0.0f); | 1436 | if (isPortraitPhone_App() && !cmp_String(&d->id, "sidebar")) { |
1437 | p.alpha *= iClamp(visibleArea / rootArea * 2, 0.0f, 1.0f); | ||
1438 | } | ||
1439 | else if (area > 0) { | ||
1440 | p.alpha *= visibleArea / area; | ||
1441 | } | ||
1442 | else { | ||
1443 | p.alpha = 0; | ||
1444 | } | ||
1445 | //printf("area:%f visarea:%f alpha:%d\n", rootArea, visibleArea, p.alpha); | ||
1387 | } | 1446 | } |
1388 | SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND); | 1447 | SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND); |
1389 | fillRect_Paint(&p, rect_Root(d->root), backgroundFadeColor_Widget()); | 1448 | fillRect_Paint(&p, rect_Root(d->root), backgroundFadeColor_Widget()); |
@@ -1851,7 +1910,7 @@ iAny *findParentClass_Widget(const iWidget *d, const iAnyClass *class) { | |||
1851 | } | 1910 | } |
1852 | 1911 | ||
1853 | iAny *findOverflowScrollable_Widget(iWidget *d) { | 1912 | iAny *findOverflowScrollable_Widget(iWidget *d) { |
1854 | const iRect rootRect = rect_Root(d->root); | 1913 | const iRect rootRect = visibleRect_Root(d->root); |
1855 | for (iWidget *w = d; w; w = parent_Widget(w)) { | 1914 | for (iWidget *w = d; w; w = parent_Widget(w)) { |
1856 | if (flags_Widget(w) & overflowScrollable_WidgetFlag) { | 1915 | if (flags_Widget(w) & overflowScrollable_WidgetFlag) { |
1857 | const iRect bounds = boundsWithoutVisualOffset_Widget(w); | 1916 | const iRect bounds = boundsWithoutVisualOffset_Widget(w); |
@@ -1956,12 +2015,16 @@ iBool isAffectedByVisualOffset_Widget(const iWidget *d) { | |||
1956 | if (w->flags & visualOffset_WidgetFlag) { | 2015 | if (w->flags & visualOffset_WidgetFlag) { |
1957 | return iTrue; | 2016 | return iTrue; |
1958 | } | 2017 | } |
2018 | if (visualOffsetByReference_Widget(w) != 0) { | ||
2019 | return iTrue; | ||
2020 | } | ||
1959 | } | 2021 | } |
1960 | return iFalse; | 2022 | return iFalse; |
1961 | } | 2023 | } |
1962 | 2024 | ||
1963 | void setFocus_Widget(iWidget *d) { | 2025 | void setFocus_Widget(iWidget *d) { |
1964 | iWindow *win = get_Window(); | 2026 | iWindow *win = d ? window_Widget(d) : get_Window(); |
2027 | iAssert(win); | ||
1965 | if (win->focus != d) { | 2028 | if (win->focus != d) { |
1966 | if (win->focus) { | 2029 | if (win->focus) { |
1967 | iAssert(!contains_PtrSet(win->focus->root->pendingDestruction, win->focus)); | 2030 | iAssert(!contains_PtrSet(win->focus->root->pendingDestruction, win->focus)); |
@@ -1976,6 +2039,13 @@ void setFocus_Widget(iWidget *d) { | |||
1976 | } | 2039 | } |
1977 | } | 2040 | } |
1978 | 2041 | ||
2042 | void setKeyboardGrab_Widget(iWidget *d) { | ||
2043 | iWindow *win = d ? window_Widget(d) : get_Window(); | ||
2044 | iAssert(win); | ||
2045 | win->focus = d; | ||
2046 | /* no notifications sent */ | ||
2047 | } | ||
2048 | |||
1979 | iWidget *focus_Widget(void) { | 2049 | iWidget *focus_Widget(void) { |
1980 | return get_Window()->focus; | 2050 | return get_Window()->focus; |
1981 | } | 2051 | } |
diff --git a/src/ui/widget.h b/src/ui/widget.h index 4025f5c5..fb7eb5e2 100644 --- a/src/ui/widget.h +++ b/src/ui/widget.h | |||
@@ -123,6 +123,11 @@ enum iWidgetFlag { | |||
123 | #define refChildrenOffset_WidgetFlag iBit64(63) /* visual offset determined by the offset of referenced children */ | 123 | #define refChildrenOffset_WidgetFlag iBit64(63) /* visual offset determined by the offset of referenced children */ |
124 | #define nativeMenu_WidgetFlag iBit64(64) | 124 | #define nativeMenu_WidgetFlag iBit64(64) |
125 | 125 | ||
126 | enum iWidgetFlag2 { | ||
127 | slidingSheetDraggable_WidgetFlag2 = iBit(1), | ||
128 | fadeBackground_WidgetFlag2 = iBit(2), | ||
129 | }; | ||
130 | |||
126 | enum iWidgetAddPos { | 131 | enum iWidgetAddPos { |
127 | back_WidgetAddPos, | 132 | back_WidgetAddPos, |
128 | front_WidgetAddPos, | 133 | front_WidgetAddPos, |
@@ -139,7 +144,9 @@ struct Impl_Widget { | |||
139 | iObject object; | 144 | iObject object; |
140 | iString id; | 145 | iString id; |
141 | int64_t flags; | 146 | int64_t flags; |
147 | int flags2; | ||
142 | iRect rect; | 148 | iRect rect; |
149 | iInt2 oldSize; /* in previous arrangement; for notification */ | ||
143 | iInt2 minSize; | 150 | iInt2 minSize; |
144 | iWidget * sizeRef; | 151 | iWidget * sizeRef; |
145 | iWidget * offsetRef; | 152 | iWidget * offsetRef; |
@@ -230,6 +237,24 @@ iLocalDef int height_Widget(const iAnyObject *d) { | |||
230 | } | 237 | } |
231 | return 0; | 238 | return 0; |
232 | } | 239 | } |
240 | iLocalDef int leftPad_Widget(const iWidget *d) { | ||
241 | return d->padding[0]; | ||
242 | } | ||
243 | iLocalDef int topPad_Widget(const iWidget *d) { | ||
244 | return d->padding[1]; | ||
245 | } | ||
246 | iLocalDef int rightPad_Widget(const iWidget *d) { | ||
247 | return d->padding[2]; | ||
248 | } | ||
249 | iLocalDef int bottomPad_Widget(const iWidget *d) { | ||
250 | return d->padding[3]; | ||
251 | } | ||
252 | iLocalDef iInt2 tlPad_Widget(const iWidget *d) { | ||
253 | return init_I2(leftPad_Widget(d), topPad_Widget(d)); | ||
254 | } | ||
255 | iLocalDef iInt2 brPad_Widget(const iWidget *d) { | ||
256 | return init_I2(rightPad_Widget(d), bottomPad_Widget(d)); | ||
257 | } | ||
233 | iLocalDef iObjectList *children_Widget(iAnyObject *d) { | 258 | iLocalDef iObjectList *children_Widget(iAnyObject *d) { |
234 | if (d == NULL) return NULL; | 259 | if (d == NULL) return NULL; |
235 | iAssert(isInstance_Object(d, &Class_Widget)); | 260 | iAssert(isInstance_Object(d, &Class_Widget)); |
@@ -302,7 +327,8 @@ void scrollInfo_Widget (const iWidget *, iWidgetScrollInfo *inf | |||
302 | 327 | ||
303 | int backgroundFadeColor_Widget (void); | 328 | int backgroundFadeColor_Widget (void); |
304 | 329 | ||
305 | void setFocus_Widget (iWidget *); | 330 | void setFocus_Widget (iWidget *); /* widget must be flagged `focusable` */ |
331 | void setKeyboardGrab_Widget (iWidget *); /* sets focus on any widget */ | ||
306 | iWidget * focus_Widget (void); | 332 | iWidget * focus_Widget (void); |
307 | void setHover_Widget (iWidget *); | 333 | void setHover_Widget (iWidget *); |
308 | iWidget * hover_Widget (void); | 334 | iWidget * hover_Widget (void); |
diff --git a/src/ui/window.c b/src/ui/window.c index 9f12cabf..af36bb22 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) { | |||
442 | static SDL_Surface *loadImage_(const iBlock *data, int resized) { | 442 | static SDL_Surface *loadImage_(const iBlock *data, int resized) { |
443 | int w = 0, h = 0, num = 4; | 443 | int w = 0, h = 0, num = 4; |
444 | stbi_uc *pixels = stbi_load_from_memory( | 444 | stbi_uc *pixels = stbi_load_from_memory( |
445 | constData_Block(data), size_Block(data), &w, &h, &num, STBI_rgb_alpha); | 445 | constData_Block(data), (int) size_Block(data), &w, &h, &num, STBI_rgb_alpha); |
446 | if (resized) { | 446 | if (resized) { |
447 | stbi_uc *rsPixels = malloc(num * resized * resized); | 447 | stbi_uc *rsPixels = malloc(num * resized * resized); |
448 | stbir_resize_uint8(pixels, w, h, 0, rsPixels, resized, resized, 0, num); | 448 | stbir_resize_uint8(pixels, w, h, 0, rsPixels, resized, resized, 0, num); |
@@ -560,6 +560,7 @@ void init_MainWindow(iMainWindow *d, iRect rect) { | |||
560 | d->splitMode = 0; | 560 | d->splitMode = 0; |
561 | d->pendingSplitMode = 0; | 561 | d->pendingSplitMode = 0; |
562 | d->pendingSplitUrl = new_String(); | 562 | d->pendingSplitUrl = new_String(); |
563 | d->pendingSplitOrigin = new_String(); | ||
563 | d->place.initialPos = rect.pos; | 564 | d->place.initialPos = rect.pos; |
564 | d->place.normalRect = rect; | 565 | d->place.normalRect = rect; |
565 | d->place.lastNotifiedSize = zero_I2(); | 566 | d->place.lastNotifiedSize = zero_I2(); |
@@ -605,6 +606,7 @@ void init_MainWindow(iMainWindow *d, iRect rect) { | |||
605 | #endif | 606 | #endif |
606 | setCurrent_Text(d->base.text); | 607 | setCurrent_Text(d->base.text); |
607 | SDL_GetRendererOutputSize(d->base.render, &d->base.size.x, &d->base.size.y); | 608 | SDL_GetRendererOutputSize(d->base.render, &d->base.size.x, &d->base.size.y); |
609 | d->maxDrawableHeight = d->base.size.y; | ||
608 | setupUserInterface_MainWindow(d); | 610 | setupUserInterface_MainWindow(d); |
609 | postCommand_App("~bindings.changed"); /* update from bindings */ | 611 | postCommand_App("~bindings.changed"); /* update from bindings */ |
610 | /* Load the border shadow texture. */ { | 612 | /* Load the border shadow texture. */ { |
@@ -642,6 +644,7 @@ void deinit_MainWindow(iMainWindow *d) { | |||
642 | if (theMainWindow_ == d) { | 644 | if (theMainWindow_ == d) { |
643 | theMainWindow_ = NULL; | 645 | theMainWindow_ = NULL; |
644 | } | 646 | } |
647 | delete_String(d->pendingSplitOrigin); | ||
645 | delete_String(d->pendingSplitUrl); | 648 | delete_String(d->pendingSplitUrl); |
646 | deinit_Window(&d->base); | 649 | deinit_Window(&d->base); |
647 | } | 650 | } |
@@ -1006,6 +1009,28 @@ iBool processEvent_Window(iWindow *d, const SDL_Event *ev) { | |||
1006 | postCommand_App("media.player.update"); /* in case a player needs updating */ | 1009 | postCommand_App("media.player.update"); /* in case a player needs updating */ |
1007 | return iTrue; | 1010 | return iTrue; |
1008 | } | 1011 | } |
1012 | if (event.type == SDL_USEREVENT && isCommand_UserEvent(ev, "window.sysframe") && mw) { | ||
1013 | /* This command is sent on Android to update the keyboard height. */ | ||
1014 | const char *cmd = command_UserEvent(ev); | ||
1015 | /* | ||
1016 | 0 | ||
1017 | | | ||
1018 | top | ||
1019 | | | | ||
1020 | | bottom (top of keyboard) : | ||
1021 | | | : keyboardHeight | ||
1022 | maxDrawableHeight : | ||
1023 | | | ||
1024 | fullheight | ||
1025 | */ | ||
1026 | const int top = argLabel_Command(cmd, "top"); | ||
1027 | const int bottom = argLabel_Command(cmd, "bottom"); | ||
1028 | if (!SDL_IsScreenKeyboardShown(mw->base.win)) { | ||
1029 | mw->maxDrawableHeight = bottom - top; | ||
1030 | } | ||
1031 | setKeyboardHeight_MainWindow(mw, top + mw->maxDrawableHeight - bottom); | ||
1032 | return iTrue; | ||
1033 | } | ||
1009 | if (processEvent_Touch(&event)) { | 1034 | if (processEvent_Touch(&event)) { |
1010 | return iTrue; | 1035 | return iTrue; |
1011 | } | 1036 | } |
@@ -1236,11 +1261,15 @@ void draw_MainWindow(iMainWindow *d) { | |||
1236 | isDrawing_ = iTrue; | 1261 | isDrawing_ = iTrue; |
1237 | setCurrent_Text(d->base.text); | 1262 | setCurrent_Text(d->base.text); |
1238 | /* Check if root needs resizing. */ { | 1263 | /* Check if root needs resizing. */ { |
1264 | const iBool wasPortrait = isPortrait_App(); | ||
1239 | iInt2 renderSize; | 1265 | iInt2 renderSize; |
1240 | SDL_GetRendererOutputSize(w->render, &renderSize.x, &renderSize.y); | 1266 | SDL_GetRendererOutputSize(w->render, &renderSize.x, &renderSize.y); |
1241 | if (!isEqual_I2(renderSize, w->size)) { | 1267 | if (!isEqual_I2(renderSize, w->size)) { |
1242 | updateSize_MainWindow_(d, iTrue); | 1268 | updateSize_MainWindow_(d, iTrue); |
1243 | processEvents_App(postedEventsOnly_AppEventMode); | 1269 | processEvents_App(postedEventsOnly_AppEventMode); |
1270 | if (isPortrait_App() != wasPortrait) { | ||
1271 | d->maxDrawableHeight = renderSize.y; | ||
1272 | } | ||
1244 | } | 1273 | } |
1245 | } | 1274 | } |
1246 | const int winFlags = SDL_GetWindowFlags(d->base.win); | 1275 | const int winFlags = SDL_GetWindowFlags(d->base.win); |
@@ -1268,6 +1297,14 @@ void draw_MainWindow(iMainWindow *d) { | |||
1268 | } | 1297 | } |
1269 | /* Draw widgets. */ | 1298 | /* Draw widgets. */ |
1270 | w->frameTime = SDL_GetTicks(); | 1299 | w->frameTime = SDL_GetTicks(); |
1300 | iForIndices(i, d->base.roots) { | ||
1301 | iRoot *root = d->base.roots[i]; | ||
1302 | if (root) { | ||
1303 | /* Some widgets may need a just-in-time visual update. */ | ||
1304 | notifyVisualOffsetChange_Root(root); | ||
1305 | root->didChangeArrangement = iFalse; | ||
1306 | } | ||
1307 | } | ||
1271 | if (isExposed_Window(w)) { | 1308 | if (isExposed_Window(w)) { |
1272 | w->isInvalidated = iFalse; | 1309 | w->isInvalidated = iFalse; |
1273 | extern int drawCount_; | 1310 | extern int drawCount_; |
@@ -1442,6 +1479,7 @@ iBool isOpenGLRenderer_Window(void) { | |||
1442 | } | 1479 | } |
1443 | 1480 | ||
1444 | void setKeyboardHeight_MainWindow(iMainWindow *d, int height) { | 1481 | void setKeyboardHeight_MainWindow(iMainWindow *d, int height) { |
1482 | height = iMax(0, height); | ||
1445 | if (d->keyboardHeight != height) { | 1483 | if (d->keyboardHeight != height) { |
1446 | d->keyboardHeight = height; | 1484 | d->keyboardHeight = height; |
1447 | postCommandf_App("keyboard.changed arg:%d", height); | 1485 | postCommandf_App("keyboard.changed arg:%d", height); |
@@ -1528,18 +1566,20 @@ void setSplitMode_MainWindow(iMainWindow *d, int splitFlags) { | |||
1528 | } | 1566 | } |
1529 | } | 1567 | } |
1530 | if (!isEmpty_String(d->pendingSplitUrl)) { | 1568 | if (!isEmpty_String(d->pendingSplitUrl)) { |
1531 | postCommandf_Root(w->roots[newRootIndex], "open url:%s", | 1569 | postCommandf_Root(w->roots[newRootIndex], "open origin:%s url:%s", |
1570 | cstr_String(d->pendingSplitOrigin), | ||
1532 | cstr_String(d->pendingSplitUrl)); | 1571 | cstr_String(d->pendingSplitUrl)); |
1533 | clear_String(d->pendingSplitUrl); | 1572 | clear_String(d->pendingSplitUrl); |
1573 | clear_String(d->pendingSplitOrigin); | ||
1534 | } | 1574 | } |
1535 | else if (~splitFlags & noEvents_WindowSplit) { | 1575 | else if (~splitFlags & noEvents_WindowSplit) { |
1536 | iWidget *docTabs0 = findChild_Widget(w->roots[newRootIndex ^ 1]->widget, "doctabs"); | 1576 | iWidget *docTabs0 = findChild_Widget(w->roots[newRootIndex ^ 1]->widget, "doctabs"); |
1537 | iWidget *docTabs1 = findChild_Widget(w->roots[newRootIndex]->widget, "doctabs"); | 1577 | iWidget *docTabs1 = findChild_Widget(w->roots[newRootIndex]->widget, "doctabs"); |
1538 | /* If the old root has multiple tabs, move the current one to the new split. */ | 1578 | /* If the old root has multiple tabs, move the current one to the new split. */ |
1539 | if (tabCount_Widget(docTabs0) >= 2) { | 1579 | if (tabCount_Widget(docTabs0) >= 2) { |
1540 | int movedIndex = tabPageIndex_Widget(docTabs0, moved); | 1580 | size_t movedIndex = tabPageIndex_Widget(docTabs0, moved); |
1541 | removeTabPage_Widget(docTabs0, movedIndex); | 1581 | removeTabPage_Widget(docTabs0, movedIndex); |
1542 | showTabPage_Widget(docTabs0, tabPage_Widget(docTabs0, iMax(movedIndex - 1, 0))); | 1582 | showTabPage_Widget(docTabs0, tabPage_Widget(docTabs0, iMax((int) movedIndex - 1, 0))); |
1543 | iRelease(removeTabPage_Widget(docTabs1, 0)); /* delete the default tab */ | 1583 | iRelease(removeTabPage_Widget(docTabs1, 0)); /* delete the default tab */ |
1544 | setRoot_Widget(as_Widget(moved), w->roots[newRootIndex]); | 1584 | setRoot_Widget(as_Widget(moved), w->roots[newRootIndex]); |
1545 | prependTabPage_Widget(docTabs1, iClob(moved), "", 0, 0); | 1585 | prependTabPage_Widget(docTabs1, iClob(moved), "", 0, 0); |
diff --git a/src/ui/window.h b/src/ui/window.h index 6c921f09..b4e348d2 100644 --- a/src/ui/window.h +++ b/src/ui/window.h | |||
@@ -114,8 +114,10 @@ struct Impl_MainWindow { | |||
114 | int splitMode; | 114 | int splitMode; |
115 | int pendingSplitMode; | 115 | int pendingSplitMode; |
116 | iString * pendingSplitUrl; /* URL to open in a newly opened split */ | 116 | iString * pendingSplitUrl; /* URL to open in a newly opened split */ |
117 | iString * pendingSplitOrigin; /* tab from where split was initiated, if any */ | ||
117 | SDL_Texture * appIcon; | 118 | SDL_Texture * appIcon; |
118 | int keyboardHeight; /* mobile software keyboards */ | 119 | int keyboardHeight; /* mobile software keyboards */ |
120 | int maxDrawableHeight; | ||
119 | }; | 121 | }; |
120 | 122 | ||
121 | iLocalDef enum iWindowType type_Window(const iAnyWindow *d) { | 123 | iLocalDef enum iWindowType type_Window(const iAnyWindow *d) { |
@@ -186,10 +188,10 @@ void setKeyboardHeight_MainWindow (iMainWindow *, int height); | |||
186 | void setSplitMode_MainWindow (iMainWindow *, int splitMode); | 188 | void setSplitMode_MainWindow (iMainWindow *, int splitMode); |
187 | void checkPendingSplit_MainWindow (iMainWindow *); | 189 | void checkPendingSplit_MainWindow (iMainWindow *); |
188 | void swapRoots_MainWindow (iMainWindow *); | 190 | void swapRoots_MainWindow (iMainWindow *); |
189 | void showToolbars_MainWindow (iMainWindow *, iBool show); | 191 | //void showToolbars_MainWindow (iMainWindow *, iBool show); |
190 | void resize_MainWindow (iMainWindow *, int w, int h); | 192 | void resize_MainWindow (iMainWindow *, int w, int h); |
191 | 193 | ||
192 | iBool processEvent_MainWindow (iMainWindow *, const SDL_Event *); | 194 | //iBool processEvent_MainWindow (iMainWindow *, const SDL_Event *); |
193 | void draw_MainWindow (iMainWindow *); | 195 | void draw_MainWindow (iMainWindow *); |
194 | void drawWhileResizing_MainWindow (iMainWindow *, int w, int h); /* workaround for SDL bug */ | 196 | void drawWhileResizing_MainWindow (iMainWindow *, int w, int h); /* workaround for SDL bug */ |
195 | 197 | ||
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) { | |||
105 | char *endp = NULL; | 105 | char *endp = NULL; |
106 | const unsigned long long ts = strtoull(line.start, &endp, 10); | 106 | const unsigned long long ts = strtoull(line.start, &endp, 10); |
107 | if (ts == 0) break; | 107 | if (ts == 0) break; |
108 | const uint32_t flags = strtoul(skipSpace_CStr(endp), &endp, 16); | 108 | const uint32_t flags = (uint32_t) strtoul(skipSpace_CStr(endp), &endp, 16); |
109 | const char *urlStart = skipSpace_CStr(endp); | 109 | const char *urlStart = skipSpace_CStr(endp); |
110 | iVisitedUrl item; | 110 | iVisitedUrl item; |
111 | item.when.ts = (struct timespec){ .tv_sec = ts }; | 111 | item.when.ts = (struct timespec){ .tv_sec = ts }; |