summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt9
-rw-r--r--po/de.po12
-rw-r--r--po/en.po40
-rw-r--r--po/eo.po5
-rw-r--r--po/es.po92
-rw-r--r--po/fi.po67
-rw-r--r--po/fr.po61
-rw-r--r--po/ia.po8
-rw-r--r--po/ie.po73
-rw-r--r--po/lt.po125
-rw-r--r--po/pl.po5
-rw-r--r--po/ru.po46
-rw-r--r--po/sr.po70
-rw-r--r--po/tok.po46
-rw-r--r--po/zh_Hant.po144
-rw-r--r--res/about/license.gmi2
-rw-r--r--res/about/version.gmi56
-rw-r--r--res/arg-help.txt8
-rw-r--r--res/fi.skyjake.Lagrange.appdata.xml48
-rw-r--r--res/fonts/LICENSE_SmolEmoji.txt94
-rw-r--r--res/fonts/NotoSansSymbols-Regular.ttfbin0 -> 168520 bytes
-rw-r--r--res/fonts/NotoSansSymbols2-Regular.ttfbin0 -> 583072 bytes
-rw-r--r--res/fonts/SmolEmoji-Regular.ttfbin0 -> 50904 bytes
-rw-r--r--res/fonts/Symbola.ttfbin3310696 -> 0 bytes
-rw-r--r--res/lang/de.binbin20305 -> 20699 bytes
-rw-r--r--res/lang/en.binbin19054 -> 19404 bytes
-rw-r--r--res/lang/es.binbin21021 -> 21778 bytes
-rw-r--r--res/lang/fi.binbin20995 -> 21901 bytes
-rw-r--r--res/lang/fr.binbin21511 -> 22311 bytes
-rw-r--r--res/lang/ia.binbin21212 -> 21609 bytes
-rw-r--r--res/lang/ie.binbin20383 -> 21167 bytes
-rw-r--r--res/lang/pl.binbin22016 -> 22415 bytes
-rw-r--r--res/lang/ru.binbin32094 -> 32646 bytes
-rw-r--r--res/lang/sr.binbin30673 -> 32051 bytes
-rw-r--r--res/lang/tok.binbin19417 -> 19792 bytes
-rw-r--r--res/lang/zh_Hans.binbin18176 -> 18568 bytes
-rw-r--r--res/lang/zh_Hant.binbin18250 -> 18753 bytes
-rw-r--r--src/app.c181
-rw-r--r--src/app.h3
-rw-r--r--src/defs.h7
-rw-r--r--src/feeds.c8
-rw-r--r--src/gempub.c56
-rw-r--r--src/gempub.h5
-rw-r--r--src/gmcerts.c27
-rw-r--r--src/gmcerts.h5
-rw-r--r--src/gmdocument.c17
-rw-r--r--src/gmdocument.h2
-rw-r--r--src/gmrequest.c4
-rw-r--r--src/gmrequest.h2
-rw-r--r--src/gmutil.c20
-rw-r--r--src/gmutil.h2
-rw-r--r--src/history.c5
-rw-r--r--src/ipc.c13
-rw-r--r--src/ipc.h3
-rw-r--r--src/macos.m3
-rw-r--r--src/periodic.c8
-rw-r--r--src/prefs.c8
-rw-r--r--src/prefs.h2
-rw-r--r--src/ui/certimportwidget.c11
-rw-r--r--src/ui/documentwidget.c429
-rw-r--r--src/ui/documentwidget.h1
-rw-r--r--src/ui/inputwidget.c4
-rw-r--r--src/ui/inputwidget.h4
-rw-r--r--src/ui/keys.c15
-rw-r--r--src/ui/keys.h2
-rw-r--r--src/ui/listwidget.c16
-rw-r--r--src/ui/listwidget.h1
-rw-r--r--src/ui/lookupwidget.c15
-rw-r--r--src/ui/mediaui.c12
-rw-r--r--src/ui/mobile.c53
-rw-r--r--src/ui/root.c27
-rw-r--r--src/ui/scrollwidget.c3
-rw-r--r--src/ui/sidebarwidget.c200
-rw-r--r--src/ui/text.c166
-rw-r--r--src/ui/text.h35
-rw-r--r--src/ui/translation.c4
-rw-r--r--src/ui/util.c67
-rw-r--r--src/ui/widget.c20
-rw-r--r--src/ui/widget.h1
-rw-r--r--src/ui/window.c17
80 files changed, 2127 insertions, 368 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index edee3d85..006a3df8 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -18,14 +18,14 @@
18cmake_minimum_required (VERSION 3.9) 18cmake_minimum_required (VERSION 3.9)
19 19
20project (Lagrange 20project (Lagrange
21 VERSION 1.5.0 21 VERSION 1.5.2
22 DESCRIPTION "A Beautiful Gemini Client" 22 DESCRIPTION "A Beautiful Gemini Client"
23 LANGUAGES C 23 LANGUAGES C
24) 24)
25set (COPYRIGHT_YEAR 2021) 25set (COPYRIGHT_YEAR 2021)
26if (IOS) 26if (IOS)
27 set (PROJECT_VERSION 1.4) # pinned for TestFlight 27 set (PROJECT_VERSION 1.4) # pinned for TestFlight
28 set (IOS_BUNDLE_VERSION 7) # just increment this 28 set (IOS_BUNDLE_VERSION 8) # just increment this
29endif () 29endif ()
30 30
31# Build configuration. 31# Build configuration.
@@ -72,17 +72,19 @@ set (EMBED_RESOURCES
72 res/fonts/NotoSansArabicUI-Regular.ttf 72 res/fonts/NotoSansArabicUI-Regular.ttf
73 res/fonts/NotoSansJP-Regular.ttf 73 res/fonts/NotoSansJP-Regular.ttf
74 res/fonts/NotoSansSC-Regular.ttf 74 res/fonts/NotoSansSC-Regular.ttf
75 res/fonts/NotoSansSymbols-Regular.ttf
76 res/fonts/NotoSansSymbols2-Regular.ttf
75 res/fonts/Nunito-Bold.ttf 77 res/fonts/Nunito-Bold.ttf
76 res/fonts/Nunito-ExtraBold.ttf 78 res/fonts/Nunito-ExtraBold.ttf
77 res/fonts/Nunito-ExtraLight.ttf 79 res/fonts/Nunito-ExtraLight.ttf
78 res/fonts/Nunito-LightItalic.ttf 80 res/fonts/Nunito-LightItalic.ttf
79 res/fonts/Nunito-Regular.ttf 81 res/fonts/Nunito-Regular.ttf
82 res/fonts/SmolEmoji-Regular.ttf
80 res/fonts/SourceSans3-Bold.ttf 83 res/fonts/SourceSans3-Bold.ttf
81 res/fonts/SourceSans3-ExtraLight.ttf 84 res/fonts/SourceSans3-ExtraLight.ttf
82 res/fonts/SourceSans3-It.ttf 85 res/fonts/SourceSans3-It.ttf
83 res/fonts/SourceSans3-Regular.ttf 86 res/fonts/SourceSans3-Regular.ttf
84 res/fonts/SourceSans3-Semibold.ttf 87 res/fonts/SourceSans3-Semibold.ttf
85 res/fonts/Symbola.ttf
86 res/lang/de.bin 88 res/lang/de.bin
87 res/lang/en.bin 89 res/lang/en.bin
88 res/lang/es.bin 90 res/lang/es.bin
@@ -396,6 +398,7 @@ Categories=Network
396Exec=${CMAKE_INSTALL_PREFIX}/bin/lagrange %U 398Exec=${CMAKE_INSTALL_PREFIX}/bin/lagrange %U
397Terminal=false 399Terminal=false
398Type=Application 400Type=Application
401StartupWMClass=lagrange
399Icon=fi.skyjake.Lagrange 402Icon=fi.skyjake.Lagrange
400MimeType=x-scheme-handler/gemini;x-scheme-handler/gopher 403MimeType=x-scheme-handler/gemini;x-scheme-handler/gopher
401") 404")
diff --git a/po/de.po b/po/de.po
index 63f395e7..3af2dff3 100644
--- a/po/de.po
+++ b/po/de.po
@@ -1,8 +1,8 @@
1msgid "" 1msgid ""
2msgstr "" 2msgstr ""
3"Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" 3"Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n"
4"PO-Revision-Date: 2021-05-06 09:39+0000\n" 4"PO-Revision-Date: 2021-05-15 06:18+0000\n"
5"Last-Translator: Jaakko Keränen <jaakko.keranen@iki.fi>\n" 5"Last-Translator: Nikolay Korotkiy <sikmir@gmail.com>\n"
6"Language-Team: German <http://weblate.skyjake.fi/projects/lagrange/ui/de/>\n" 6"Language-Team: German <http://weblate.skyjake.fi/projects/lagrange/ui/de/>\n"
7"Language: de\n" 7"Language: de\n"
8"MIME-Version: 1.0\n" 8"MIME-Version: 1.0\n"
@@ -1100,7 +1100,7 @@ msgid "prefs.mono"
1100msgstr "Körper in Monospace:" 1100msgstr "Körper in Monospace:"
1101 1101
1102msgid "prefs.linewidth" 1102msgid "prefs.linewidth"
1103msgstr "Zeilenbreite" 1103msgstr "Zeilenbreite:"
1104 1104
1105msgid "prefs.linewidth.fill" 1105msgid "prefs.linewidth.fill"
1106msgstr "Füllung" 1106msgstr "Füllung"
@@ -1208,3 +1208,9 @@ msgstr "SEITENINHALT"
1208 1208
1209msgid "heading.prefs.proxies" 1209msgid "heading.prefs.proxies"
1210msgstr "PROXIES" 1210msgstr "PROXIES"
1211
1212msgid "gempub.meta.lang"
1213msgstr "Sprache"
1214
1215msgid "lang.pl"
1216msgstr "Polnisch"
diff --git a/po/en.po b/po/en.po
index 60beade4..8fe6b96d 100644
--- a/po/en.po
+++ b/po/en.po
@@ -108,8 +108,8 @@ msgstr "Powered by SDL 2, OpenSSL, and ☕️"
108msgid "cancel" 108msgid "cancel"
109msgstr "Cancel" 109msgstr "Cancel"
110 110
111msgid "dismiss" 111msgid "close"
112msgstr "Dismiss" 112msgstr "Close"
113 113
114msgid "dlg.message.ok" 114msgid "dlg.message.ok"
115msgstr "Continue" 115msgstr "Continue"
@@ -191,6 +191,9 @@ msgstr "Save to Files"
191msgid "menu.save.downloads" 191msgid "menu.save.downloads"
192msgstr "Save to Downloads" 192msgstr "Save to Downloads"
193 193
194msgid "menu.save.downloads.open"
195msgstr "Save to Downloads and Open File"
196
194msgid "menu.sidebar" 197msgid "menu.sidebar"
195msgstr "Toggle Sidebar" 198msgstr "Toggle Sidebar"
196 199
@@ -542,8 +545,8 @@ msgstr "Stop Using on This Page"
542msgid "ident.stopuse.all" 545msgid "ident.stopuse.all"
543msgstr "Stop Using Everywhere" 546msgstr "Stop Using Everywhere"
544 547
545msgid "ident.showuse" 548msgid "ident.export"
546msgstr "Show Usage" 549msgstr "Export"
547 550
548msgid "heading.ident.use" 551msgid "heading.ident.use"
549msgstr "IDENTITY USAGE" 552msgstr "IDENTITY USAGE"
@@ -597,6 +600,9 @@ msgstr "Unsubscribe"
597msgid "error.unsupported.suggestsave" 600msgid "error.unsupported.suggestsave"
598msgstr "You can save it as a file to your Downloads folder: press %s or select \"%s\" from the menu." 601msgstr "You can save it as a file to your Downloads folder: press %s or select \"%s\" from the menu."
599 602
603msgid "error.server.msg"
604msgstr "Server responded with the message:"
605
600msgid "heading.pageinfo" 606msgid "heading.pageinfo"
601msgstr "PAGE INFORMATION" 607msgstr "PAGE INFORMATION"
602 608
@@ -916,7 +922,7 @@ msgid "dlg.newident.until"
916msgstr "Valid until:" 922msgstr "Valid until:"
917 923
918msgid "hint.newident.date" 924msgid "hint.newident.date"
919msgstr "YYYY-MM-DD HH:MM:SS" 925msgstr "YYYY or YYYY-MM-DD"
920 926
921msgid "hint.newident.optional" 927msgid "hint.newident.optional"
922msgstr "optional" 928msgstr "optional"
@@ -930,6 +936,18 @@ msgstr "Temporary:"
930msgid "dlg.newident.notsaved" 936msgid "dlg.newident.notsaved"
931msgstr "not saved to disk" 937msgstr "not saved to disk"
932 938
939msgid "dlg.newident.scope"
940msgstr "Use on:"
941
942msgid "dlg.newident.scope.domain"
943msgstr "Current Domain"
944
945msgid "dlg.newident.scope.page"
946msgstr "Current Page"
947
948msgid "dlg.newident.scope.none"
949msgstr "Not Used"
950
933msgid "dlg.newident.email" 951msgid "dlg.newident.email"
934msgstr "Email:" 952msgstr "Email:"
935 953
@@ -945,6 +963,9 @@ msgstr "Organization:"
945msgid "dlg.newident.country" 963msgid "dlg.newident.country"
946msgstr "Country:" 964msgstr "Country:"
947 965
966msgid "dlg.newident.more"
967msgstr "More…"
968
948msgid "dlg.newident.create" 969msgid "dlg.newident.create"
949msgstr "Create Identity" 970msgstr "Create Identity"
950 971
@@ -1139,6 +1160,9 @@ msgstr "UI scale factor:"
1139msgid "prefs.customframe" 1160msgid "prefs.customframe"
1140msgstr "Custom window frame:" 1161msgstr "Custom window frame:"
1141 1162
1163msgid "prefs.animate"
1164msgstr "Animations:"
1165
1142msgid "prefs.retainwindow" 1166msgid "prefs.retainwindow"
1143msgstr "Retain placement:" 1167msgstr "Retain placement:"
1144 1168
@@ -1201,6 +1225,9 @@ msgstr "On Dark"
1201msgid "prefs.boldlink.light" 1225msgid "prefs.boldlink.light"
1202msgstr "On Light" 1226msgstr "On Light"
1203 1227
1228msgid "prefs.userfont"
1229msgstr "Symbol font:"
1230
1204msgid "prefs.linewidth" 1231msgid "prefs.linewidth"
1205msgstr "Line width:" 1232msgstr "Line width:"
1206 1233
@@ -1366,6 +1393,9 @@ msgstr "Set view split mode"
1366msgid "keys.split.next" 1393msgid "keys.split.next"
1367msgstr "Switch focus to next split" 1394msgstr "Switch focus to next split"
1368 1395
1396msgid "keys.split.item"
1397msgstr "Split view menu:"
1398
1369msgid "keys.hoverurl" 1399msgid "keys.hoverurl"
1370msgstr "Toggle show URL on hover" 1400msgstr "Toggle show URL on hover"
1371 1401
diff --git a/po/eo.po b/po/eo.po
index 2ad7847c..2dd2cb79 100644
--- a/po/eo.po
+++ b/po/eo.po
@@ -1,7 +1,7 @@
1msgid "" 1msgid ""
2msgstr "" 2msgstr ""
3"Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" 3"Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n"
4"PO-Revision-Date: 2021-05-05 03:55+0000\n" 4"PO-Revision-Date: 2021-05-15 06:18+0000\n"
5"Last-Translator: Nikolay Korotkiy <sikmir@gmail.com>\n" 5"Last-Translator: Nikolay Korotkiy <sikmir@gmail.com>\n"
6"Language-Team: Esperanto <http://weblate.skyjake.fi/projects/lagrange/ui/eo/>" 6"Language-Team: Esperanto <http://weblate.skyjake.fi/projects/lagrange/ui/eo/>"
7"\n" 7"\n"
@@ -695,3 +695,6 @@ msgstr "Eldonita"
695 695
696msgid "gempub.meta.version" 696msgid "gempub.meta.version"
697msgstr "Versio" 697msgstr "Versio"
698
699msgid "lang.pl"
700msgstr "Pola"
diff --git a/po/es.po b/po/es.po
index 6edf2c41..9e540dc4 100644
--- a/po/es.po
+++ b/po/es.po
@@ -1,7 +1,7 @@
1msgid "" 1msgid ""
2msgstr "" 2msgstr ""
3"Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" 3"Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n"
4"PO-Revision-Date: 2021-05-05 03:55+0000\n" 4"PO-Revision-Date: 2021-06-04 14:18+0000\n"
5"Last-Translator: Wally Hackenslacker <mastor89@protonmail.com>\n" 5"Last-Translator: Wally Hackenslacker <mastor89@protonmail.com>\n"
6"Language-Team: Spanish <http://weblate.skyjake.fi/projects/lagrange/ui/es/>\n" 6"Language-Team: Spanish <http://weblate.skyjake.fi/projects/lagrange/ui/es/>\n"
7"Language: es\n" 7"Language: es\n"
@@ -919,7 +919,7 @@ msgid "dlg.newident.until"
919msgstr "Válido hasta:" 919msgstr "Válido hasta:"
920 920
921msgid "hint.newident.date" 921msgid "hint.newident.date"
922msgstr "YYYY-MM-DD HH:MM:SS" 922msgstr "YYYY ó YYYY-MM-DD"
923 923
924msgid "hint.newident.optional" 924msgid "hint.newident.optional"
925msgstr "opcional" 925msgstr "opcional"
@@ -1560,3 +1560,91 @@ msgstr "Idioma"
1560 1560
1561msgid "gempub.meta.license" 1561msgid "gempub.meta.license"
1562msgstr "Licencia" 1562msgstr "Licencia"
1563
1564msgid "lang.pl"
1565msgstr "Polaco"
1566
1567#, c-format
1568msgid "doc.archive"
1569msgstr "%s es un archivo comprimido."
1570
1571msgid "doc.archive.view"
1572msgstr "Ver el contenido del archivo"
1573
1574msgid "bookmark.tag.linksplit"
1575msgstr "Abrir enlaces a un lado"
1576
1577msgid "lang.tok"
1578msgstr "Toki Pona"
1579
1580msgid "archive.exit"
1581msgstr "Salir del archivo"
1582
1583#, c-format
1584msgid "archive.summary"
1585msgid_plural "archive.summary.n"
1586msgstr[0] "Este archivo contiene %zu item y su tamaño comprimido es de %.1f MB."
1587msgstr[1] "Este archivo contiene %zu items y su tamaño comprimido es de %.1f MB."
1588
1589#, c-format
1590msgid "dir.summary"
1591msgid_plural "dir.summary.n"
1592msgstr[0] "Este directorio contiene %zu item."
1593msgstr[1] "Este directorio contiene %zu items."
1594
1595msgid "keys.tab.close.other"
1596msgstr "Cerrar otras pestañas"
1597
1598msgid "prefs.pinsplit"
1599msgstr "Fijar pestaña para vista dividida:"
1600
1601msgid "prefs.pinsplit.none"
1602msgstr "Ninguna"
1603
1604msgid "prefs.pinsplit.left"
1605msgstr "Pestaña Izquierda"
1606
1607msgid "prefs.pinsplit.right"
1608msgstr "Pestaña Derecha"
1609
1610msgid "dir.empty"
1611msgstr "Este directorio está vacío."
1612
1613msgid "prefs.animate"
1614msgstr "Animaciones:"
1615
1616msgid "close"
1617msgstr "Cerrar"
1618
1619msgid "ident.export"
1620msgstr "Exportar"
1621
1622msgid "error.server.msg"
1623msgstr "El servidor respondió con el mensaje:"
1624
1625msgid "dlg.input.linebreak"
1626msgstr "Salto de linea"
1627
1628msgid "dlg.newident.scope"
1629msgstr "Usar en:"
1630
1631msgid "prefs.userfont"
1632msgstr "Fuente simbólica:"
1633
1634msgid "keys.split.item"
1635msgstr "Menú de vista dividida:"
1636
1637msgid "dlg.newident.scope.domain"
1638msgstr "Dominio Actual"
1639
1640msgid "dlg.newident.scope.page"
1641msgstr "Página Actual"
1642
1643msgid "dlg.newident.scope.none"
1644msgstr "Sin Usar"
1645
1646msgid "dlg.newident.more"
1647msgstr "Más…"
1648
1649msgid "menu.save.downloads.open"
1650msgstr "Guardar a Descargas y Abrir Archivo"
diff --git a/po/fi.po b/po/fi.po
index 60dfc33b..20b853f5 100644
--- a/po/fi.po
+++ b/po/fi.po
@@ -3,7 +3,7 @@ msgstr ""
3"Project-Id-Version: PACKAGE VERSION\n" 3"Project-Id-Version: PACKAGE VERSION\n"
4"Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" 4"Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n"
5"POT-Creation-Date: 2021-03-23 09:09+0000\n" 5"POT-Creation-Date: 2021-03-23 09:09+0000\n"
6"PO-Revision-Date: 2021-05-13 05:38+0000\n" 6"PO-Revision-Date: 2021-05-29 12:27+0000\n"
7"Last-Translator: Jaakko Keränen <jaakko.keranen@iki.fi>\n" 7"Last-Translator: Jaakko Keränen <jaakko.keranen@iki.fi>\n"
8"Language-Team: Finnish <http://weblate.skyjake.fi/projects/lagrange/ui/fi/>\n" 8"Language-Team: Finnish <http://weblate.skyjake.fi/projects/lagrange/ui/fi/>\n"
9"Language: fi\n" 9"Language: fi\n"
@@ -679,7 +679,7 @@ msgid "dlg.newident.until"
679msgstr "Voimassa:" 679msgstr "Voimassa:"
680 680
681msgid "hint.newident.date" 681msgid "hint.newident.date"
682msgstr "VVVV-KK-PP TT:MM:SS" 682msgstr "VVVV tai VVVV-KK-PP"
683 683
684msgid "hint.newident.optional" 684msgid "hint.newident.optional"
685msgstr "vapaaehtoinen" 685msgstr "vapaaehtoinen"
@@ -1579,3 +1579,66 @@ msgstr "Aktivoi seuraava jakaus"
1579 1579
1580msgid "lang.pl" 1580msgid "lang.pl"
1581msgstr "Puola" 1581msgstr "Puola"
1582
1583msgid "close"
1584msgstr "Sulje"
1585
1586msgid "ident.export"
1587msgstr "Vie"
1588
1589msgid "error.server.msg"
1590msgstr "Palvelin vastasi seuraavalla viestillä:"
1591
1592msgid "dlg.input.linebreak"
1593msgstr "Rivinvaihto"
1594
1595msgid "dlg.newident.scope"
1596msgstr "Käytössä:"
1597
1598msgid "dlg.newident.more"
1599msgstr "Lisää…"
1600
1601msgid "prefs.userfont"
1602msgstr "Kuvakefontti:"
1603
1604msgid "keys.split.menu.vert21"
1605msgstr "Jaetun näkymän valikko: pysty 2:1"
1606
1607msgid "dlg.newident.scope.domain"
1608msgstr "Tämä verkkotunnus"
1609
1610msgid "dlg.newident.scope.page"
1611msgstr "Tämä sivu"
1612
1613msgid "dlg.newident.scope.none"
1614msgstr "Ei käytetä"
1615
1616msgid "keys.split.menu.merge"
1617msgstr "Jaetun näkymän valikko: yhdistä välilehdet"
1618
1619msgid "keys.split.menu.swap"
1620msgstr "Jaetun näkymän valikko: vaihda puolia"
1621
1622msgid "keys.split.menu.horiz"
1623msgstr "Jaetun näkymän valikko: vaaka"
1624
1625msgid "keys.split.menu.horiz12"
1626msgstr "Jaetun näkymän valikko: vaaka 1:2"
1627
1628msgid "keys.split.menu.horiz21"
1629msgstr "Jaetun näkymän valikko: vaaka 2:1"
1630
1631msgid "keys.split.menu.vert"
1632msgstr "Jaetun näkymän valikko: pysty"
1633
1634msgid "keys.split.menu.vert12"
1635msgstr "Jaetun näkymän valikko: pysty 1:2"
1636
1637msgid "prefs.animate"
1638msgstr "Animaatiot:"
1639
1640msgid "menu.save.downloads.open"
1641msgstr "Tallenna latauskansioon ja avaa tiedosto"
1642
1643msgid "keys.split.item"
1644msgstr "Jaetun näkymän valikko:"
diff --git a/po/fr.po b/po/fr.po
index 29ed8118..e0522323 100644
--- a/po/fr.po
+++ b/po/fr.po
@@ -1,7 +1,7 @@
1msgid "" 1msgid ""
2msgstr "" 2msgstr ""
3"Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" 3"Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n"
4"PO-Revision-Date: 2021-05-05 03:55+0000\n" 4"PO-Revision-Date: 2021-05-21 08:18+0000\n"
5"Last-Translator: MCMic <come@chilliet.eu>\n" 5"Last-Translator: MCMic <come@chilliet.eu>\n"
6"Language-Team: French <http://weblate.skyjake.fi/projects/lagrange/ui/fr/>\n" 6"Language-Team: French <http://weblate.skyjake.fi/projects/lagrange/ui/fr/>\n"
7"Language: fr\n" 7"Language: fr\n"
@@ -158,7 +158,7 @@ msgstr "Copier l’empreinte"
158 158
159# Inline download status message. 159# Inline download status message.
160msgid "media.download.warnclose" 160msgid "media.download.warnclose"
161msgstr "Le téléchargement sera annulé si l'onglet est fermé." 161msgstr "Le téléchargement sera annulé si cet onglet est fermé."
162 162
163# Inline download status message. 163# Inline download status message.
164msgid "media.download.complete" 164msgid "media.download.complete"
@@ -1547,3 +1547,60 @@ msgstr "Licence"
1547 1547
1548msgid "gempub.cover.view" 1548msgid "gempub.cover.view"
1549msgstr "Voir le contenu du Gempub" 1549msgstr "Voir le contenu du Gempub"
1550
1551msgid "lang.pl"
1552msgstr "Polonais"
1553
1554msgid "link.side.newtab"
1555msgstr "Ouvrir le lien dans un nouvel onglet sur le côté"
1556
1557msgid "keys.tab.close.other"
1558msgstr "Fermer les autres onglets"
1559
1560msgid "doc.archive.view"
1561msgstr "Afficher le contenu de l’archive"
1562
1563#, c-format
1564msgid "doc.archive"
1565msgstr "%s est une archive compressée."
1566
1567msgid "bookmark.tag.linksplit"
1568msgstr "Ouverture des liens sur le côté"
1569
1570msgid "link.side"
1571msgstr "Ouvrir le lien sur le côté"
1572
1573msgid "lang.tok"
1574msgstr "Toki Pona"
1575
1576msgid "prefs.pinsplit"
1577msgstr "Épingler un des côtés :"
1578
1579msgid "prefs.pinsplit.none"
1580msgstr "Aucun"
1581
1582msgid "archive.exit"
1583msgstr "Sortir de l’archive"
1584
1585#, c-format
1586msgid "archive.summary"
1587msgid_plural "archive.summary.n"
1588msgstr[0] ""
1589"Cette archive contient %zu élément et sa taille compressée est de %.1f Mo."
1590msgstr[1] ""
1591"Cette archive contient %zu éléments et sa taille compressée est de %.1f Mo."
1592
1593msgid "dir.empty"
1594msgstr "Ce répertoire est vide."
1595
1596#, c-format
1597msgid "dir.summary"
1598msgid_plural "dir.summary.n"
1599msgstr[0] "Ce répertoire contient %zu élément."
1600msgstr[1] "Ce répertoire contient %zu éléments."
1601
1602msgid "menu.split.swap"
1603msgstr "Échanger les côtés"
1604
1605msgid "keys.split.menu"
1606msgstr "Définir le mode de vues séparées"
diff --git a/po/ia.po b/po/ia.po
index 5fc721f3..2ea0e277 100644
--- a/po/ia.po
+++ b/po/ia.po
@@ -1,7 +1,7 @@
1msgid "" 1msgid ""
2msgstr "" 2msgstr ""
3"Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" 3"Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n"
4"PO-Revision-Date: 2021-05-06 09:39+0000\n" 4"PO-Revision-Date: 2021-05-24 13:18+0000\n"
5"Last-Translator: Olga Smirnova <mistresssilvara@hotmail.com>\n" 5"Last-Translator: Olga Smirnova <mistresssilvara@hotmail.com>\n"
6"Language-Team: Interlingua <http://weblate.skyjake.fi/projects/lagrange/ui/" 6"Language-Team: Interlingua <http://weblate.skyjake.fi/projects/lagrange/ui/"
7"ia/>\n" 7"ia/>\n"
@@ -1549,3 +1549,9 @@ msgstr "Editor"
1549 1549
1550msgid "gempub.meta.pubdate" 1550msgid "gempub.meta.pubdate"
1551msgstr "Data de publication" 1551msgstr "Data de publication"
1552
1553msgid "lang.pl"
1554msgstr "Polonese"
1555
1556msgid "gempub.cover.viewlocal"
1557msgstr "On pote vider iste libro Gempub post que lo es salvate localmente."
diff --git a/po/ie.po b/po/ie.po
index 27782f11..13f94026 100644
--- a/po/ie.po
+++ b/po/ie.po
@@ -1,7 +1,7 @@
1msgid "" 1msgid ""
2msgstr "" 2msgstr ""
3"Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" 3"Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n"
4"PO-Revision-Date: 2021-05-06 09:39+0000\n" 4"PO-Revision-Date: 2021-06-04 14:18+0000\n"
5"Last-Translator: Olga Smirnova <mistresssilvara@hotmail.com>\n" 5"Last-Translator: Olga Smirnova <mistresssilvara@hotmail.com>\n"
6"Language-Team: Occidental <http://weblate.skyjake.fi/projects/lagrange/ui/ie/" 6"Language-Team: Occidental <http://weblate.skyjake.fi/projects/lagrange/ui/ie/"
7">\n" 7">\n"
@@ -783,7 +783,7 @@ msgid "dlg.newident.until"
783msgstr "Valid til:" 783msgstr "Valid til:"
784 784
785msgid "hint.newident.date" 785msgid "hint.newident.date"
786msgstr "AAAA-MM-DD HH:MM:SS" 786msgstr "AAAA o AAAA-MM-DD"
787 787
788msgid "hint.newident.optional" 788msgid "hint.newident.optional"
789msgstr "facultativ" 789msgstr "facultativ"
@@ -1589,3 +1589,72 @@ msgstr "Cluder li altri cartes"
1589 1589
1590msgid "keys.split.menu" 1590msgid "keys.split.menu"
1591msgstr "Selecter li mode de fension" 1591msgstr "Selecter li mode de fension"
1592
1593msgid "lang.pl"
1594msgstr "Polonesi"
1595
1596msgid "keys.split.menu.horiz21"
1597msgstr "Fendet vise: horizontalmen 2:1"
1598
1599msgid "close"
1600msgstr "Cluder"
1601
1602msgid "ident.export"
1603msgstr "Exportar"
1604
1605msgid "error.server.msg"
1606msgstr "Li servitor respondet per li missage:"
1607
1608msgid "dlg.input.linebreak"
1609msgstr "Rupter li linea"
1610
1611msgid "dlg.newident.scope"
1612msgstr "Usar sur:"
1613
1614msgid "dlg.newident.more"
1615msgstr "Plu…"
1616
1617msgid "prefs.userfont"
1618msgstr "Fonde simbolic:"
1619
1620msgid "dlg.newident.scope.domain"
1621msgstr "Li actual dominia"
1622
1623msgid "dlg.newident.scope.page"
1624msgstr "Li actual págine"
1625
1626msgid "dlg.newident.scope.none"
1627msgstr "Ne usat"
1628
1629msgid "keys.split.menu.vert"
1630msgstr "Fendet vise: verticalmen"
1631
1632msgid "keys.split.menu.vert12"
1633msgstr "Fendet vise: verticalmen 1:2"
1634
1635msgid "keys.split.menu.vert21"
1636msgstr "Fendet vise: verticalmen 2:1"
1637
1638msgid "keys.split.menu.merge"
1639msgstr "Fendet vise: unir cartes"
1640
1641msgid "keys.split.menu.horiz"
1642msgstr "Fendet vise: horizontalmen"
1643
1644msgid "keys.split.menu.horiz12"
1645msgstr "Fendet vise: horizontalmen 1:2"
1646
1647msgid "prefs.animate"
1648msgstr "Animationes:"
1649
1650msgid "keys.split.item"
1651msgstr "Menú de fendet vise:"
1652
1653msgid "menu.save.downloads.open"
1654msgstr "Gardar al Descargates e aperter"
1655
1656msgid "link.side.newtab"
1657msgstr "Aperter in un nov carte láteralmen"
1658
1659msgid "prefs.pinsplit"
1660msgstr "Fixation de fendet vise:"
diff --git a/po/lt.po b/po/lt.po
new file mode 100644
index 00000000..ee536e7f
--- /dev/null
+++ b/po/lt.po
@@ -0,0 +1,125 @@
1msgid ""
2msgstr ""
3"Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n"
4"PO-Revision-Date: 2021-06-04 14:18+0000\n"
5"Last-Translator: Arns Udovič <zordsdavini@arns.lt>\n"
6"Language-Team: Lithuanian <http://weblate.skyjake.fi/projects/lagrange/ui/lt/"
7">\n"
8"Language: lt\n"
9"MIME-Version: 1.0\n"
10"Content-Type: text/plain; charset=UTF-8\n"
11"Content-Transfer-Encoding: 8bit\n"
12"Plural-Forms: nplurals=3; plural=(n % 10 == 1 && (n % 100 < 11 || n % 100 > "
13"19)) ? 0 : ((n % 10 >= 2 && n % 10 <= 9 && (n % 100 < 11 || n % 100 > 19)) ? "
14"1 : 2);\n"
15"X-Generator: Weblate 4.5.1\n"
16
17msgid "doc.pre.nocaption"
18msgstr "Neformatuotas tekstas be aprašymo"
19
20# Link download progress message.
21msgid "doc.fetching"
22msgstr "Kraunama"
23
24#, c-format
25msgid "doc.archive"
26msgstr "%s yra suspaustas archyvas."
27
28msgid "doc.archive.view"
29msgstr "Peržiūrėti archyvo turinį"
30
31# Inline download status message.
32msgid "media.download.warnclose"
33msgstr "Parsiuntimas bus nutrauktas, jei uždarysite skirtuką."
34
35# Inline download status message.
36msgid "media.download.complete"
37msgstr "Parsiuntimas baigtas."
38
39# Used in inline audio player metadata popup.
40msgid "audio.meta.title"
41msgstr "Pavadinimas"
42
43# Used in inline audio player metadata popup.
44msgid "audio.meta.artist"
45msgstr "Atlikėjas"
46
47# Used in inline audio player metadata popup.
48msgid "audio.meta.genre"
49msgstr "Žanras"
50
51# Used in inline audio player metadata popup.
52msgid "audio.meta.date"
53msgstr "Data"
54
55# Hertz, unit for frequency values
56msgid "hz"
57msgstr "Hz"
58
59msgid "feeds.list.refreshtime.now"
60msgstr "Paskutinis atnaujinimas įvyko vos prieš akimirką."
61
62#, c-format
63msgid "feeds.list.refreshtime"
64msgstr "Paskutinis atnaujinimas įvyko %s."
65
66#, c-format
67msgid "minutes.ago"
68msgid_plural "minutes.ago.n"
69msgstr[0] "prieš %d minutę"
70msgstr[1] "prieš %d minutes"
71msgstr[2] "prieš %d minučių"
72
73#, c-format
74msgid "hours.ago"
75msgid_plural "hours.ago.n"
76msgstr[0] "prieš %d valandą"
77msgstr[1] "preš %d valandas"
78msgstr[2] "prieš %d valandų"
79
80#, c-format
81msgid "days.ago"
82msgid_plural "days.ago.n"
83msgstr[0] "prieš %d dieną"
84msgstr[1] "prieš %d dienas"
85msgstr[2] "prieš %d dienų"
86
87# Alt-text of the preformatted logo.
88msgid "about.logo"
89msgstr "ASCII menas: žodis „Lagrange“, kuriame naudojamas didelis šriftas"
90
91msgid "about.tagline"
92msgstr "Graži Gemini naršyklė"
93
94msgid "about.version"
95msgstr "Versija"
96
97msgid "cancel"
98msgstr "Atšaukti"
99
100msgid "close"
101msgstr "Uždaryti"
102
103msgid "dlg.message.ok"
104msgstr "Tęsti"
105
106msgid "dlg.default"
107msgstr " OK "
108
109msgid "toggle.yes"
110msgstr "Taip"
111
112msgid "toggle.no"
113msgstr "Ne"
114
115msgid "menu.title.file"
116msgstr "Failas"
117
118msgid "menu.title.edit"
119msgstr "Redaguoti"
120
121msgid "menu.title.bookmarks"
122msgstr "Žymės"
123
124msgid "gempub.meta.license"
125msgstr "Licenzija"
diff --git a/po/pl.po b/po/pl.po
index 01e295b8..db8de568 100644
--- a/po/pl.po
+++ b/po/pl.po
@@ -1,7 +1,7 @@
1msgid "" 1msgid ""
2msgstr "" 2msgstr ""
3"Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" 3"Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n"
4"PO-Revision-Date: 2021-05-13 04:58+0000\n" 4"PO-Revision-Date: 2021-05-21 08:18+0000\n"
5"Last-Translator: Waterrail <maksymiliankrol03@gmail.com>\n" 5"Last-Translator: Waterrail <maksymiliankrol03@gmail.com>\n"
6"Language-Team: Polish <http://weblate.skyjake.fi/projects/lagrange/ui/pl/>\n" 6"Language-Team: Polish <http://weblate.skyjake.fi/projects/lagrange/ui/pl/>\n"
7"Language: pl\n" 7"Language: pl\n"
@@ -1627,3 +1627,6 @@ msgstr ""
1627"Żądanie proxy nie powiodło się, ponieważ serwer nie był w stanie pomyślnie " 1627"Żądanie proxy nie powiodło się, ponieważ serwer nie był w stanie pomyślnie "
1628"zakończyć transakcji ze zdalnym hostem. Mogły wystąpić problemy z łącznością " 1628"zakończyć transakcji ze zdalnym hostem. Mogły wystąpić problemy z łącznością "
1629"sieciową." 1629"sieciową."
1630
1631msgid "lang.pl"
1632msgstr "polski"
diff --git a/po/ru.po b/po/ru.po
index adab62c6..e43fa4af 100644
--- a/po/ru.po
+++ b/po/ru.po
@@ -3,7 +3,7 @@ msgstr ""
3"Project-Id-Version: PACKAGE VERSION\n" 3"Project-Id-Version: PACKAGE VERSION\n"
4"Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" 4"Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n"
5"POT-Creation-Date: 2021-03-23 19:02+0000\n" 5"POT-Creation-Date: 2021-03-23 19:02+0000\n"
6"PO-Revision-Date: 2021-05-06 09:39+0000\n" 6"PO-Revision-Date: 2021-06-01 07:18+0000\n"
7"Last-Translator: jan Anja <cyber@sysrq.in>\n" 7"Last-Translator: jan Anja <cyber@sysrq.in>\n"
8"Language-Team: Russian <http://weblate.skyjake.fi/projects/lagrange/ui/ru/>\n" 8"Language-Team: Russian <http://weblate.skyjake.fi/projects/lagrange/ui/ru/>\n"
9"Language: ru\n" 9"Language: ru\n"
@@ -688,7 +688,7 @@ msgid "dlg.newident.until"
688msgstr "Действителен до:" 688msgstr "Действителен до:"
689 689
690msgid "hint.newident.date" 690msgid "hint.newident.date"
691msgstr "ГГГГ--Д :ММ:" 691msgstr "ГГГГ -М-"
692 692
693msgid "hint.newident.optional" 693msgid "hint.newident.optional"
694msgstr "необязательно" 694msgstr "необязательно"
@@ -1595,3 +1595,45 @@ msgstr[2] "Каталог содержит %zu элементов."
1595 1595
1596msgid "keys.tab.close.other" 1596msgid "keys.tab.close.other"
1597msgstr "Закрыть другие владки" 1597msgstr "Закрыть другие владки"
1598
1599msgid "lang.pl"
1600msgstr "Польский"
1601
1602msgid "ident.export"
1603msgstr "Экспорт"
1604
1605msgid "close"
1606msgstr "Закрыть"
1607
1608msgid "error.server.msg"
1609msgstr "Сообщение от сервера:"
1610
1611msgid "dlg.input.linebreak"
1612msgstr "Перенос строки"
1613
1614msgid "dlg.newident.scope.domain"
1615msgstr "Текущий домен"
1616
1617msgid "prefs.animate"
1618msgstr "Анимации:"
1619
1620msgid "keys.split.item"
1621msgstr "Меню действий с окном:"
1622
1623msgid "prefs.userfont"
1624msgstr "Шрифт TrueType:"
1625
1626msgid "dlg.newident.scope"
1627msgstr "Область использования:"
1628
1629msgid "dlg.newident.more"
1630msgstr "Ещё…"
1631
1632msgid "menu.save.downloads.open"
1633msgstr "Сохранить и открыть"
1634
1635msgid "dlg.newident.scope.page"
1636msgstr "Текущая страница"
1637
1638msgid "dlg.newident.scope.none"
1639msgstr "Нигде"
diff --git a/po/sr.po b/po/sr.po
index d456fa24..edec39f5 100644
--- a/po/sr.po
+++ b/po/sr.po
@@ -1,7 +1,7 @@
1msgid "" 1msgid ""
2msgstr "" 2msgstr ""
3"Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" 3"Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n"
4"PO-Revision-Date: 2021-05-06 15:03+0000\n" 4"PO-Revision-Date: 2021-06-01 07:18+0000\n"
5"Last-Translator: Страхиња Радић <contact@strahinja.org>\n" 5"Last-Translator: Страхиња Радић <contact@strahinja.org>\n"
6"Language-Team: Serbian <http://weblate.skyjake.fi/projects/lagrange/ui/sr/>\n" 6"Language-Team: Serbian <http://weblate.skyjake.fi/projects/lagrange/ui/sr/>\n"
7"Language: sr\n" 7"Language: sr\n"
@@ -503,7 +503,7 @@ msgid "dlg.newident.until"
503msgstr "Важи до:" 503msgstr "Важи до:"
504 504
505msgid "hint.newident.date" 505msgid "hint.newident.date"
506msgstr "ГГГГ--Д :ММ:" 506msgstr "ГГГГ Г-М-"
507 507
508msgid "hint.newident.optional" 508msgid "hint.newident.optional"
509msgstr "необавезно" 509msgstr "необавезно"
@@ -1616,3 +1616,69 @@ msgstr[2] "Овај директоријум садржи %zu ставки."
1616 1616
1617msgid "keys.tab.close.other" 1617msgid "keys.tab.close.other"
1618msgstr "Затварање осталих картица" 1618msgstr "Затварање осталих картица"
1619
1620msgid "lang.pl"
1621msgstr "Пољски"
1622
1623msgid "dlg.newident.scope.domain"
1624msgstr "Текућем домену"
1625
1626msgid "dlg.newident.scope.page"
1627msgstr "Текућој страници"
1628
1629msgid "dlg.newident.scope.none"
1630msgstr "Не користити"
1631
1632msgid "keys.split.menu.merge"
1633msgstr "Мени раздвојеног погледа: Спој картице"
1634
1635msgid "keys.split.menu.swap"
1636msgstr "Мени раздвојеног погледа: Замени стране"
1637
1638msgid "keys.split.menu.horiz"
1639msgstr "Мени раздвојеног погледа: Хоризонтално"
1640
1641msgid "keys.split.menu.horiz12"
1642msgstr "Мени раздвојеног погледа: Хоризонтално 1:2"
1643
1644msgid "keys.split.menu.horiz21"
1645msgstr "Мени раздвојеног погледа: Хоризонтално 2:1"
1646
1647msgid "keys.split.menu.vert"
1648msgstr "Мени раздвојеног погледа: Вертикално"
1649
1650msgid "keys.split.menu.vert12"
1651msgstr "Мени раздвојеног погледа: Вертикално 1:2"
1652
1653msgid "keys.split.menu.vert21"
1654msgstr "Мени раздвојеног погледа: Вертикално 2:1"
1655
1656msgid "dlg.newident.more"
1657msgstr "Више…"
1658
1659msgid "dlg.newident.scope"
1660msgstr "Користити на:"
1661
1662msgid "prefs.userfont"
1663msgstr "Фонт за симболе:"
1664
1665msgid "close"
1666msgstr "Затвори"
1667
1668msgid "ident.export"
1669msgstr "Извези"
1670
1671msgid "error.server.msg"
1672msgstr "Сервер је одговорио поруком:"
1673
1674msgid "dlg.input.linebreak"
1675msgstr "Прелом реда"
1676
1677msgid "prefs.animate"
1678msgstr "Анимације:"
1679
1680msgid "keys.split.item"
1681msgstr "Мени раздвојеног погледа:"
1682
1683msgid "menu.save.downloads.open"
1684msgstr "Сачувај у фолдеру за преузимања и отвори"
diff --git a/po/tok.po b/po/tok.po
index 79d31fd1..1e6e890d 100644
--- a/po/tok.po
+++ b/po/tok.po
@@ -1,7 +1,7 @@
1msgid "" 1msgid ""
2msgstr "" 2msgstr ""
3"Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" 3"Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n"
4"PO-Revision-Date: 2021-05-10 06:26+0000\n" 4"PO-Revision-Date: 2021-06-01 07:18+0000\n"
5"Last-Translator: jan Anja <cyber@sysrq.in>\n" 5"Last-Translator: jan Anja <cyber@sysrq.in>\n"
6"Language-Team: Toki Pona <http://weblate.skyjake.fi/projects/lagrange/ui/tok/" 6"Language-Team: Toki Pona <http://weblate.skyjake.fi/projects/lagrange/ui/tok/"
7">\n" 7">\n"
@@ -877,7 +877,7 @@ msgid "prefs.saturation"
877msgstr "kule:" 877msgstr "kule:"
878 878
879msgid "prefs.headingfont" 879msgid "prefs.headingfont"
880msgstr "nasin lukin pi sitelen suli:" 880msgstr "nasin lukin tawa sitelen suli:"
881 881
882msgid "prefs.font" 882msgid "prefs.font"
883msgstr "nasin lukin tawa sitelen:" 883msgstr "nasin lukin tawa sitelen:"
@@ -1572,3 +1572,45 @@ msgstr[0] "ijo %zu li lon poki ni."
1572 1572
1573msgid "keys.tab.close.other" 1573msgid "keys.tab.close.other"
1574msgstr "o pini e poki ante" 1574msgstr "o pini e poki ante"
1575
1576msgid "lang.pl"
1577msgstr "toki Posuka"
1578
1579msgid "prefs.userfont"
1580msgstr "sitelen TTF:"
1581
1582msgid "dlg.newident.scope.page"
1583msgstr "lipu ni"
1584
1585msgid "dlg.newident.scope.none"
1586msgstr "lipu ala"
1587
1588msgid "dlg.newident.scope.domain"
1589msgstr "mun ni"
1590
1591msgid "prefs.animate"
1592msgstr "tawa namako:"
1593
1594msgid "menu.save.downloads.open"
1595msgstr "o awen li open e lipu"
1596
1597msgid "keys.split.item"
1598msgstr "ijo kipisi:"
1599
1600msgid "ident.export"
1601msgstr "o pana"
1602
1603msgid "error.server.msg"
1604msgstr "ilo pana li toki e ni:"
1605
1606msgid "dlg.input.linebreak"
1607msgstr "linja sin"
1608
1609msgid "close"
1610msgstr "o pini"
1611
1612msgid "dlg.newident.more"
1613msgstr "namako…"
1614
1615msgid "dlg.newident.scope"
1616msgstr "o kepeken e ona lon:"
diff --git a/po/zh_Hant.po b/po/zh_Hant.po
index 93119373..26f1377a 100644
--- a/po/zh_Hant.po
+++ b/po/zh_Hant.po
@@ -1,7 +1,7 @@
1msgid "" 1msgid ""
2msgstr "" 2msgstr ""
3"Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n" 3"Report-Msgid-Bugs-To: jaakko.keranen@iki.fi\n"
4"PO-Revision-Date: 2021-04-14 09:18+0000\n" 4"PO-Revision-Date: 2021-05-18 04:18+0000\n"
5"Last-Translator: Shibo Lyu <github@of.sb>\n" 5"Last-Translator: Shibo Lyu <github@of.sb>\n"
6"Language-Team: Chinese (Traditional) <http://weblate.skyjake.fi/projects/" 6"Language-Team: Chinese (Traditional) <http://weblate.skyjake.fi/projects/"
7"lagrange/ui/zh_Hant/>\n" 7"lagrange/ui/zh_Hant/>\n"
@@ -1408,3 +1408,145 @@ msgstr "建立…"
1408 1408
1409msgid "sidebar.action.ident.import" 1409msgid "sidebar.action.ident.import"
1410msgstr "匯入…" 1410msgstr "匯入…"
1411
1412#, c-format
1413msgid "dir.summary"
1414msgid_plural "dir.summary.n"
1415msgstr[0] "該檔案夾內含 %zu 項。"
1416
1417msgid "lang.pl"
1418msgstr "波蘭語"
1419
1420# User preference that controls whether index.gmi pages get automatically opened when browsing the contents of a directory inside a compressed archive.
1421msgid "prefs.archive.openindex"
1422msgstr "檢視存檔內檔案夾時使用 index.gmi:"
1423
1424#, c-format
1425msgid "archive.summary"
1426msgid_plural "archive.summary.n"
1427msgstr[0] "該存檔內含 %zu 項,壓縮後共 %.1f MB。"
1428
1429msgid "keys.split.menu"
1430msgstr "設定分屏模式"
1431
1432msgid "menu.downloads"
1433msgstr "顯示下載檔案夾"
1434
1435#, c-format
1436msgid "doc.archive"
1437msgstr "%s 是一個壓縮檔。"
1438
1439msgid "doc.archive.view"
1440msgstr "檢視存檔內容"
1441
1442msgid "bookmark.tag.linksplit"
1443msgstr "在側邊打開連結"
1444
1445msgid "dlg.save.opendownload"
1446msgstr "打開下載的檔案"
1447
1448msgid "link.side"
1449msgstr "在側邊打開連結"
1450
1451msgid "link.side.newtab"
1452msgstr "在側邊新標籤頁中打開連結"
1453
1454msgid "lang.ia"
1455msgstr "因特語"
1456
1457msgid "lang.tok"
1458msgstr "道本語"
1459
1460msgid "heading.newident.missing"
1461msgstr "資料缺失"
1462
1463msgid "dlg.newindent.missing.commonname"
1464msgstr "必須指定一個「通用名稱(CN)」。"
1465
1466msgid "heading.newident.date.bad"
1467msgstr "無效日期"
1468
1469msgid "dlg.newident.date.past"
1470msgstr "過期日必須在未來。"
1471
1472msgid "dlg.newident.date.example"
1473msgstr ""
1474"請檢查「有效期至」欄位。例子:\n"
1475"• 2030\n"
1476"• 2025-06-30\n"
1477"• 2021-12-31 23:59:59"
1478
1479msgid "menu.split.merge"
1480msgstr "合併標籤頁"
1481
1482msgid "menu.split.swap"
1483msgstr "交換位置"
1484
1485msgid "menu.split.horizontal"
1486msgstr "橫向"
1487
1488msgid "menu.split.vertical"
1489msgstr "縱向"
1490
1491msgid "menu.view.split"
1492msgstr "分屏…"
1493
1494msgid "gempub.cover.untitled"
1495msgstr "未名書"
1496
1497msgid "prefs.pinsplit"
1498msgstr "總在側邊打開連結:"
1499
1500msgid "prefs.pinsplit.none"
1501msgstr "關閉"
1502
1503msgid "prefs.pinsplit.left"
1504msgstr "左"
1505
1506msgid "prefs.pinsplit.right"
1507msgstr "右"
1508
1509msgid "archive.exit"
1510msgstr "退出存檔"
1511
1512msgid "dir.empty"
1513msgstr "檔案夾為空。"
1514
1515msgid "keys.tab.close.other"
1516msgstr "關閉其他標籤頁"
1517
1518msgid "keys.split.next"
1519msgstr "切換焦點至下一分屏"
1520
1521msgid "gempub.cover.viewlocal"
1522msgstr "該 Gempub 書籍可以在被儲存後閱讀。"
1523
1524msgid "gempub.cover.aboutbook"
1525msgstr "有關本書"
1526
1527msgid "gempub.cover.view"
1528msgstr "檢視 Gempub 目錄"
1529
1530msgid "gempub.cover.image"
1531msgstr "封面圖"
1532
1533msgid "gempub.meta.author"
1534msgstr "作者"
1535
1536msgid "gempub.meta.version"
1537msgstr "版本"
1538
1539msgid "gempub.meta.revdate"
1540msgstr "修訂日期"
1541
1542msgid "gempub.meta.pub"
1543msgstr "已發行"
1544
1545msgid "gempub.meta.pubdate"
1546msgstr "發行日期"
1547
1548msgid "gempub.meta.lang"
1549msgstr "語言"
1550
1551msgid "gempub.meta.license"
1552msgstr "授權協議"
diff --git a/res/about/license.gmi b/res/about/license.gmi
index 087ce3cf..6721ab9b 100644
--- a/res/about/license.gmi
+++ b/res/about/license.gmi
@@ -107,10 +107,10 @@ This application uses fonts licensed under the Open Font License.
107=> https://www.google.com/get/noto/#sans-arab Noto Sans Arabic 107=> https://www.google.com/get/noto/#sans-arab Noto Sans Arabic
108=> https://www.google.com/get/noto/help/cjk/ Noto Sans CJK (JP, SC) 108=> https://www.google.com/get/noto/help/cjk/ Noto Sans CJK (JP, SC)
109=> https://github.com/googlefonts/nunito/blob/master/OFL.txt Nunito 109=> https://github.com/googlefonts/nunito/blob/master/OFL.txt Nunito
110=> https://github.com/skyjake/lagrange/blob/dev/res/fonts/LICENSE_SmolEmoji.txt Smol Emoji
110=> https://github.com/adobe-fonts/source-sans/blob/release/LICENSE.md Source Sans 3 111=> https://github.com/adobe-fonts/source-sans/blob/release/LICENSE.md Source Sans 3
111 112
112Additional fonts: 113Additional fonts:
113 114
114=> https://github.com/googlefonts/noto-emoji/blob/master/LICENSE Noto Emoji (Apache License 2.0) 115=> https://github.com/googlefonts/noto-emoji/blob/master/LICENSE Noto Emoji (Apache License 2.0)
115=> https://dn-works.com/ufas/ Symbola (Public Domain)
116=> https://fonts.google.com/specimen/Tinos#license Tinos (Apache License 2.0) 116=> https://fonts.google.com/specimen/Tinos#license Tinos (Apache License 2.0)
diff --git a/res/about/version.gmi b/res/about/version.gmi
index 23eeebef..0a51df34 100644
--- a/res/about/version.gmi
+++ b/res/about/version.gmi
@@ -6,6 +6,62 @@
6``` 6```
7# Release notes 7# Release notes
8 8
9## 1.5.2
10* Fixed pasting a PEM-formatted certificate and/or private key via clipboard in Import Identity.
11* Normalize page contents (NFC) to avoid most common issues with diacritics.
12
13## 1.5.1
14* Updated UI translations.
15* Updated "Smol Emoji" font with new and improved glyphs.
16
17## 1.5
18* Added "Smol Emoji" and "Noto Sans Symbols" fonts, removed Symbola.
19⚠️ Many Emoji and pictographs defined in the last five years are currently missing.
20* Added document footer buttons: on certain pages (e.g., error messages) show relevant actions in the bottom of the page. For example, if a certificate is required for viewing a page, show buttons for creating a new identity and showing the Identities sidebar.
21* Error pages include the human-readable text sent by the server.
22* Disregard old feed entries whose unread status would have been forgotten.
23* Added UI language: Polish.
24
25Identity management:
26* Revised New Identity dialog. An option is provided to automatically use the new identity on the current domain/page. The additional fields are hidden by default.
27* Improved usability of Identities sidebar. No more accidental activations: left-clicking an identity opens the context menu without making any changes. The context menu shows each active URL as a menu item for easy access. Identity icons reflect the usage status: all identities used on the current domain get highlighted in addition to the currently used one.
28* Identities can be exported: certificate and private key are opened in a new tab in plain text PEM format.
29* Fixed issues with identity usage: a higher-up URL overrides and deactivates all contained URLs to avoid redundant activation.
30
31Text input:
32* Revised text input widgets: added support for multiple lines, and when entering user response to a query, show how many bytes are remaining for the response URL about to be submitted. In dialogs, input fields expand vertically instead of scrolling their content horizontally.
33* Input widgets allow inserting newlines using Shift+Return.
34* Disallow sending query responses that are too long (1024 bytes maximum).
35* Shift-click to select a range of text in input widgets (i.e, without dragging).
36
37Rendering:
38* Animate showing and hiding of sidebars and dialogs. Animations are enabled by default, by can be disabled with Preferences > Interface > Animations.
39* Added setting for a custom TrueType symbol font for any missing characters. Note: Must be a .TTF file — OpenType and bitmap fonts are not supported.
40* Link navigation shortcut icons (home row and numbered) are drawn with a consistent appearance.
41* Improved icon alignment in lists.
42* Reduced line gap between word-wrapped top-level headings.
43* Modal dialog background dimming fades in/out smoothly.
44* macOS: Workaround for an issue that causes UI refresh to pause occasionally for ~100 ms.
45
46Split view:
47* Added keybindings for split view menu items.
48* Changed default split view keys to conform to Emacs (3 for horizontal, 2 for vertical split).
49* Fixes and improvements for touch screen event handling in split view mode.
50
51Command line:
52* Added --url-or-search (-u) command line option. Depending on the parameter, either open an URL or make a search query.
53* Open all URLs/files specified on the command line in new tabs, and raise the window if the app is already running. (Kudos to Alyssa Rosenzweig.)
54
55Gempub:
56* Linear navigation through the book with Left/Right arrow keys and via footer buttons. The navigation order is determined by links on the Gempub index page.
57
58## 1.4.2
59* Fixed UI colors being all black on the first run.
60* Fixed right mouse click on an inactive split not having any effect.
61* Fixed action buttons showing under the Help link in an empty Identities sidebar.
62* Fixed potential crash at shutdown.
63* Fixed minor UI layout issues.
64
9## 1.4.1 65## 1.4.1
10* Fixed removing the left side split by closing all its tabs. The URL input field got confused about which tab was currently open, and the wrong theme was active. 66* Fixed removing the left side split by closing all its tabs. The URL input field got confused about which tab was currently open, and the wrong theme was active.
11* Fixed tab merging when unsplitting the window: keep the currently active tab open. 67* Fixed tab merging when unsplitting the window: keep the currently active tab open.
diff --git a/res/arg-help.txt b/res/arg-help.txt
index d8e8c76c..e87881d3 100644
--- a/res/arg-help.txt
+++ b/res/arg-help.txt
@@ -1,13 +1,17 @@
1Usage: lagrange [options] [URLs] [paths] 1Usage: lagrange [options] [URLs] [paths]
2 2
3URLs and local files are opened in separate tabs. By default the first 3When multiple URLs and local files are specified, they are opened in
4URL/path opens in the currently open tab. 4separate tabs.
5 5
6General options: 6General options:
7 7
8 -E, --echo Print all internal app events to stdout. 8 -E, --echo Print all internal app events to stdout.
9 --help Print these instructions. 9 --help Print these instructions.
10 --sw Disable hardware accelerated rendering. 10 --sw Disable hardware accelerated rendering.
11 -u, --url-or-search URL | text
12 Open a URL, or make a search query with given text.
13 This only works if the search query URL has been
14 configured.
11 -V, --version Print the application version. 15 -V, --version Print the application version.
12 16
13Options that control a running instance of Lagrange: 17Options that control a running instance of Lagrange:
diff --git a/res/fi.skyjake.Lagrange.appdata.xml b/res/fi.skyjake.Lagrange.appdata.xml
index e3b75771..8f8ca7b3 100644
--- a/res/fi.skyjake.Lagrange.appdata.xml
+++ b/res/fi.skyjake.Lagrange.appdata.xml
@@ -45,6 +45,54 @@
45 <update_contact>jaakko.keranen@iki.fi</update_contact> 45 <update_contact>jaakko.keranen@iki.fi</update_contact>
46 46
47 <releases> 47 <releases>
48 <release version="1.5.1" date="2021-06-06">
49 <description>
50 <p>Resource update:</p>
51 <ul>
52 <li>Latest UI translation strings.</li>
53 <li>More glyphs in the Smol Emoji font.</li>
54 </ul>
55 </description>
56 <url>https://github.com/skyjake/lagrange/releases/tag/v1.5.1</url>
57 </release>
58 <release version="1.5" date="2021-05-29">
59 <description>
60 <p>This release contains several user interface improvements.</p>
61 <ul>
62 <li>Improved identity management: revised certificate creation UI,
63 taking a new identity into use immediately, sidebar context
64 menu shows used URLs as clickable menu items, exporting
65 identities.</li>
66 <li>Improved text input: inserting line breaks, input fields use word
67 wrapping and expand vertically, query URL length
68 indicator.</li>
69 <li>Show buttons in the page footer area for performing relevant
70 actions conveniently.</li>
71 <li>Animated showing of sidebars and dialogs.</li>
72 <li>Font updates, setting for custom fallback TrueType font.</li>
73 <li>Gempub: Linear navigation through the book.</li>
74 <li>New UI language: Polish.</li>
75 </ul>
76 <p>The full release notes can be viewed inside the app by opening
77 the "about:version" page.</p>
78 </description>
79 <url>https://github.com/skyjake/lagrange/releases/tag/v1.5.0</url>
80 </release>
81 <release version="1.4.2" date="2021-05-22">
82 <description>
83 <p>Bug fixes:</p>
84 <ul>
85 <li>UI colors were all black on the first run.</li>
86 <li>Right mouse click on an inactive split does not have any
87 effect.</li>
88 <li>Action buttons showing under the Help link in an empty
89 Identities sidebar.</li>
90 <li>Potential crash at shutdown.</li>
91 <li>Minor UI layout issues.</li>
92 </ul>
93 </description>
94 <url>https://github.com/skyjake/lagrange/releases/tag/v1.4.2</url>
95 </release>
48 <release version="1.4.1" date="2021-05-13"> 96 <release version="1.4.1" date="2021-05-13">
49 <description> 97 <description>
50 <p>Bug fixes:</p> 98 <p>Bug fixes:</p>
diff --git a/res/fonts/LICENSE_SmolEmoji.txt b/res/fonts/LICENSE_SmolEmoji.txt
new file mode 100644
index 00000000..3513b65a
--- /dev/null
+++ b/res/fonts/LICENSE_SmolEmoji.txt
@@ -0,0 +1,94 @@
1Copyright 2021 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3This Font Software is licensed under the SIL Open Font License,
4Version 1.1.
5
6This license is copied below, and is also available with a FAQ at:
7http://scripts.sil.org/OFL
8
9-----------------------------------------------------------
10SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
11-----------------------------------------------------------
12
13PREAMBLE
14The goals of the Open Font License (OFL) are to stimulate worldwide
15development of collaborative font projects, to support the font
16creation efforts of academic and linguistic communities, and to
17provide a free and open framework in which fonts may be shared and
18improved in partnership with others.
19
20The OFL allows the licensed fonts to be used, studied, modified and
21redistributed freely as long as they are not sold by themselves. The
22fonts, including any derivative works, can be bundled, embedded,
23redistributed and/or sold with any software provided that any reserved
24names are not used by derivative works. The fonts and derivatives,
25however, cannot be released under any other type of license. The
26requirement for fonts to remain under this license does not apply to
27any document created using the fonts or their derivatives.
28
29DEFINITIONS
30"Font Software" refers to the set of files released by the Copyright
31Holder(s) under this license and clearly marked as such. This may
32include source files, build scripts and documentation.
33
34"Reserved Font Name" refers to any names specified as such after the
35copyright statement(s).
36
37"Original Version" refers to the collection of Font Software
38components as distributed by the Copyright Holder(s).
39
40"Modified Version" refers to any derivative made by adding to,
41deleting, or substituting -- in part or in whole -- any of the
42components of the Original Version, by changing formats or by porting
43the Font Software to a new environment.
44
45"Author" refers to any designer, engineer, programmer, technical
46writer or other person who contributed to the Font Software.
47
48PERMISSION & CONDITIONS
49Permission is hereby granted, free of charge, to any person obtaining
50a copy of the Font Software, to use, study, copy, merge, embed,
51modify, redistribute, and sell modified and unmodified copies of the
52Font Software, subject to the following conditions:
53
541) Neither the Font Software nor any of its individual components, in
55Original or Modified Versions, may be sold by itself.
56
572) Original or Modified Versions of the Font Software may be bundled,
58redistributed and/or sold with any software, provided that each copy
59contains the above copyright notice and this license. These can be
60included either as stand-alone text files, human-readable headers or
61in the appropriate machine-readable metadata fields within text or
62binary files as long as those fields can be easily viewed by the user.
63
643) No Modified Version of the Font Software may use the Reserved Font
65Name(s) unless explicit written permission is granted by the
66corresponding Copyright Holder. This restriction only applies to the
67primary font name as presented to the users.
68
694) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
70Software shall not be used to promote, endorse or advertise any
71Modified Version, except to acknowledge the contribution(s) of the
72Copyright Holder(s) and the Author(s) or with their explicit written
73permission.
74
755) The Font Software, modified or unmodified, in part or in whole,
76must be distributed entirely under this license, and must not be
77distributed under any other license. The requirement for fonts to
78remain under this license does not apply to any document created using
79the Font Software.
80
81TERMINATION
82This license becomes null and void if any of the above conditions are
83not met.
84
85DISCLAIMER
86THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
87EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
88MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
89OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
90COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
91INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
92DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
93FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
94OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/res/fonts/NotoSansSymbols-Regular.ttf b/res/fonts/NotoSansSymbols-Regular.ttf
new file mode 100644
index 00000000..68847551
--- /dev/null
+++ b/res/fonts/NotoSansSymbols-Regular.ttf
Binary files differ
diff --git a/res/fonts/NotoSansSymbols2-Regular.ttf b/res/fonts/NotoSansSymbols2-Regular.ttf
new file mode 100644
index 00000000..79706435
--- /dev/null
+++ b/res/fonts/NotoSansSymbols2-Regular.ttf
Binary files differ
diff --git a/res/fonts/SmolEmoji-Regular.ttf b/res/fonts/SmolEmoji-Regular.ttf
new file mode 100644
index 00000000..806766c2
--- /dev/null
+++ b/res/fonts/SmolEmoji-Regular.ttf
Binary files differ
diff --git a/res/fonts/Symbola.ttf b/res/fonts/Symbola.ttf
deleted file mode 100644
index f4bb1b7b..00000000
--- a/res/fonts/Symbola.ttf
+++ /dev/null
Binary files differ
diff --git a/res/lang/de.bin b/res/lang/de.bin
index 45c01abd..b89c8b0f 100644
--- a/res/lang/de.bin
+++ b/res/lang/de.bin
Binary files differ
diff --git a/res/lang/en.bin b/res/lang/en.bin
index 0a9a3c0f..0ac42d88 100644
--- a/res/lang/en.bin
+++ b/res/lang/en.bin
Binary files differ
diff --git a/res/lang/es.bin b/res/lang/es.bin
index 0d3e80c7..8f3940f1 100644
--- a/res/lang/es.bin
+++ b/res/lang/es.bin
Binary files differ
diff --git a/res/lang/fi.bin b/res/lang/fi.bin
index 00039ebd..d19dc0e8 100644
--- a/res/lang/fi.bin
+++ b/res/lang/fi.bin
Binary files differ
diff --git a/res/lang/fr.bin b/res/lang/fr.bin
index 139f8e51..0b78f3b7 100644
--- a/res/lang/fr.bin
+++ b/res/lang/fr.bin
Binary files differ
diff --git a/res/lang/ia.bin b/res/lang/ia.bin
index e7607899..e6590afc 100644
--- a/res/lang/ia.bin
+++ b/res/lang/ia.bin
Binary files differ
diff --git a/res/lang/ie.bin b/res/lang/ie.bin
index 79b517df..f852ba0e 100644
--- a/res/lang/ie.bin
+++ b/res/lang/ie.bin
Binary files differ
diff --git a/res/lang/pl.bin b/res/lang/pl.bin
index e208f3ba..9f42024c 100644
--- a/res/lang/pl.bin
+++ b/res/lang/pl.bin
Binary files differ
diff --git a/res/lang/ru.bin b/res/lang/ru.bin
index 4e775c94..5c9e3488 100644
--- a/res/lang/ru.bin
+++ b/res/lang/ru.bin
Binary files differ
diff --git a/res/lang/sr.bin b/res/lang/sr.bin
index 19408d1a..f4239f1f 100644
--- a/res/lang/sr.bin
+++ b/res/lang/sr.bin
Binary files differ
diff --git a/res/lang/tok.bin b/res/lang/tok.bin
index 2d8017cb..1a6b5baf 100644
--- a/res/lang/tok.bin
+++ b/res/lang/tok.bin
Binary files differ
diff --git a/res/lang/zh_Hans.bin b/res/lang/zh_Hans.bin
index 171bd2a8..b155036d 100644
--- a/res/lang/zh_Hans.bin
+++ b/res/lang/zh_Hans.bin
Binary files differ
diff --git a/res/lang/zh_Hant.bin b/res/lang/zh_Hant.bin
index 6518c7bf..57ce2d83 100644
--- a/res/lang/zh_Hant.bin
+++ b/res/lang/zh_Hant.bin
Binary files differ
diff --git a/src/app.c b/src/app.c
index 3bf9aec1..71cff954 100644
--- a/src/app.c
+++ b/src/app.c
@@ -204,6 +204,7 @@ static iString *serializePrefs_App_(const iApp *d) {
204 appendFormat_String(str, "uiscale arg:%f\n", uiScale_Window(d->window)); 204 appendFormat_String(str, "uiscale arg:%f\n", uiScale_Window(d->window));
205 appendFormat_String(str, "prefs.dialogtab arg:%d\n", d->prefs.dialogTab); 205 appendFormat_String(str, "prefs.dialogtab arg:%d\n", d->prefs.dialogTab);
206 appendFormat_String(str, "font.set arg:%d\n", d->prefs.font); 206 appendFormat_String(str, "font.set arg:%d\n", d->prefs.font);
207 appendFormat_String(str, "font.user path:%s\n", cstr_String(&d->prefs.symbolFontPath));
207 appendFormat_String(str, "headingfont.set arg:%d\n", d->prefs.headingFont); 208 appendFormat_String(str, "headingfont.set arg:%d\n", d->prefs.headingFont);
208 appendFormat_String(str, "zoom.set arg:%d\n", d->prefs.zoomPercent); 209 appendFormat_String(str, "zoom.set arg:%d\n", d->prefs.zoomPercent);
209 appendFormat_String(str, "smoothscroll arg:%d\n", d->prefs.smoothScrolling); 210 appendFormat_String(str, "smoothscroll arg:%d\n", d->prefs.smoothScrolling);
@@ -212,6 +213,7 @@ static iString *serializePrefs_App_(const iApp *d) {
212 appendFormat_String(str, "decodeurls arg:%d\n", d->prefs.decodeUserVisibleURLs); 213 appendFormat_String(str, "decodeurls arg:%d\n", d->prefs.decodeUserVisibleURLs);
213 appendFormat_String(str, "linewidth.set arg:%d\n", d->prefs.lineWidth); 214 appendFormat_String(str, "linewidth.set arg:%d\n", d->prefs.lineWidth);
214 /* TODO: Set up an array of booleans in Prefs and do these in a loop. */ 215 /* TODO: Set up an array of booleans in Prefs and do these in a loop. */
216 appendFormat_String(str, "prefs.animate.changed arg:%d\n", d->prefs.uiAnimations);
215 appendFormat_String(str, "prefs.mono.gemini.changed arg:%d\n", d->prefs.monospaceGemini); 217 appendFormat_String(str, "prefs.mono.gemini.changed arg:%d\n", d->prefs.monospaceGemini);
216 appendFormat_String(str, "prefs.mono.gopher.changed arg:%d\n", d->prefs.monospaceGopher); 218 appendFormat_String(str, "prefs.mono.gopher.changed arg:%d\n", d->prefs.monospaceGopher);
217 appendFormat_String(str, "prefs.boldlink.dark.changed arg:%d\n", d->prefs.boldLinkDark); 219 appendFormat_String(str, "prefs.boldlink.dark.changed arg:%d\n", d->prefs.boldLinkDark);
@@ -545,6 +547,7 @@ static void terminate_App_(int rc) {
545static void communicateWithRunningInstance_App_(iApp *d, iProcessId instance, 547static void communicateWithRunningInstance_App_(iApp *d, iProcessId instance,
546 const iStringList *openCmds) { 548 const iStringList *openCmds) {
547 iString *cmds = new_String(); 549 iString *cmds = new_String();
550 iBool requestRaise = iFalse;
548 const iProcessId pid = currentId_Process(); 551 const iProcessId pid = currentId_Process();
549 iConstForEach(CommandLine, i, &d->args) { 552 iConstForEach(CommandLine, i, &d->args) {
550 if (i.argType == value_CommandLineArgType) { 553 if (i.argType == value_CommandLineArgType) {
@@ -552,6 +555,7 @@ static void communicateWithRunningInstance_App_(iApp *d, iProcessId instance,
552 } 555 }
553 if (equal_CommandLineConstIterator(&i, "go-home")) { 556 if (equal_CommandLineConstIterator(&i, "go-home")) {
554 appendCStr_String(cmds, "navigate.home\n"); 557 appendCStr_String(cmds, "navigate.home\n");
558 requestRaise = iTrue;
555 } 559 }
556 else if (equal_CommandLineConstIterator(&i, "new-tab")) { 560 else if (equal_CommandLineConstIterator(&i, "new-tab")) {
557 iCommandLineArg *arg = argument_CommandLineConstIterator(&i); 561 iCommandLineArg *arg = argument_CommandLineConstIterator(&i);
@@ -563,6 +567,7 @@ static void communicateWithRunningInstance_App_(iApp *d, iProcessId instance,
563 appendCStr_String(cmds, "tabs.new\n"); 567 appendCStr_String(cmds, "tabs.new\n");
564 } 568 }
565 iRelease(arg); 569 iRelease(arg);
570 requestRaise = iTrue;
566 } 571 }
567 else if (equal_CommandLineConstIterator(&i, "close-tab")) { 572 else if (equal_CommandLineConstIterator(&i, "close-tab")) {
568 appendCStr_String(cmds, "tabs.close\n"); 573 appendCStr_String(cmds, "tabs.close\n");
@@ -573,13 +578,15 @@ static void communicateWithRunningInstance_App_(iApp *d, iProcessId instance,
573 } 578 }
574 if (!isEmpty_StringList(openCmds)) { 579 if (!isEmpty_StringList(openCmds)) {
575 append_String(cmds, collect_String(joinCStr_StringList(openCmds, "\n"))); 580 append_String(cmds, collect_String(joinCStr_StringList(openCmds, "\n")));
581 requestRaise = iTrue;
576 } 582 }
577 if (isEmpty_String(cmds)) { 583 if (isEmpty_String(cmds)) {
578 /* By default open a new tab. */ 584 /* By default open a new tab. */
579 appendCStr_String(cmds, "tabs.new\n"); 585 appendCStr_String(cmds, "tabs.new\n");
586 requestRaise = iTrue;
580 } 587 }
581 if (!isEmpty_String(cmds)) { 588 if (!isEmpty_String(cmds)) {
582 iString *result = communicate_Ipc(cmds); 589 iString *result = communicate_Ipc(cmds, requestRaise);
583 if (result) { 590 if (result) {
584 fwrite(cstr_String(result), 1, size_String(result), stdout); 591 fwrite(cstr_String(result), 1, size_String(result), stdout);
585 fflush(stdout); 592 fflush(stdout);
@@ -594,6 +601,18 @@ static void communicateWithRunningInstance_App_(iApp *d, iProcessId instance,
594} 601}
595#endif /* defined (LAGRANGE_ENABLE_IPC) */ 602#endif /* defined (LAGRANGE_ENABLE_IPC) */
596 603
604static iBool hasCommandLineOpenableScheme_(const iRangecc uri) {
605 static const char *schemes[] = {
606 "gemini:", "gopher:", "finger:", "file:", "data:", "about:"
607 };
608 iForIndices(i, schemes) {
609 if (startsWithCase_Rangecc(uri, schemes[i])) {
610 return iTrue;
611 }
612 }
613 return iFalse;
614}
615
597static void init_App_(iApp *d, int argc, char **argv) { 616static void init_App_(iApp *d, int argc, char **argv) {
598 init_CommandLine(&d->args, argc, argv); 617 init_CommandLine(&d->args, argc, argv);
599 /* Where was the app started from? We ask SDL first because the command line alone is 618 /* Where was the app started from? We ask SDL first because the command line alone is
@@ -628,6 +647,7 @@ static void init_App_(iApp *d, int argc, char **argv) {
628 defineValues_CommandLine(&d->args, "go-home", 0); 647 defineValues_CommandLine(&d->args, "go-home", 0);
629 defineValues_CommandLine(&d->args, "help", 0); 648 defineValues_CommandLine(&d->args, "help", 0);
630 defineValues_CommandLine(&d->args, listTabUrls_CommandLineOption, 0); 649 defineValues_CommandLine(&d->args, listTabUrls_CommandLineOption, 0);
650 defineValues_CommandLine(&d->args, openUrlOrSearch_CommandLineOption, 1);
631 defineValuesN_CommandLine(&d->args, "new-tab", 0, 1); 651 defineValuesN_CommandLine(&d->args, "new-tab", 0, 1);
632 defineValues_CommandLine(&d->args, "sw", 0); 652 defineValues_CommandLine(&d->args, "sw", 0);
633 defineValues_CommandLine(&d->args, "version;V", 0); 653 defineValues_CommandLine(&d->args, "version;V", 0);
@@ -643,30 +663,38 @@ static void init_App_(iApp *d, int argc, char **argv) {
643 terminate_App_(0); 663 terminate_App_(0);
644 } 664 }
645 /* Check for URLs. */ 665 /* Check for URLs. */
646 iBool newTab = iFalse;
647 iConstForEach(CommandLine, i, &d->args) { 666 iConstForEach(CommandLine, i, &d->args) {
648 const iRangecc arg = i.entry; 667 const iRangecc arg = i.entry;
649 if (i.argType == value_CommandLineArgType) { 668 if (i.argType == value_CommandLineArgType) {
650 /* URLs and file paths accepted. */ 669 /* URLs and file paths accepted. */
651 const iBool isKnownScheme = 670 const iBool isOpenable = hasCommandLineOpenableScheme_(arg);
652 startsWithCase_Rangecc(arg, "gemini:") || startsWithCase_Rangecc(arg, "gopher:") || 671 if (isOpenable || fileExistsCStr_FileInfo(cstr_Rangecc(arg))) {
653 startsWithCase_Rangecc(arg, "finger:") || startsWithCase_Rangecc(arg, "file:") ||
654 startsWithCase_Rangecc(arg, "data:") || startsWithCase_Rangecc(arg, "about:");
655 if (isKnownScheme || fileExistsCStr_FileInfo(cstr_Rangecc(arg))) {
656 iString *decUrl = 672 iString *decUrl =
657 isKnownScheme ? urlDecodeExclude_String(collectNewRange_String(arg), "/?#:") 673 isOpenable ? urlDecodeExclude_String(collectNewRange_String(arg), "/?#:")
658 : makeFileUrl_String(collectNewRange_String(arg)); 674 : makeFileUrl_String(collectNewRange_String(arg));
659 pushBack_StringList(openCmds, 675 pushBack_StringList(openCmds,
660 collectNewFormat_String( 676 collectNewFormat_String(
661 "open newtab:%d url:%s", newTab, cstr_String(decUrl))); 677 "open newtab:1 url:%s", cstr_String(decUrl)));
662 delete_String(decUrl); 678 delete_String(decUrl);
663 newTab = iTrue;
664 } 679 }
665 else { 680 else {
666 fprintf(stderr, "Invalid URL/file: %s\n", cstr_Rangecc(arg)); 681 fprintf(stderr, "Invalid URL/file: %s\n", cstr_Rangecc(arg));
667 terminate_App_(1); 682 terminate_App_(1);
668 } 683 }
669 } 684 }
685 else if (equal_CommandLineConstIterator(&i, openUrlOrSearch_CommandLineOption)) {
686 const iCommandLineArg *arg = iClob(argument_CommandLineConstIterator(&i));
687 const iString *input = value_CommandLineArg(arg, 0);
688 if (startsWith_String(input, "//")) {
689 input = collectNewFormat_String("gemini:%s", cstr_String(input));
690 }
691 if (hasCommandLineOpenableScheme_(range_String(input))) {
692 input = collect_String(urlDecodeExclude_String(input, "/?#:"));
693 }
694 pushBack_StringList(
695 openCmds,
696 collectNewFormat_String("search newtab:1 query:%s", cstr_String(input)));
697 }
670 else if (!isDefined_CommandLine(&d->args, collectNewRange_String(i.entry))) { 698 else if (!isDefined_CommandLine(&d->args, collectNewRange_String(i.entry))) {
671 fprintf(stderr, "Unknown option: %s\n", cstr_Rangecc(arg)); 699 fprintf(stderr, "Unknown option: %s\n", cstr_Rangecc(arg));
672 terminate_App_(1); 700 terminate_App_(1);
@@ -1541,24 +1569,25 @@ static iBool handlePrefsCommands_(iWidget *d, const char *cmd) {
1541 isSelected_Widget(findChild_Widget(d, "prefs.imageloadscroll"))); 1569 isSelected_Widget(findChild_Widget(d, "prefs.imageloadscroll")));
1542 postCommandf_App("hidetoolbarscroll arg:%d", 1570 postCommandf_App("hidetoolbarscroll arg:%d",
1543 isSelected_Widget(findChild_Widget(d, "prefs.hidetoolbarscroll"))); 1571 isSelected_Widget(findChild_Widget(d, "prefs.hidetoolbarscroll")));
1544 postCommandf_App("ostheme arg:%d", 1572 postCommandf_App("ostheme arg:%d", isSelected_Widget(findChild_Widget(d, "prefs.ostheme")));
1545 isSelected_Widget(findChild_Widget(d, "prefs.ostheme"))); 1573 postCommandf_App("font.user path:%s",
1574 cstrText_InputWidget(findChild_Widget(d, "prefs.userfont")));
1546 postCommandf_App("decodeurls arg:%d", 1575 postCommandf_App("decodeurls arg:%d",
1547 isSelected_Widget(findChild_Widget(d, "prefs.decodeurls"))); 1576 isSelected_Widget(findChild_Widget(d, "prefs.decodeurls")));
1548 postCommandf_App("searchurl address:%s", 1577 postCommandf_App("searchurl address:%s",
1549 cstr_String(text_InputWidget(findChild_Widget(d, "prefs.searchurl")))); 1578 cstrText_InputWidget(findChild_Widget(d, "prefs.searchurl")));
1550 postCommandf_App("cachesize.set arg:%d", 1579 postCommandf_App("cachesize.set arg:%d",
1551 toInt_String(text_InputWidget(findChild_Widget(d, "prefs.cachesize")))); 1580 toInt_String(text_InputWidget(findChild_Widget(d, "prefs.cachesize"))));
1552 postCommandf_App("ca.file path:%s", 1581 postCommandf_App("ca.file path:%s",
1553 cstr_String(text_InputWidget(findChild_Widget(d, "prefs.ca.file")))); 1582 cstrText_InputWidget(findChild_Widget(d, "prefs.ca.file")));
1554 postCommandf_App("ca.path path:%s", 1583 postCommandf_App("ca.path path:%s",
1555 cstr_String(text_InputWidget(findChild_Widget(d, "prefs.ca.path")))); 1584 cstrText_InputWidget(findChild_Widget(d, "prefs.ca.path")));
1556 postCommandf_App("proxy.gemini address:%s", 1585 postCommandf_App("proxy.gemini address:%s",
1557 cstr_String(text_InputWidget(findChild_Widget(d, "prefs.proxy.gemini")))); 1586 cstrText_InputWidget(findChild_Widget(d, "prefs.proxy.gemini")));
1558 postCommandf_App("proxy.gopher address:%s", 1587 postCommandf_App("proxy.gopher address:%s",
1559 cstr_String(text_InputWidget(findChild_Widget(d, "prefs.proxy.gopher")))); 1588 cstrText_InputWidget(findChild_Widget(d, "prefs.proxy.gopher")));
1560 postCommandf_App("proxy.http address:%s", 1589 postCommandf_App("proxy.http address:%s",
1561 cstr_String(text_InputWidget(findChild_Widget(d, "prefs.proxy.http")))); 1590 cstrText_InputWidget(findChild_Widget(d, "prefs.proxy.http")));
1562 const iWidget *tabs = findChild_Widget(d, "prefs.tabs"); 1591 const iWidget *tabs = findChild_Widget(d, "prefs.tabs");
1563 if (tabs) { 1592 if (tabs) {
1564 postCommandf_App("prefs.dialogtab arg:%u", 1593 postCommandf_App("prefs.dialogtab arg:%u",
@@ -1665,12 +1694,36 @@ iDocumentWidget *newTab_App(const iDocumentWidget *duplicateOf, iBool switchToNe
1665 1694
1666static iBool handleIdentityCreationCommands_(iWidget *dlg, const char *cmd) { 1695static iBool handleIdentityCreationCommands_(iWidget *dlg, const char *cmd) {
1667 iApp *d = &app_; 1696 iApp *d = &app_;
1697 if (equal_Command(cmd, "ident.showmore")) {
1698 iForEach(ObjectList, i, children_Widget(findChild_Widget(dlg, "headings"))) {
1699 if (flags_Widget(i.object) & collapse_WidgetFlag) {
1700 setFlags_Widget(i.object, hidden_WidgetFlag, iFalse);
1701 }
1702 }
1703 iForEach(ObjectList, j, children_Widget(findChild_Widget(dlg, "values"))) {
1704 if (flags_Widget(j.object) & collapse_WidgetFlag) {
1705 setFlags_Widget(j.object, hidden_WidgetFlag, iFalse);
1706 }
1707 }
1708 setFlags_Widget(child_Widget(findChild_Widget(dlg, "dialogbuttons"), 0), disabled_WidgetFlag,
1709 iTrue);
1710 arrange_Widget(dlg);
1711 refresh_Widget(dlg);
1712 return iTrue;
1713 }
1714 if (equal_Command(cmd, "ident.scope")) {
1715 iLabelWidget *scope = findChild_Widget(dlg, "ident.scope");
1716 setText_LabelWidget(scope,
1717 text_LabelWidget(child_Widget(
1718 findChild_Widget(as_Widget(scope), "menu"), arg_Command(cmd))));
1719 return iTrue;
1720 }
1668 if (equal_Command(cmd, "ident.temp.changed")) { 1721 if (equal_Command(cmd, "ident.temp.changed")) {
1669 setFlags_Widget( 1722 setFlags_Widget(
1670 findChild_Widget(dlg, "ident.temp.note"), hidden_WidgetFlag, !arg_Command(cmd)); 1723 findChild_Widget(dlg, "ident.temp.note"), hidden_WidgetFlag, !arg_Command(cmd));
1671 return iFalse; 1724 return iFalse;
1672 } 1725 }
1673 if (equal_Command(cmd, "ident.accept") || equal_Command(cmd, "cancel")) { 1726 if (equal_Command(cmd, "ident.accept") || equal_Command(cmd, "ident.cancel")) {
1674 if (equal_Command(cmd, "ident.accept")) { 1727 if (equal_Command(cmd, "ident.accept")) {
1675 const iString *commonName = text_InputWidget (findChild_Widget(dlg, "ident.common")); 1728 const iString *commonName = text_InputWidget (findChild_Widget(dlg, "ident.common"));
1676 const iString *email = text_InputWidget (findChild_Widget(dlg, "ident.email")); 1729 const iString *email = text_InputWidget (findChild_Widget(dlg, "ident.email"));
@@ -1718,11 +1771,52 @@ static iBool handleIdentityCreationCommands_(iWidget *dlg, const char *cmd) {
1718 } 1771 }
1719 } 1772 }
1720 /* The input seems fine. */ 1773 /* The input seems fine. */
1721 newIdentity_GmCerts(d->certs, isTemp ? temporary_GmIdentityFlag : 0, 1774 iGmIdentity *ident = newIdentity_GmCerts(d->certs,
1722 until, commonName, email, userId, domain, organization, country); 1775 isTemp ? temporary_GmIdentityFlag : 0,
1776 until,
1777 commonName,
1778 email,
1779 userId,
1780 domain,
1781 organization,
1782 country);
1783 /* Use in the chosen scope. */ {
1784 const iLabelWidget *scope = findChild_Widget(dlg, "ident.scope");
1785 const iString * selLabel = text_LabelWidget(scope);
1786 int selScope = 0;
1787 iConstForEach(ObjectList,
1788 i,
1789 children_Widget(findChild_Widget(constAs_Widget(scope), "menu"))) {
1790 if (isInstance_Object(i.object, &Class_LabelWidget)) {
1791 const iLabelWidget *item = i.object;
1792 if (equal_String(text_LabelWidget(item), selLabel)) {
1793 break;
1794 }
1795 selScope++;
1796 }
1797 }
1798 const iString *docUrl = url_DocumentWidget(document_Root(dlg->root));
1799 iString *useUrl = NULL;
1800 switch (selScope) {
1801 case 0: /* current domain */
1802 useUrl = collectNewFormat_String("gemini://%s",
1803 cstr_Rangecc(urlHost_String(docUrl)));
1804 break;
1805 case 1: /* current page */
1806 useUrl = collect_String(copy_String(docUrl));
1807 break;
1808 default: /* not used */
1809 break;
1810 }
1811 if (useUrl) {
1812 signIn_GmCerts(d->certs, ident, useUrl);
1813 postCommand_App("navigate.reload");
1814 }
1815 }
1723 postCommandf_App("sidebar.mode arg:%d show:1", identities_SidebarMode); 1816 postCommandf_App("sidebar.mode arg:%d show:1", identities_SidebarMode);
1724 postCommand_App("idents.changed"); 1817 postCommand_App("idents.changed");
1725 } 1818 }
1819 setupSheetTransition_Mobile(dlg, iFalse);
1726 destroy_Widget(dlg); 1820 destroy_Widget(dlg);
1727 return iTrue; 1821 return iTrue;
1728 } 1822 }
@@ -1813,6 +1907,22 @@ iBool handleCommand_App(const char *cmd) {
1813 resetFonts_Text(); 1907 resetFonts_Text();
1814 return iTrue; 1908 return iTrue;
1815 } 1909 }
1910 else if (equal_Command(cmd, "font.user")) {
1911 const char *path = suffixPtr_Command(cmd, "path");
1912 if (cmp_String(&d->prefs.symbolFontPath, path)) {
1913 if (!isFrozen) {
1914 setFreezeDraw_Window(get_Window(), iTrue);
1915 }
1916 setCStr_String(&d->prefs.symbolFontPath, path);
1917 loadUserFonts_Text();
1918 resetFonts_Text();
1919 if (!isFrozen) {
1920 postCommand_App("font.changed");
1921 postCommand_App("window.unfreeze");
1922 }
1923 }
1924 return iTrue;
1925 }
1816 else if (equal_Command(cmd, "font.set")) { 1926 else if (equal_Command(cmd, "font.set")) {
1817 if (!isFrozen) { 1927 if (!isFrozen) {
1818 setFreezeDraw_Window(get_Window(), iTrue); 1928 setFreezeDraw_Window(get_Window(), iTrue);
@@ -2011,6 +2121,10 @@ iBool handleCommand_App(const char *cmd) {
2011 d->prefs.openArchiveIndexPages = arg_Command(cmd) != 0; 2121 d->prefs.openArchiveIndexPages = arg_Command(cmd) != 0;
2012 return iTrue; 2122 return iTrue;
2013 } 2123 }
2124 else if (equal_Command(cmd, "prefs.animate.changed")) {
2125 d->prefs.uiAnimations = arg_Command(cmd) != 0;
2126 return iTrue;
2127 }
2014 else if (equal_Command(cmd, "saturation.set")) { 2128 else if (equal_Command(cmd, "saturation.set")) {
2015 d->prefs.saturation = (float) arg_Command(cmd) / 100.0f; 2129 d->prefs.saturation = (float) arg_Command(cmd) / 100.0f;
2016 if (!isFrozen) { 2130 if (!isFrozen) {
@@ -2070,6 +2184,20 @@ iBool handleCommand_App(const char *cmd) {
2070 } 2184 }
2071 return iTrue; 2185 return iTrue;
2072 } 2186 }
2187 else if (equal_Command(cmd, "search")) {
2188 const int newTab = argLabel_Command(cmd, "newtab");
2189 const iString *query = collect_String(suffix_Command(cmd, "query"));
2190 if (!isLikelyUrl_String(query)) {
2191 const iString *url = searchQueryUrl_App(query);
2192 if (!isEmpty_String(url)) {
2193 postCommandf_App("open newtab:%d url:%s", newTab, cstr_String(url));
2194 }
2195 }
2196 else {
2197 postCommandf_App("open newtab:%d url:%s", newTab, cstr_String(query));
2198 }
2199 return iTrue;
2200 }
2073 else if (equal_Command(cmd, "open")) { 2201 else if (equal_Command(cmd, "open")) {
2074 iString *url = collectNewCStr_String(suffixPtr_Command(cmd, "url")); 2202 iString *url = collectNewCStr_String(suffixPtr_Command(cmd, "url"));
2075 const iBool noProxy = argLabel_Command(cmd, "noproxy"); 2203 const iBool noProxy = argLabel_Command(cmd, "noproxy");
@@ -2239,6 +2367,8 @@ iBool handleCommand_App(const char *cmd) {
2239 setToggle_Widget(findChild_Widget(dlg, "prefs.archive.openindex"), d->prefs.openArchiveIndexPages); 2367 setToggle_Widget(findChild_Widget(dlg, "prefs.archive.openindex"), d->prefs.openArchiveIndexPages);
2240 setToggle_Widget(findChild_Widget(dlg, "prefs.ostheme"), d->prefs.useSystemTheme); 2368 setToggle_Widget(findChild_Widget(dlg, "prefs.ostheme"), d->prefs.useSystemTheme);
2241 setToggle_Widget(findChild_Widget(dlg, "prefs.customframe"), d->prefs.customFrame); 2369 setToggle_Widget(findChild_Widget(dlg, "prefs.customframe"), d->prefs.customFrame);
2370 setToggle_Widget(findChild_Widget(dlg, "prefs.animate"), d->prefs.uiAnimations);
2371 setText_InputWidget(findChild_Widget(dlg, "prefs.userfont"), &d->prefs.symbolFontPath);
2242 updatePrefsPinSplitButtons_(dlg, d->prefs.pinSplit); 2372 updatePrefsPinSplitButtons_(dlg, d->prefs.pinSplit);
2243 updateDropdownSelection_(findChild_Widget(dlg, "prefs.uilang"), cstr_String(&d->prefs.uiLanguage)); 2373 updateDropdownSelection_(findChild_Widget(dlg, "prefs.uilang"), cstr_String(&d->prefs.uiLanguage));
2244 setToggle_Widget(findChild_Widget(dlg, "prefs.retainwindow"), d->prefs.retainWindowSize); 2374 setToggle_Widget(findChild_Widget(dlg, "prefs.retainwindow"), d->prefs.retainWindowSize);
@@ -2454,6 +2584,11 @@ iBool handleCommand_App(const char *cmd) {
2454 return iTrue; 2584 return iTrue;
2455 } 2585 }
2456 else if (equal_Command(cmd, "ipc.signal")) { 2586 else if (equal_Command(cmd, "ipc.signal")) {
2587 if (argLabel_Command(cmd, "raise")) {
2588 if (d->window && d->window->win) {
2589 SDL_RaiseWindow(d->window->win);
2590 }
2591 }
2457 signal_Ipc(arg_Command(cmd)); 2592 signal_Ipc(arg_Command(cmd));
2458 return iTrue; 2593 return iTrue;
2459 } 2594 }
diff --git a/src/app.h b/src/app.h
index 0fb6be29..918cd396 100644
--- a/src/app.h
+++ b/src/app.h
@@ -42,7 +42,8 @@ iDeclareType(Visited)
42iDeclareType(Window) 42iDeclareType(Window)
43 43
44/* Command line options strings. */ 44/* Command line options strings. */
45#define listTabUrls_CommandLineOption "list-tab-urls;L" 45#define listTabUrls_CommandLineOption "list-tab-urls;L"
46#define openUrlOrSearch_CommandLineOption "url-or-search;u"
46 47
47enum iAppDeviceType { 48enum iAppDeviceType {
48 desktop_AppDeviceType, 49 desktop_AppDeviceType,
diff --git a/src/defs.h b/src/defs.h
index 0da404bf..ff68e7e5 100644
--- a/src/defs.h
+++ b/src/defs.h
@@ -42,6 +42,8 @@ enum iFileVersion {
42 42
43/* Icons */ 43/* Icons */
44 44
45#define rightArrowhead_Icon "\u27a4"
46#define leftArrowhead_Icon "\u27a4"
45#define warning_Icon "\u26a0" 47#define warning_Icon "\u26a0"
46#define openLock_Icon "\U0001f513" 48#define openLock_Icon "\U0001f513"
47#define closedLock_Icon "\U0001f512" 49#define closedLock_Icon "\U0001f512"
@@ -62,12 +64,13 @@ enum iFileVersion {
62#define whiteStar_Icon "\u2606" 64#define whiteStar_Icon "\u2606"
63#define person_Icon "\U0001f464" 65#define person_Icon "\U0001f464"
64#define download_Icon "\u2ba7" 66#define download_Icon "\u2ba7"
67#define export_Icon "\U0001f4e4"
65#define hourglass_Icon "\u231b" 68#define hourglass_Icon "\u231b"
66#define timer_Icon "\u23f2" 69#define timer_Icon "\u23f2"
67#define home_Icon "\U0001f3e0" 70#define home_Icon "\U0001f3e0"
68#define edit_Icon "\u270e" 71#define edit_Icon "\u270e"
69#define delete_Icon "\u232b" 72#define delete_Icon "\u232b"
70#define copy_Icon "\u2bba" 73#define copy_Icon "\u2398" //"\u2bba"
71#define check_Icon "\u2714" 74#define check_Icon "\u2714"
72#define ballotCheck_Icon "\U0001f5f9" 75#define ballotCheck_Icon "\U0001f5f9"
73#define inbox_Icon "\U0001f4e5" 76#define inbox_Icon "\U0001f4e5"
@@ -97,7 +100,7 @@ enum iFileVersion {
97#define globe_Icon "\U0001f310" 100#define globe_Icon "\U0001f310"
98#define magnifyingGlass_Icon "\U0001f50d" 101#define magnifyingGlass_Icon "\U0001f50d"
99#define midEllipsis_Icon "\u00b7\u00b7\u00b7" 102#define midEllipsis_Icon "\u00b7\u00b7\u00b7"
100#define return_Icon "\u21a9" 103#define return_Icon "\u23ce"
101 104
102#if defined (iPlatformApple) 105#if defined (iPlatformApple)
103# define shift_Icon "\u21e7" 106# define shift_Icon "\u21e7"
diff --git a/src/feeds.c b/src/feeds.c
index dcd97b43..6b102fef 100644
--- a/src/feeds.c
+++ b/src/feeds.c
@@ -300,8 +300,16 @@ static iBool isHeadingEntry_FeedEntry_(const iFeedEntry *d) {
300static iBool updateEntries_Feeds_(iFeeds *d, iPtrArray *incoming) { 300static iBool updateEntries_Feeds_(iFeeds *d, iPtrArray *incoming) {
301 iBool gotNew = iFalse; 301 iBool gotNew = iFalse;
302 lock_Mutex(d->mtx); 302 lock_Mutex(d->mtx);
303 iTime now;
304 initCurrent_Time(&now);
303 iForEach(PtrArray, i, incoming) { 305 iForEach(PtrArray, i, incoming) {
304 iFeedEntry *entry = i.ptr; 306 iFeedEntry *entry = i.ptr;
307 /* Disregard old entries. */
308 if (secondsSince_Time(&now, &entry->posted) >= maxAge_Visited) {
309 /* We don't remember this far back, so the unread status of the entry would
310 be incorrect. */
311 continue;
312 }
305 size_t pos; 313 size_t pos;
306 if (locate_SortedArray(&d->entries, &entry, &pos)) { 314 if (locate_SortedArray(&d->entries, &entry, &pos)) {
307 iFeedEntry *existing = *(iFeedEntry **) at_SortedArray(&d->entries, pos); 315 iFeedEntry *existing = *(iFeedEntry **) at_SortedArray(&d->entries, pos);
diff --git a/src/gempub.c b/src/gempub.c
index aa03d835..1f5d58ce 100644
--- a/src/gempub.c
+++ b/src/gempub.c
@@ -94,6 +94,9 @@ static void parseNavigationLinks_Gempub_(const iGempub *d) {
94 set_String(&link.url, absoluteUrl_String(url_GmRequest(index), collectNewRange_String(url))); 94 set_String(&link.url, absoluteUrl_String(url_GmRequest(index), collectNewRange_String(url)));
95 setRange_String(&link.label, capturedRange_RegExpMatch(&m, 2)); 95 setRange_String(&link.label, capturedRange_RegExpMatch(&m, 2));
96 trim_String(&link.label); 96 trim_String(&link.label);
97 if (isEmpty_String(&link.label)) {
98 setRange_String(&link.label, url);
99 }
97 pushBack_Array(d->navLinks, &link); 100 pushBack_Array(d->navLinks, &link);
98 } 101 }
99 iEndCollect(); 102 iEndCollect();
@@ -213,6 +216,10 @@ iBool isOpen_Gempub(const iGempub *d) {
213 return d->arch != NULL; 216 return d->arch != NULL;
214} 217}
215 218
219const iString *property_Gempub(const iGempub *d, enum iGempubProperty prop) {
220 return &d->props[prop];
221}
222
216const iString *coverPageUrl_Gempub(const iGempub *d) { 223const iString *coverPageUrl_Gempub(const iGempub *d) {
217 return &d->baseUrl; 224 return &d->baseUrl;
218} 225}
@@ -232,6 +239,39 @@ const iString *navStartLinkUrl_Gempub(const iGempub *d) {
232 return &((const iGempubNavLink *) constFront_Array(d->navLinks))->url; 239 return &((const iGempubNavLink *) constFront_Array(d->navLinks))->url;
233} 240}
234 241
242size_t navSize_Gempub(const iGempub *d) {
243 parseNavigationLinks_Gempub_(d);
244 return size_Array(d->navLinks);
245}
246
247size_t navIndex_Gempub(const iGempub *d, const iString *url) {
248 parseNavigationLinks_Gempub_(d);
249 const iString *normUrl = withSpacesEncoded_String(url);
250 iConstForEach(Array, i, d->navLinks) {
251 const iGempubNavLink *nav = i.value;
252 if (equalCase_String(&nav->url, normUrl)) {
253 return index_ArrayConstIterator(&i);
254 }
255 }
256 return iInvalidPos;
257}
258
259const iString *navLinkUrl_Gempub(const iGempub *d, size_t index) {
260 parseNavigationLinks_Gempub_(d);
261 if (index < size_Array(d->navLinks)) {
262 return &constValue_Array(d->navLinks, index, iGempubNavLink).url;
263 }
264 return NULL;
265}
266
267const iString *navLinkLabel_Gempub(const iGempub *d, size_t index) {
268 parseNavigationLinks_Gempub_(d);
269 if (index < size_Array(d->navLinks)) {
270 return &constValue_Array(d->navLinks, index, iGempubNavLink).label;
271 }
272 return NULL;
273}
274
235static iBool hasProperty_Gempub_(const iGempub *d, enum iGempubProperty prop) { 275static iBool hasProperty_Gempub_(const iGempub *d, enum iGempubProperty prop) {
236 return !isEmpty_String(&d->props[prop]); 276 return !isEmpty_String(&d->props[prop]);
237} 277}
@@ -243,7 +283,7 @@ static void appendProperty_Gempub_(const iGempub *d, const char *label,
243 } 283 }
244} 284}
245 285
246static iBool isRemote_Gempub_(const iGempub *d) { 286iBool isRemote_Gempub(const iGempub *d) {
247 return !equalCase_Rangecc(urlScheme_String(&d->baseUrl), "file"); 287 return !equalCase_Rangecc(urlScheme_String(&d->baseUrl), "file");
248} 288}
249 289
@@ -258,7 +298,7 @@ iString *coverPageSource_Gempub(const iGempub *d) {
258 } 298 }
259 appendCStr_String(out, "\n"); 299 appendCStr_String(out, "\n");
260 appendProperty_Gempub_(d, "${gempub.meta.author}:", author_GempubProperty, out); 300 appendProperty_Gempub_(d, "${gempub.meta.author}:", author_GempubProperty, out);
261 if (!isRemote_Gempub_(d)) { 301 if (!isRemote_Gempub(d)) {
262 appendFormat_String(out, "\n=> %s " book_Icon " ${gempub.cover.view}\n", 302 appendFormat_String(out, "\n=> %s " book_Icon " ${gempub.cover.view}\n",
263 cstr_String(indexPageUrl_Gempub(d))); 303 cstr_String(indexPageUrl_Gempub(d)));
264 if (hasProperty_Gempub_(d, cover_GempubProperty)) { 304 if (hasProperty_Gempub_(d, cover_GempubProperty)) {
@@ -269,12 +309,12 @@ iString *coverPageSource_Gempub(const iGempub *d) {
269 else { 309 else {
270 iString *key = collectNew_String(); /* TODO: add a helper for this */ 310 iString *key = collectNew_String(); /* TODO: add a helper for this */
271 toString_Sym(SDLK_s, KMOD_PRIMARY, key); 311 toString_Sym(SDLK_s, KMOD_PRIMARY, key);
272 appendCStr_String(out, "\n${gempub.cover.viewlocal} "); 312 appendCStr_String(out, "\n${gempub.cover.viewlocal}\n");
273 appendFormat_String(out, 313// appendFormat_String(out,
274 cstr_Lang("error.unsupported.suggestsave"), 314// cstr_Lang("error.unsupported.suggestsave"),
275 cstr_String(key), 315// cstr_String(key),
276 saveToDownloads_Label); 316// saveToDownloads_Label);
277 appendCStr_String(out, "\n"); 317// appendCStr_String(out, "\n");
278 } 318 }
279 appendCStr_String(out, "\n## ${gempub.cover.aboutbook}\n"); 319 appendCStr_String(out, "\n## ${gempub.cover.aboutbook}\n");
280 appendProperty_Gempub_(d, "${gempub.meta.version}:", version_GempubProperty, out); 320 appendProperty_Gempub_(d, "${gempub.meta.version}:", version_GempubProperty, out);
diff --git a/src/gempub.h b/src/gempub.h
index 6912bcc9..e5f1b8eb 100644
--- a/src/gempub.h
+++ b/src/gempub.h
@@ -53,6 +53,7 @@ void close_Gempub (iGempub *);
53void setBaseUrl_Gempub (iGempub *, const iString *baseUrl); 53void setBaseUrl_Gempub (iGempub *, const iString *baseUrl);
54 54
55iBool isOpen_Gempub (const iGempub *); 55iBool isOpen_Gempub (const iGempub *);
56iBool isRemote_Gempub (const iGempub *);
56iString * coverPageSource_Gempub (const iGempub *); 57iString * coverPageSource_Gempub (const iGempub *);
57iBool preloadCoverImage_Gempub(const iGempub *, iGmDocument *doc); 58iBool preloadCoverImage_Gempub(const iGempub *, iGmDocument *doc);
58 59
@@ -60,5 +61,9 @@ const iString * property_Gempub (const iGempub *, enum iGempubProperty);
60const iString * coverPageUrl_Gempub (const iGempub *); 61const iString * coverPageUrl_Gempub (const iGempub *);
61const iString * indexPageUrl_Gempub (const iGempub *); 62const iString * indexPageUrl_Gempub (const iGempub *);
62const iString * navStartLinkUrl_Gempub (const iGempub *); /* for convenience */ 63const iString * navStartLinkUrl_Gempub (const iGempub *); /* for convenience */
64size_t navSize_Gempub (const iGempub *);
65size_t navIndex_Gempub (const iGempub *, const iString *url);
66const iString * navLinkUrl_Gempub (const iGempub *, size_t index);
67const iString * navLinkLabel_Gempub (const iGempub *, size_t index);
63 68
64extern const char *mimeType_Gempub; 69extern const char *mimeType_Gempub;
diff --git a/src/gmcerts.c b/src/gmcerts.c
index 35d784f6..8577cf2b 100644
--- a/src/gmcerts.c
+++ b/src/gmcerts.c
@@ -105,7 +105,7 @@ void deserialize_GmIdentity(iGmIdentity *d, iStream *ins) {
105 iString url; 105 iString url;
106 init_String(&url); 106 init_String(&url);
107 deserialize_String(&url, ins); 107 deserialize_String(&url, ins);
108 insert_StringSet(d->useUrls, &url); 108 setUse_GmIdentity(d, &url, iTrue);
109 deinit_String(&url); 109 deinit_String(&url);
110 } 110 }
111} 111}
@@ -161,11 +161,30 @@ iBool isUsedOn_GmIdentity(const iGmIdentity *d, const iString *url) {
161 return iFalse; 161 return iFalse;
162} 162}
163 163
164iBool isUsedOnDomain_GmIdentity(const iGmIdentity *d, const iRangecc domain) {
165 iConstForEach(StringSet, i, d->useUrls) {
166 const iRangecc host = urlHost_String(i.value);
167 if (equalRangeCase_Rangecc(host, domain)) {
168 return iTrue;
169 }
170 }
171 return iFalse;
172}
173
164void setUse_GmIdentity(iGmIdentity *d, const iString *url, iBool use) { 174void setUse_GmIdentity(iGmIdentity *d, const iString *url, iBool use) {
165 if (use && isUsedOn_GmIdentity(d, url)) { 175 if (use && isUsedOn_GmIdentity(d, url)) {
166 return; /* Redudant. */ 176 return; /* Redudant. */
167 } 177 }
168 if (use) { 178 if (use) {
179 /* Remove all use-URLs that become redundant by this newly added URL. */
180 /* TODO: StringSet could have a non-const iterator. */
181 iForEach(Array, i, &d->useUrls->strings.values) {
182 iString *used = i.value;
183 if (startsWithCase_String(used, cstr_String(url))) {
184 deinit_String(used);
185 remove_ArrayIterator(&i);
186 }
187 }
169#if !defined (NDEBUG) 188#if !defined (NDEBUG)
170 const iBool wasInserted = 189 const iBool wasInserted =
171#endif 190#endif
@@ -182,7 +201,11 @@ void clearUse_GmIdentity(iGmIdentity *d) {
182} 201}
183 202
184const iString *name_GmIdentity(const iGmIdentity *d) { 203const iString *name_GmIdentity(const iGmIdentity *d) {
185 return collect_String(subject_TlsCertificate(d->cert)); 204 iString *name = collect_String(subject_TlsCertificate(d->cert));
205 if (startsWith_String(name, "CN = ")) {
206 remove_Block(&name->chars, 0, 5);
207 }
208 return name;
186} 209}
187 210
188iDefineTypeConstruction(GmIdentity) 211iDefineTypeConstruction(GmIdentity)
diff --git a/src/gmcerts.h b/src/gmcerts.h
index a9859845..a962d8b6 100644
--- a/src/gmcerts.h
+++ b/src/gmcerts.h
@@ -44,8 +44,9 @@ struct Impl_GmIdentity {
44 int flags; 44 int flags;
45}; 45};
46 46
47iBool isUsed_GmIdentity (const iGmIdentity *); 47iBool isUsed_GmIdentity (const iGmIdentity *);
48iBool isUsedOn_GmIdentity (const iGmIdentity *, const iString *url); 48iBool isUsedOn_GmIdentity (const iGmIdentity *, const iString *url);
49iBool isUsedOnDomain_GmIdentity (const iGmIdentity *, const iRangecc domain);
49 50
50void setUse_GmIdentity (iGmIdentity *, const iString *url, iBool use); 51void setUse_GmIdentity (iGmIdentity *, const iString *url, iBool use);
51void clearUse_GmIdentity (iGmIdentity *); 52void clearUse_GmIdentity (iGmIdentity *);
diff --git a/src/gmdocument.c b/src/gmdocument.c
index f8d41172..56953255 100644
--- a/src/gmdocument.c
+++ b/src/gmdocument.c
@@ -31,6 +31,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
31#include "visited.h" 31#include "visited.h"
32#include "bookmarks.h" 32#include "bookmarks.h"
33#include "app.h" 33#include "app.h"
34#include "defs.h"
34 35
35#include <the_Foundation/ptrarray.h> 36#include <the_Foundation/ptrarray.h>
36#include <the_Foundation/regexp.h> 37#include <the_Foundation/regexp.h>
@@ -329,14 +330,15 @@ static void alignDecoration_GmRun_(iGmRun *run, iBool isCentered) {
329 int xAdjust = 0; 330 int xAdjust = 0;
330 if (!isCentered) { 331 if (!isCentered) {
331 /* Keep the icon aligned to the left edge. */ 332 /* Keep the icon aligned to the left edge. */
333 const int alignWidth = width_Rect(run->visBounds) * 3 / 4;
332 xAdjust -= left_Rect(visBounds); 334 xAdjust -= left_Rect(visBounds);
333 if (visWidth > width_Rect(run->visBounds)) { 335 if (visWidth > alignWidth) {
334 /* ...unless it's a wide icon, in which case move it to the left. */ 336 /* ...unless it's a wide icon, in which case move it to the left. */
335 xAdjust -= visWidth - width_Rect(run->visBounds); 337 xAdjust -= visWidth - alignWidth;
336 } 338 }
337 else if (visWidth < width_Rect(run->visBounds) * 3 / 4) { 339 else if (visWidth < alignWidth) {
338 /* ...or a narrow icon, which needs to be centered but leave a gap. */ 340 /* ...or a narrow icon, which needs to be centered but leave a gap. */
339 xAdjust += (width_Rect(run->visBounds) * 3 / 4 - visWidth) / 2; 341 xAdjust += (alignWidth - visWidth) / 2;
340 } 342 }
341 } 343 }
342 else { 344 else {
@@ -401,7 +403,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
401 static const float bottomMargin[max_GmLineType] = { 403 static const float bottomMargin[max_GmLineType] = {
402 0.0f, 0.333f, 1.0f, 0.5f, 0.5f, 0.5f, 0.5f, 0.25f 404 0.0f, 0.333f, 1.0f, 0.5f, 0.5f, 0.5f, 0.5f, 0.25f
403 }; 405 };
404 static const char *arrow = "\u27a4"; 406 static const char *arrow = rightArrowhead_Icon;
405 static const char *envelope = "\U0001f4e7"; 407 static const char *envelope = "\U0001f4e7";
406 static const char *bullet = "\u2022"; 408 static const char *bullet = "\u2022";
407 static const char *folder = "\U0001f4c1"; 409 static const char *folder = "\U0001f4c1";
@@ -618,7 +620,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
618 iGmRun bulRun = run; 620 iGmRun bulRun = run;
619 if (prefs->font == literata_TextFont) { 621 if (prefs->font == literata_TextFont) {
620 /* Something wrong this the glyph in Literata, looks cropped. */ 622 /* Something wrong this the glyph in Literata, looks cropped. */
621 bulRun.font = defaultContentSized_FontId; 623 bulRun.font = defaultContentRegular_FontId;
622 } 624 }
623 bulRun.color = tmQuote_ColorId; 625 bulRun.color = tmQuote_ColorId;
624 bulRun.visBounds.pos = addX_I2(pos, (indents[text_GmLineType] - 0.55f) * gap_Text); 626 bulRun.visBounds.pos = addX_I2(pos, (indents[text_GmLineType] - 0.55f) * gap_Text);
@@ -724,7 +726,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
724 lineHeightReduction = 0.10f; 726 lineHeightReduction = 0.10f;
725 } 727 }
726 else if (type == heading2_GmLineType) { 728 else if (type == heading2_GmLineType) {
727 lineHeightReduction = 0.05f; 729 lineHeightReduction = 0.06f;
728 } 730 }
729 /* Visited links are never bold. */ 731 /* Visited links are never bold. */
730 if (run.linkId && linkFlags_GmDocument(d, run.linkId) & visited_GmLinkFlag) { 732 if (run.linkId && linkFlags_GmDocument(d, run.linkId) & visited_GmLinkFlag) {
@@ -1517,6 +1519,7 @@ static void normalize_GmDocument(iGmDocument *d) {
1517 printf("wasNormalized: %d\n", wasNormalized); 1519 printf("wasNormalized: %d\n", wasNormalized);
1518 fflush(stdout); 1520 fflush(stdout);
1519 set_String(&d->source, collect_String(normalized)); 1521 set_String(&d->source, collect_String(normalized));
1522 normalize_String(&d->source); /* NFC */
1520 printf("orig:%zu norm:%zu\n", size_String(&d->unormSource), size_String(&d->source)); 1523 printf("orig:%zu norm:%zu\n", size_String(&d->unormSource), size_String(&d->source));
1521 /* normalized source has an extra newline at the end */ 1524 /* normalized source has an extra newline at the end */
1522// iAssert(wasNormalized || equal_String(&d->unormSource, &d->source)); 1525// iAssert(wasNormalized || equal_String(&d->unormSource, &d->source));
diff --git a/src/gmdocument.h b/src/gmdocument.h
index 5137bb28..943a408c 100644
--- a/src/gmdocument.h
+++ b/src/gmdocument.h
@@ -65,7 +65,7 @@ iBool isDark_GmDocumentTheme(enum iGmDocumentTheme);
65 65
66typedef uint16_t iGmLinkId; 66typedef uint16_t iGmLinkId;
67 67
68enum iGmLinkFlags { 68enum iGmLinkFlag {
69 gemini_GmLinkFlag = iBit(1), 69 gemini_GmLinkFlag = iBit(1),
70 gopher_GmLinkFlag = iBit(2), 70 gopher_GmLinkFlag = iBit(2),
71 finger_GmLinkFlag = iBit(3), 71 finger_GmLinkFlag = iBit(3),
diff --git a/src/gmrequest.c b/src/gmrequest.c
index 3dd92eec..1325c025 100644
--- a/src/gmrequest.c
+++ b/src/gmrequest.c
@@ -346,6 +346,7 @@ static const iBlock *replaceVariables_(const iBlock *block) {
346 if (equal_Rangecc(name, "APP_VERSION")) { 346 if (equal_Rangecc(name, "APP_VERSION")) {
347 repl = range_CStr(LAGRANGE_APP_VERSION); 347 repl = range_CStr(LAGRANGE_APP_VERSION);
348 } 348 }
349#if 0
349 else if (startsWith_Rangecc(name, "BT:")) { /* block text */ 350 else if (startsWith_Rangecc(name, "BT:")) { /* block text */
350 repl = range_String(collect_String(renderBlockChars_Text( 351 repl = range_String(collect_String(renderBlockChars_Text(
351 &fontFiraSansRegular_Embedded, 352 &fontFiraSansRegular_Embedded,
@@ -356,12 +357,13 @@ static const iBlock *replaceVariables_(const iBlock *block) {
356 } 357 }
357 else if (startsWith_Rangecc(name, "ST:")) { /* shaded text */ 358 else if (startsWith_Rangecc(name, "ST:")) { /* shaded text */
358 repl = range_String(collect_String(renderBlockChars_Text( 359 repl = range_String(collect_String(renderBlockChars_Text(
359 &fontSymbola_Embedded, 360 &fontSmolEmojiRegular_Embedded,
360 20, 361 20,
361 shading_TextBlockMode, 362 shading_TextBlockMode,
362 &(iString){ iBlockLiteral( 363 &(iString){ iBlockLiteral(
363 name.start + 3, size_Range(&name) - 3, size_Range(&name) - 3) }))); 364 name.start + 3, size_Range(&name) - 3, size_Range(&name) - 3) })));
364 } 365 }
366#endif
365 else if (equal_Rangecc(name, "ALT")) { 367 else if (equal_Rangecc(name, "ALT")) {
366#if defined (iPlatformApple) 368#if defined (iPlatformApple)
367 repl = range_CStr("\u2325"); 369 repl = range_CStr("\u2325");
diff --git a/src/gmrequest.h b/src/gmrequest.h
index 2cf9e4ff..0cba4ea3 100644
--- a/src/gmrequest.h
+++ b/src/gmrequest.h
@@ -30,7 +30,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
30iDeclareType(GmCerts) 30iDeclareType(GmCerts)
31iDeclareType(GmResponse) 31iDeclareType(GmResponse)
32 32
33enum iGmCertFlags { 33enum iGmCertFlag {
34 available_GmCertFlag = iBit(1), /* certificate provided by server */ 34 available_GmCertFlag = iBit(1), /* certificate provided by server */
35 trusted_GmCertFlag = iBit(2), /* TOFU status */ 35 trusted_GmCertFlag = iBit(2), /* TOFU status */
36 timeVerified_GmCertFlag = iBit(3), /* has not expired */ 36 timeVerified_GmCertFlag = iBit(3), /* has not expired */
diff --git a/src/gmutil.c b/src/gmutil.c
index 718a0a9a..3ca93901 100644
--- a/src/gmutil.c
+++ b/src/gmutil.c
@@ -262,7 +262,20 @@ void urlEncodePath_String(iString *d) {
262 delete_String(encoded); 262 delete_String(encoded);
263} 263}
264 264
265static iBool isSupportedUrlScheme_Rangecc_(iRangecc scheme) { 265iBool isKnownScheme_Rangecc(iRangecc scheme) {
266 if (isKnownUrlScheme_Rangecc(scheme)) {
267 return iTrue;
268 }
269 static const char *uriSchemes[] = { "about", "data" };
270 iForIndices(i, uriSchemes) {
271 if (equalCase_Rangecc(scheme, uriSchemes[i])) {
272 return iTrue;
273 }
274 }
275 return iFalse;
276}
277
278iBool isKnownUrlScheme_Rangecc(iRangecc scheme) {
266 static const char *schemes[] = { "gemini", "gopher", "finger", "http", "https", "file" }; 279 static const char *schemes[] = { "gemini", "gopher", "finger", "http", "https", "file" };
267 iForIndices(i, schemes) { 280 iForIndices(i, schemes) {
268 if (equalCase_Rangecc(scheme, schemes[i])) { 281 if (equalCase_Rangecc(scheme, schemes[i])) {
@@ -277,7 +290,7 @@ const iString *absoluteUrl_String(const iString *d, const iString *urlMaybeRelat
277 iUrl rel; 290 iUrl rel;
278 init_Url(&orig, d); 291 init_Url(&orig, d);
279 init_Url(&rel, urlMaybeRelative); 292 init_Url(&rel, urlMaybeRelative);
280 if (!isEmpty_Range(&rel.scheme) && !isSupportedUrlScheme_Rangecc_(rel.scheme) && 293 if (!isEmpty_Range(&rel.scheme) && !isKnownUrlScheme_Rangecc(rel.scheme) &&
281 isEmpty_Range(&rel.host)) { 294 isEmpty_Range(&rel.host)) {
282 /* Probably not an URL, so we can't make this absolute. */ 295 /* Probably not an URL, so we can't make this absolute. */
283 return urlMaybeRelative; 296 return urlMaybeRelative;
@@ -475,6 +488,9 @@ const char *mediaType_Path(const iString *path) {
475 endsWithCase_String(path, ".hpp")) { 488 endsWithCase_String(path, ".hpp")) {
476 return "text/plain"; 489 return "text/plain";
477 } 490 }
491 else if (endsWithCase_String(path, ".pem")) {
492 return "application/x-pem-file";
493 }
478 else if (endsWithCase_String(path, ".zip")) { 494 else if (endsWithCase_String(path, ".zip")) {
479 return "application/zip"; 495 return "application/zip";
480 } 496 }
diff --git a/src/gmutil.h b/src/gmutil.h
index 09d333e7..e7ff7cc5 100644
--- a/src/gmutil.h
+++ b/src/gmutil.h
@@ -110,6 +110,8 @@ iRangecc urlUser_String (const iString *);
110iRangecc urlRoot_String (const iString *); 110iRangecc urlRoot_String (const iString *);
111const iString * absoluteUrl_String (const iString *, const iString *urlMaybeRelative); 111const iString * absoluteUrl_String (const iString *, const iString *urlMaybeRelative);
112iBool isLikelyUrl_String (const iString *); 112iBool isLikelyUrl_String (const iString *);
113iBool isKnownScheme_Rangecc (iRangecc scheme); /* any URI scheme */
114iBool isKnownUrlScheme_Rangecc(iRangecc scheme); /* URL schemes only */
113void punyEncodeDomain_Rangecc(iRangecc domain, iString *encoded_out); 115void punyEncodeDomain_Rangecc(iRangecc domain, iString *encoded_out);
114void punyEncodeUrlHost_String(iString *absoluteUrl); 116void punyEncodeUrlHost_String(iString *absoluteUrl);
115void stripDefaultUrlPort_String(iString *); 117void stripDefaultUrlPort_String(iString *);
diff --git a/src/history.c b/src/history.c
index 87cf28e6..fdd0ff55 100644
--- a/src/history.c
+++ b/src/history.c
@@ -286,7 +286,7 @@ void add_History(iHistory *d, const iString *url ){
286 286
287iBool goBack_History(iHistory *d) { 287iBool goBack_History(iHistory *d) {
288 lock_Mutex(d->mtx); 288 lock_Mutex(d->mtx);
289 if (d->recentPos < size_Array(&d->recent) - 1) { 289 if (!isEmpty_Array(&d->recent) && d->recentPos < size_Array(&d->recent) - 1) {
290 d->recentPos++; 290 d->recentPos++;
291 postCommandf_Root(get_Root(), 291 postCommandf_Root(get_Root(),
292 "open history:1 scroll:%f url:%s", 292 "open history:1 scroll:%f url:%s",
@@ -322,7 +322,8 @@ iBool atLatest_History(const iHistory *d) {
322 322
323iBool atOldest_History(const iHistory *d) { 323iBool atOldest_History(const iHistory *d) {
324 iBool isOldest; 324 iBool isOldest;
325 iGuardMutex(d->mtx, isOldest = (d->recentPos == size_Array(&d->recent) - 1)); 325 iGuardMutex(d->mtx, isOldest = (isEmpty_Array(&d->recent) ||
326 d->recentPos == size_Array(&d->recent) - 1));
326 return isOldest; 327 return isOldest;
327} 328}
328 329
diff --git a/src/ipc.c b/src/ipc.c
index b0e996fb..ce98b6cf 100644
--- a/src/ipc.c
+++ b/src/ipc.c
@@ -176,8 +176,9 @@ iBool write_Ipc(iProcessId pid, const iString *input, enum iIpcWrite type) {
176 iFile *f = newCStr_File(inputFilePath_(&ipc_, pid)); 176 iFile *f = newCStr_File(inputFilePath_(&ipc_, pid));
177 if (open_File(f, text_FileMode | append_FileMode)) { 177 if (open_File(f, text_FileMode | append_FileMode)) {
178 write_File(f, utf8_String(input)); 178 write_File(f, utf8_String(input));
179 if (type == command_IpcWrite) { 179 if (type != response_IpcWrite) {
180 printf_Stream(stream_File(f), "\nipc.signal arg:%d\n", currentId_Process()); 180 printf_Stream(stream_File(f), "\nipc.signal arg:%d%s\n", currentId_Process(),
181 type == commandAndRaise_IpcWrite ? " raise:1" : "");
181 } 182 }
182 close_File(f); 183 close_File(f);
183 ok = iTrue; 184 ok = iTrue;
@@ -186,10 +187,10 @@ iBool write_Ipc(iProcessId pid, const iString *input, enum iIpcWrite type) {
186 return ok; 187 return ok;
187} 188}
188 189
189iString *communicate_Ipc(const iString *command) { 190iString *communicate_Ipc(const iString *command, iBool requestRaise) {
190 const iProcessId dst = check_Ipc(); 191 const iProcessId dst = check_Ipc();
191 if (dst) { 192 if (dst) {
192 if (write_Ipc(dst, command, command_IpcWrite)) { 193 if (write_Ipc(dst, command, requestRaise ? commandAndRaise_IpcWrite : command_IpcWrite)) {
193 response_ = new_IpcResponse(); 194 response_ = new_IpcResponse();
194 signal(SIGUSR1, handleSignal_IpcResponse_); 195 signal(SIGUSR1, handleSignal_IpcResponse_);
195 lock_Mutex(&response_->mtx); 196 lock_Mutex(&response_->mtx);
@@ -300,7 +301,7 @@ iBool write_Ipc(iProcessId pid, const iString *input, enum iIpcWrite type) {
300 return ok; 301 return ok;
301} 302}
302 303
303iString *communicate_Ipc(const iString *command) { 304iString *communicate_Ipc(const iString *command, iBool requestRaise) {
304 iProcessId pid = check_Ipc(); 305 iProcessId pid = check_Ipc();
305 if (!pid) { 306 if (!pid) {
306 return NULL; 307 return NULL;
@@ -308,7 +309,7 @@ iString *communicate_Ipc(const iString *command) {
308 /* Open a mailslot for the response. */ 309 /* Open a mailslot for the response. */
309 HANDLE responseSlot = CreateMailslotA(slotName_(currentId_Process()), 0, 1000, NULL); 310 HANDLE responseSlot = CreateMailslotA(slotName_(currentId_Process()), 0, 1000, NULL);
310 /* Write the commands. */ 311 /* Write the commands. */
311 if (!write_Ipc(pid, command, command_IpcWrite)) { 312 if (!write_Ipc(pid, command, requestRaise ? commandAndRaise_IpcWrite : command_IpcWrite)) {
312 CloseHandle(responseSlot); 313 CloseHandle(responseSlot);
313 return NULL; 314 return NULL;
314 } 315 }
diff --git a/src/ipc.h b/src/ipc.h
index 69852876..5fd6167d 100644
--- a/src/ipc.h
+++ b/src/ipc.h
@@ -31,11 +31,12 @@ void deinit_Ipc (void);
31 31
32iProcessId check_Ipc (void); 32iProcessId check_Ipc (void);
33void listen_Ipc (void); 33void listen_Ipc (void);
34iString * communicate_Ipc (const iString *command); 34iString * communicate_Ipc (const iString *command, iBool requestRaise);
35void signal_Ipc (iProcessId pid); 35void signal_Ipc (iProcessId pid);
36 36
37enum iIpcWrite { 37enum iIpcWrite {
38 command_IpcWrite, 38 command_IpcWrite,
39 commandAndRaise_IpcWrite,
39 response_IpcWrite, 40 response_IpcWrite,
40}; 41};
41 42
diff --git a/src/macos.m b/src/macos.m
index 709a97f7..6bacfdd1 100644
--- a/src/macos.m
+++ b/src/macos.m
@@ -60,8 +60,9 @@ static NSString *currentSystemAppearance_(void) {
60} 60}
61 61
62iBool shouldDefaultToMetalRenderer_MacOS(void) { 62iBool shouldDefaultToMetalRenderer_MacOS(void) {
63 return iFalse; /*
63 const iInt2 ver = macVer_(); 64 const iInt2 ver = macVer_();
64 return ver.x > 10 || ver.y > 13; 65 return ver.x > 10 || ver.y > 13;*/
65} 66}
66 67
67/*----------------------------------------------------------------------------------------------*/ 68/*----------------------------------------------------------------------------------------------*/
diff --git a/src/periodic.c b/src/periodic.c
index 068f2a1e..ef3d8033 100644
--- a/src/periodic.c
+++ b/src/periodic.c
@@ -91,9 +91,11 @@ iBool dispatchCommands_Periodic(iPeriodic *d) {
91 .data1 = (void *) cstr_String(&pc->command), 91 .data1 = (void *) cstr_String(&pc->command),
92 .data2 = findRoot_Window(get_Window(), pc->context) 92 .data2 = findRoot_Window(get_Window(), pc->context)
93 }; 93 };
94 setCurrent_Root(ev.data2); 94 if (ev.data2) {
95 dispatchEvent_Widget(pc->context, (const SDL_Event *) &ev); 95 setCurrent_Root(ev.data2);
96 wasPosted = iTrue; 96 dispatchEvent_Widget(pc->context, (const SDL_Event *) &ev);
97 wasPosted = iTrue;
98 }
97 } 99 }
98 removePending_Periodic_(d); 100 removePending_Periodic_(d);
99 setCurrent_Root(NULL); 101 setCurrent_Root(NULL);
diff --git a/src/prefs.c b/src/prefs.c
index e6eba2a6..385dee78 100644
--- a/src/prefs.c
+++ b/src/prefs.c
@@ -33,6 +33,7 @@ void init_Prefs(iPrefs *d) {
33 d->accent = cyan_ColorAccent; 33 d->accent = cyan_ColorAccent;
34 d->customFrame = iFalse; /* needs some more work to be default */ 34 d->customFrame = iFalse; /* needs some more work to be default */
35 d->retainWindowSize = iTrue; 35 d->retainWindowSize = iTrue;
36 d->uiAnimations = iTrue;
36 d->uiScale = 1.0f; /* default set elsewhere */ 37 d->uiScale = 1.0f; /* default set elsewhere */
37 d->zoomPercent = 100; 38 d->zoomPercent = 100;
38 d->sideIcon = iTrue; 39 d->sideIcon = iTrue;
@@ -67,6 +68,7 @@ void init_Prefs(iPrefs *d) {
67 init_String(&d->httpProxy); 68 init_String(&d->httpProxy);
68 init_String(&d->downloadDir); 69 init_String(&d->downloadDir);
69 init_String(&d->searchUrl); 70 init_String(&d->searchUrl);
71 init_String(&d->symbolFontPath);
70 /* TODO: Add some platform-specific common locations? */ 72 /* TODO: Add some platform-specific common locations? */
71 if (fileExistsCStr_FileInfo("/etc/ssl/cert.pem")) { /* macOS */ 73 if (fileExistsCStr_FileInfo("/etc/ssl/cert.pem")) { /* macOS */
72 setCStr_String(&d->caFile, "/etc/ssl/cert.pem"); 74 setCStr_String(&d->caFile, "/etc/ssl/cert.pem");
@@ -74,9 +76,15 @@ void init_Prefs(iPrefs *d) {
74 if (fileExistsCStr_FileInfo("/etc/ssl/certs")) { 76 if (fileExistsCStr_FileInfo("/etc/ssl/certs")) {
75 setCStr_String(&d->caPath, "/etc/ssl/certs"); 77 setCStr_String(&d->caPath, "/etc/ssl/certs");
76 } 78 }
79 /*
80#if defined (iPlatformAppleDesktop)
81 setCStr_String(&d->symbolFontPath, "/System/Library/Fonts/Apple Symbols.ttf");
82#endif
83 */
77} 84}
78 85
79void deinit_Prefs(iPrefs *d) { 86void deinit_Prefs(iPrefs *d) {
87 deinit_String(&d->symbolFontPath);
80 deinit_String(&d->searchUrl); 88 deinit_String(&d->searchUrl);
81 deinit_String(&d->geminiProxy); 89 deinit_String(&d->geminiProxy);
82 deinit_String(&d->gopherProxy); 90 deinit_String(&d->gopherProxy);
diff --git a/src/prefs.h b/src/prefs.h
index 0f604ee2..7185c8f9 100644
--- a/src/prefs.h
+++ b/src/prefs.h
@@ -47,6 +47,7 @@ struct Impl_Prefs {
47 enum iColorAccent accent; 47 enum iColorAccent accent;
48 iBool customFrame; /* when LAGRANGE_ENABLE_CUSTOM_FRAME is defined */ 48 iBool customFrame; /* when LAGRANGE_ENABLE_CUSTOM_FRAME is defined */
49 iBool retainWindowSize; 49 iBool retainWindowSize;
50 iBool uiAnimations;
50 float uiScale; 51 float uiScale;
51 int zoomPercent; 52 int zoomPercent;
52 iBool sideIcon; 53 iBool sideIcon;
@@ -69,6 +70,7 @@ struct Impl_Prefs {
69 iString gopherProxy; 70 iString gopherProxy;
70 iString httpProxy; 71 iString httpProxy;
71 /* Style */ 72 /* Style */
73 iString symbolFontPath;
72 enum iTextFont font; 74 enum iTextFont font;
73 enum iTextFont headingFont; 75 enum iTextFont headingFont;
74 iBool monospaceGemini; 76 iBool monospaceGemini;
diff --git a/src/ui/certimportwidget.c b/src/ui/certimportwidget.c
index fdc189db..6e818137 100644
--- a/src/ui/certimportwidget.c
+++ b/src/ui/certimportwidget.c
@@ -114,6 +114,7 @@ void init_CertImportWidget(iCertImportWidget *d) {
114 setFlags_Widget(w, 114 setFlags_Widget(w,
115 mouseModal_WidgetFlag | keepOnTop_WidgetFlag | arrangeVertical_WidgetFlag | 115 mouseModal_WidgetFlag | keepOnTop_WidgetFlag | arrangeVertical_WidgetFlag |
116 arrangeSize_WidgetFlag | centerHorizontal_WidgetFlag | 116 arrangeSize_WidgetFlag | centerHorizontal_WidgetFlag |
117 parentCannotResize_WidgetFlag |
117 overflowScrollable_WidgetFlag, 118 overflowScrollable_WidgetFlag,
118 iTrue); 119 iTrue);
119 } 120 }
@@ -214,11 +215,20 @@ static iBool processEvent_CertImportWidget_(iCertImportWidget *d, const SDL_Even
214 return iTrue; 215 return iTrue;
215 } 216 }
216 } 217 }
218 if (isCommand_UserEvent(ev, "input.paste")) {
219 if (!tryImportFromClipboard_CertImportWidget_(d)) {
220 makeSimpleMessage_Widget(uiTextCaution_ColorEscape "${heading.certimport.pasted}",
221 "${dlg.certimport.notfound}");
222 }
223 postRefresh_App();
224 return iTrue;
225 }
217 if (isCommand_UserEvent(ev, "certimport.paste")) { 226 if (isCommand_UserEvent(ev, "certimport.paste")) {
218 tryImportFromClipboard_CertImportWidget_(d); 227 tryImportFromClipboard_CertImportWidget_(d);
219 return iTrue; 228 return iTrue;
220 } 229 }
221 if (isCommand_Widget(w, ev, "cancel")) { 230 if (isCommand_Widget(w, ev, "cancel")) {
231 setupSheetTransition_Mobile(w, iFalse);
222 destroy_Widget(w); 232 destroy_Widget(w);
223 return iTrue; 233 return iTrue;
224 } 234 }
@@ -226,6 +236,7 @@ static iBool processEvent_CertImportWidget_(iCertImportWidget *d, const SDL_Even
226 if (d->cert && !isEmpty_TlsCertificate(d->cert) && hasPrivateKey_TlsCertificate(d->cert)) { 236 if (d->cert && !isEmpty_TlsCertificate(d->cert) && hasPrivateKey_TlsCertificate(d->cert)) {
227 importIdentity_GmCerts(certs_App(), d->cert, text_InputWidget(d->notes)); 237 importIdentity_GmCerts(certs_App(), d->cert, text_InputWidget(d->notes));
228 d->cert = NULL; /* taken */ 238 d->cert = NULL; /* taken */
239 setupSheetTransition_Mobile(w, iFalse);
229 destroy_Widget(w); 240 destroy_Widget(w);
230 postCommand_App("idents.changed"); 241 postCommand_App("idents.changed");
231 } 242 }
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index 94337f8d..29e264e8 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -222,6 +222,7 @@ enum iDocumentWidgetFlag {
222 movingSelectMarkStart_DocumentWidgetFlag = iBit(10), 222 movingSelectMarkStart_DocumentWidgetFlag = iBit(10),
223 movingSelectMarkEnd_DocumentWidgetFlag = iBit(11), 223 movingSelectMarkEnd_DocumentWidgetFlag = iBit(11),
224 otherRootByDefault_DocumentWidgetFlag = iBit(12), /* links open to other root by default */ 224 otherRootByDefault_DocumentWidgetFlag = iBit(12), /* links open to other root by default */
225 urlChanged_DocumentWidgetFlag = iBit(13),
225}; 226};
226 227
227enum iDocumentLinkOrdinalMode { 228enum iDocumentLinkOrdinalMode {
@@ -231,15 +232,41 @@ enum iDocumentLinkOrdinalMode {
231 232
232struct Impl_DocumentWidget { 233struct Impl_DocumentWidget {
233 iWidget widget; 234 iWidget widget;
234 enum iRequestState state;
235 iPersistentDocumentState mod;
236 int flags; 235 int flags;
236
237 /* User interface: */
237 enum iDocumentLinkOrdinalMode ordinalMode; 238 enum iDocumentLinkOrdinalMode ordinalMode;
238 size_t ordinalBase; 239 size_t ordinalBase;
239 iString * titleUser; 240 iRangecc selectMark;
241 iRangecc initialSelectMark; /* for word/line selection */
242 iRangecc foundMark;
243 const iGmRun * grabbedPlayer; /* currently adjusting volume in a player */
244 float grabbedStartVolume;
245 int mediaTimer;
246 const iGmRun * hoverPre; /* for clicking */
247 const iGmRun * hoverAltPre; /* for drawing alt text */
248 const iGmRun * hoverLink;
249 const iGmRun * contextLink;
250 iClick click;
251 iInt2 contextPos; /* coordinates of latest right click */
252 int pinchZoomInitial;
253 int pinchZoomPosted;
254 iString pendingGotoHeading;
255
256 /* Network request: */
257 enum iRequestState state;
240 iGmRequest * request; 258 iGmRequest * request;
241 iAtomicInt isRequestUpdated; /* request has new content, need to parse it */ 259 iAtomicInt isRequestUpdated; /* request has new content, need to parse it */
242 iObjectList * media; 260 int certFlags;
261 iBlock * certFingerprint;
262 iDate certExpiry;
263 iString * certSubject;
264 int redirectCount;
265 iObjectList * media; /* inline media requests */
266
267 /* Document: */
268 iPersistentDocumentState mod;
269 iString * titleUser;
243 enum iGmStatusCode sourceStatus; 270 enum iGmStatusCode sourceStatus;
244 iString sourceHeader; 271 iString sourceHeader;
245 iString sourceMime; 272 iString sourceMime;
@@ -247,53 +274,36 @@ struct Impl_DocumentWidget {
247 iTime sourceTime; 274 iTime sourceTime;
248 iGempub * sourceGempub; /* NULL unless the page is Gempub content */ 275 iGempub * sourceGempub; /* NULL unless the page is Gempub content */
249 iGmDocument * doc; 276 iGmDocument * doc;
250 int certFlags; 277
251 iBlock * certFingerprint; 278 /* Rendering: */
252 iDate certExpiry;
253 iString * certSubject;
254 int redirectCount;
255 iRangecc selectMark;
256 iRangecc initialSelectMark; /* for word/line selection */
257 iRangecc foundMark;
258 int pageMargin; 279 int pageMargin;
280 float initNormScrollY;
281 iSmoothScroll scrollY;
282 iAnim sideOpacity;
283 iAnim altTextOpacity;
284 iGmRunRange visibleRuns;
259 iPtrArray visibleLinks; 285 iPtrArray visibleLinks;
260 iPtrArray visiblePre; 286 iPtrArray visiblePre;
287 iPtrArray visibleMedia; /* currently playing audio / ongoing downloads */
261 iPtrArray visibleWideRuns; /* scrollable blocks; TODO: merge into `visiblePre` */ 288 iPtrArray visibleWideRuns; /* scrollable blocks; TODO: merge into `visiblePre` */
262 iArray wideRunOffsets; 289 iArray wideRunOffsets;
263 iAnim animWideRunOffset; 290 iAnim animWideRunOffset;
264 uint16_t animWideRunId; 291 uint16_t animWideRunId;
265 iGmRunRange animWideRunRange; 292 iGmRunRange animWideRunRange;
266 iPtrArray visibleMedia; /* currently playing audio / ongoing downloads */ 293 iDrawBufs * drawBufs; /* dynamic state for drawing */
267 const iGmRun * grabbedPlayer; /* currently adjusting volume in a player */ 294 iVisBuf * visBuf;
268 float grabbedStartVolume; 295 iVisBufMeta * visBufMeta;
269 int mediaTimer;
270 const iGmRun * hoverPre; /* for clicking */
271 const iGmRun * hoverAltPre; /* for drawing alt text */
272 const iGmRun * hoverLink;
273 const iGmRun * contextLink;
274 iGmRunRange visibleRuns;
275 iGmRunRange renderRuns; 296 iGmRunRange renderRuns;
276 iClick click; 297 iPtrSet * invalidRuns;
277 iInt2 contextPos; /* coordinates of latest right click */ 298
278 iString pendingGotoHeading; 299 /* Widget structure: */
279 float initNormScrollY;
280// iAnim scrollY;
281// int overscroll;
282 iSmoothScroll scrollY;
283 iAnim sideOpacity;
284 iAnim altTextOpacity;
285 iScrollWidget *scroll; 300 iScrollWidget *scroll;
301 iWidget * footerButtons;
286 iWidget * menu; 302 iWidget * menu;
287 iWidget * playerMenu; 303 iWidget * playerMenu;
288 iWidget * copyMenu; 304 iWidget * copyMenu;
289 iVisBuf * visBuf;
290 iVisBufMeta * visBufMeta;
291 iPtrSet * invalidRuns;
292 iDrawBufs * drawBufs; /* dynamic state for drawing */
293 iTranslation * translation; 305 iTranslation * translation;
294 iWidget * phoneToolbar; 306 iWidget * phoneToolbar;
295 int pinchZoomInitial;
296 int pinchZoomPosted;
297}; 307};
298 308
299iDefineObjectConstruction(DocumentWidget) 309iDefineObjectConstruction(DocumentWidget)
@@ -308,6 +318,7 @@ void init_DocumentWidget(iDocumentWidget *d) {
308 init_PersistentDocumentState(&d->mod); 318 init_PersistentDocumentState(&d->mod);
309 d->flags = 0; 319 d->flags = 0;
310 d->phoneToolbar = NULL; 320 d->phoneToolbar = NULL;
321 d->footerButtons = NULL;
311 iZap(d->certExpiry); 322 iZap(d->certExpiry);
312 d->certFingerprint = new_Block(0); 323 d->certFingerprint = new_Block(0);
313 d->certFlags = 0; 324 d->certFlags = 0;
@@ -321,7 +332,6 @@ void init_DocumentWidget(iDocumentWidget *d) {
321 d->redirectCount = 0; 332 d->redirectCount = 0;
322 d->ordinalBase = 0; 333 d->ordinalBase = 0;
323 d->initNormScrollY = 0; 334 d->initNormScrollY = 0;
324 //init_Anim(&d->scrollY, 0);
325 init_SmoothScroll(&d->scrollY, w, scrollBegan_DocumentWidget_); 335 init_SmoothScroll(&d->scrollY, w, scrollBegan_DocumentWidget_);
326 d->animWideRunId = 0; 336 d->animWideRunId = 0;
327 init_Anim(&d->animWideRunOffset, 0); 337 init_Anim(&d->animWideRunOffset, 0);
@@ -573,7 +583,8 @@ static float normScrollPos_DocumentWidget_(const iDocumentWidget *d) {
573static int scrollMax_DocumentWidget_(const iDocumentWidget *d) { 583static int scrollMax_DocumentWidget_(const iDocumentWidget *d) {
574 const iWidget *w = constAs_Widget(d); 584 const iWidget *w = constAs_Widget(d);
575 int sm = size_GmDocument(d->doc).y - height_Rect(bounds_Widget(w)) + 585 int sm = size_GmDocument(d->doc).y - height_Rect(bounds_Widget(w)) +
576 (hasSiteBanner_GmDocument(d->doc) ? 1 : 2) * d->pageMargin * gap_UI; 586 (hasSiteBanner_GmDocument(d->doc) ? 1 : 2) * d->pageMargin * gap_UI +
587 height_Widget(d->footerButtons);
577 if (d->phoneToolbar) { 588 if (d->phoneToolbar) {
578 sm += size_Root(w->root).y - 589 sm += size_Root(w->root).y -
579 top_Rect(boundsWithoutVisualOffset_Widget(d->phoneToolbar)); 590 top_Rect(boundsWithoutVisualOffset_Widget(d->phoneToolbar));
@@ -809,6 +820,13 @@ static iRangecc currentHeading_DocumentWidget_(const iDocumentWidget *d) {
809 return heading; 820 return heading;
810} 821}
811 822
823static int updateScrollMax_DocumentWidget_(iDocumentWidget *d) {
824 arrange_Widget(d->footerButtons); /* scrollMax depends on footer height */
825 const int scrollMax = scrollMax_DocumentWidget_(d);
826 setMax_SmoothScroll(&d->scrollY, scrollMax);
827 return scrollMax;
828}
829
812static void updateVisible_DocumentWidget_(iDocumentWidget *d) { 830static void updateVisible_DocumentWidget_(iDocumentWidget *d) {
813 iChangeFlags(d->flags, 831 iChangeFlags(d->flags,
814 centerVertically_DocumentWidgetFlag, 832 centerVertically_DocumentWidgetFlag,
@@ -816,8 +834,26 @@ static void updateVisible_DocumentWidget_(iDocumentWidget *d) {
816 !isSuccess_GmStatusCode(d->sourceStatus)); 834 !isSuccess_GmStatusCode(d->sourceStatus));
817 const iRangei visRange = visibleRange_DocumentWidget_(d); 835 const iRangei visRange = visibleRange_DocumentWidget_(d);
818 const iRect bounds = bounds_Widget(as_Widget(d)); 836 const iRect bounds = bounds_Widget(as_Widget(d));
819 const int scrollMax = scrollMax_DocumentWidget_(d); 837 const int scrollMax = updateScrollMax_DocumentWidget_(d);
820 setMax_SmoothScroll(&d->scrollY, scrollMax); 838 /* Reposition the footer buttons as appropriate. */
839 /* TODO: You can just position `footerButtons` here completely without having to get
840 `Widget` involved with the offset in any way. */
841 if (d->footerButtons) {
842 const iRect bounds = bounds_Widget(as_Widget(d));
843 const iRect docBounds = documentBounds_DocumentWidget_(d);
844 const int hPad = (width_Rect(bounds) - iMin(120 * gap_UI, width_Rect(docBounds))) / 2;
845 const int vPad = 3 * gap_UI;
846 setPadding_Widget(d->footerButtons, hPad, vPad, hPad, vPad);
847 d->footerButtons->animOffsetRef = (scrollMax > 0 ? &d->scrollY.pos : NULL);
848 if (scrollMax <= 0) {
849 d->footerButtons->animOffsetRef = NULL;
850 d->footerButtons->rect.pos.y = height_Rect(bounds) - height_Widget(d->footerButtons);
851 }
852 else {
853 d->footerButtons->animOffsetRef = &d->scrollY.pos;
854 d->footerButtons->rect.pos.y = size_GmDocument(d->doc).y + 2 * gap_UI * d->pageMargin;
855 }
856 }
821 setRange_ScrollWidget(d->scroll, (iRangei){ 0, scrollMax }); 857 setRange_ScrollWidget(d->scroll, (iRangei){ 0, scrollMax });
822 const int docSize = size_GmDocument(d->doc).y; 858 const int docSize = size_GmDocument(d->doc).y;
823 setThumb_ScrollWidget(d->scroll, 859 setThumb_ScrollWidget(d->scroll,
@@ -1036,6 +1072,35 @@ static enum iGmDocumentBanner bannerType_DocumentWidget_(const iDocumentWidget *
1036 return siteDomain_GmDocumentBanner; 1072 return siteDomain_GmDocumentBanner;
1037} 1073}
1038 1074
1075static void makeFooterButtons_DocumentWidget_(iDocumentWidget *d, const iMenuItem *items, size_t count) {
1076 iWidget *w = as_Widget(d);
1077 destroy_Widget(d->footerButtons);
1078 d->footerButtons = NULL;
1079 if (count == 0) {
1080 return;
1081 }
1082 d->footerButtons = new_Widget();
1083 setFlags_Widget(d->footerButtons,
1084 unhittable_WidgetFlag | arrangeVertical_WidgetFlag |
1085 resizeWidthOfChildren_WidgetFlag | arrangeHeight_WidgetFlag |
1086 fixedPosition_WidgetFlag | resizeToParentWidth_WidgetFlag,
1087 iTrue);
1088 //setBackgroundColor_Widget(d->footerButtons, tmBackground_ColorId);
1089 for (size_t i = 0; i < count; ++i) {
1090 iLabelWidget *button = addChildFlags_Widget(
1091 d->footerButtons,
1092 iClob(newKeyMods_LabelWidget(
1093 items[i].label, items[i].key, items[i].kmods, items[i].command)),
1094 alignLeft_WidgetFlag | drawKey_WidgetFlag);
1095 checkIcon_LabelWidget(button);
1096 setFont_LabelWidget(button, uiContent_FontId);
1097 }
1098 addChild_Widget(as_Widget(d), iClob(d->footerButtons));
1099 arrange_Widget(d->footerButtons);
1100 arrange_Widget(w);
1101 updateVisible_DocumentWidget_(d); /* final placement for the buttons */
1102}
1103
1039static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode code, 1104static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode code,
1040 const iString *meta) { 1105 const iString *meta) {
1041 iString *src = collectNewCStr_String("# "); 1106 iString *src = collectNewCStr_String("# ");
@@ -1061,10 +1126,17 @@ static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode
1061 iString *key = collectNew_String(); 1126 iString *key = collectNew_String();
1062 toString_Sym(SDLK_s, KMOD_PRIMARY, key); 1127 toString_Sym(SDLK_s, KMOD_PRIMARY, key);
1063 appendFormat_String(src, "\n```\n%s\n```\n", cstr_String(meta)); 1128 appendFormat_String(src, "\n```\n%s\n```\n", cstr_String(meta));
1064 appendFormat_String(src, 1129// appendFormat_String(src,
1065 cstr_Lang("error.unsupported.suggestsave"), 1130// cstr_Lang("error.unsupported.suggestsave"),
1066 cstr_String(key), 1131// cstr_String(key),
1067 saveToDownloads_Label); 1132// saveToDownloads_Label);
1133 makeFooterButtons_DocumentWidget_(
1134 d,
1135 (iMenuItem[]){ { translateCStr_Lang(download_Icon " " saveToDownloads_Label),
1136 0,
1137 0,
1138 "document.save" } },
1139 1);
1068 break; 1140 break;
1069 } 1141 }
1070 case slowDown_GmStatusCode: 1142 case slowDown_GmStatusCode:
@@ -1072,9 +1144,19 @@ static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode
1072 cstr_String(meta)); 1144 cstr_String(meta));
1073 break; 1145 break;
1074 default: 1146 default:
1147 if (!isEmpty_String(meta)) {
1148 appendFormat_String(src, "\n\n${error.server.msg}\n> %s", cstr_String(meta));
1149 }
1075 break; 1150 break;
1076 } 1151 }
1077 } 1152 }
1153 if (category_GmStatusCode(code) == categoryClientCertificate_GmStatus) {
1154 makeFooterButtons_DocumentWidget_(
1155 d,
1156 (iMenuItem[]){ { leftHalf_Icon " ${menu.show.identities}", '4', KMOD_PRIMARY, "sidebar.mode arg:3 show:1" },
1157 { person_Icon " ${menu.identity.new}", newIdentity_KeyShortcut, "ident.new" } },
1158 2);
1159 }
1078 setBanner_GmDocument(d->doc, useBanner ? bannerType_DocumentWidget_(d) : none_GmDocumentBanner); 1160 setBanner_GmDocument(d->doc, useBanner ? bannerType_DocumentWidget_(d) : none_GmDocumentBanner);
1079 setFormat_GmDocument(d->doc, gemini_SourceFormat); 1161 setFormat_GmDocument(d->doc, gemini_SourceFormat);
1080 translate_Lang(src); 1162 translate_Lang(src);
@@ -1159,11 +1241,102 @@ static void postProcessRequestContent_DocumentWidget_(iDocumentWidget *d, iBool
1159 } 1241 }
1160 } 1242 }
1161 if (d->sourceGempub) { 1243 if (d->sourceGempub) {
1162 if (equal_String(d->mod.url, coverPageUrl_Gempub(d->sourceGempub)) && 1244 if (equal_String(d->mod.url, coverPageUrl_Gempub(d->sourceGempub))) {
1163 preloadCoverImage_Gempub(d->sourceGempub, d->doc)) { 1245 if (!isRemote_Gempub(d->sourceGempub)) {
1246 iArray *items = collectNew_Array(sizeof(iMenuItem));
1247 pushBack_Array(
1248 items,
1249 &(iMenuItem){ book_Icon " ${gempub.cover.view}",
1250 0,
1251 0,
1252 format_CStr("!open url:%s",
1253 cstr_String(indexPageUrl_Gempub(d->sourceGempub))) });
1254 if (navSize_Gempub(d->sourceGempub) > 0) {
1255 pushBack_Array(
1256 items,
1257 &(iMenuItem){
1258 format_CStr(forwardArrow_Icon " %s",
1259 cstr_String(navLinkLabel_Gempub(d->sourceGempub, 0))),
1260 SDLK_RIGHT,
1261 0,
1262 format_CStr("!open url:%s",
1263 cstr_String(navLinkUrl_Gempub(d->sourceGempub, 0))) });
1264 }
1265 makeFooterButtons_DocumentWidget_(d, constData_Array(items), size_Array(items));
1266 }
1267 else {
1268 makeFooterButtons_DocumentWidget_(
1269 d,
1270 (iMenuItem[]){ { book_Icon " ${menu.save.downloads.open}",
1271 SDLK_s,
1272 KMOD_PRIMARY | KMOD_SHIFT,
1273 "document.save open:1" },
1274 { download_Icon " " saveToDownloads_Label,
1275 SDLK_s,
1276 KMOD_PRIMARY,
1277 "document.save" } },
1278 2);
1279 }
1280 if (preloadCoverImage_Gempub(d->sourceGempub, d->doc)) {
1164 redoLayout_GmDocument(d->doc); 1281 redoLayout_GmDocument(d->doc);
1165 updateVisible_DocumentWidget_(d); 1282 updateVisible_DocumentWidget_(d);
1166 invalidate_DocumentWidget_(d); 1283 invalidate_DocumentWidget_(d);
1284 }
1285 }
1286 else if (equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {
1287 makeFooterButtons_DocumentWidget_(
1288 d,
1289 (iMenuItem[]){ { format_CStr(book_Icon " %s",
1290 cstr_String(property_Gempub(d->sourceGempub,
1291 title_GempubProperty))),
1292 SDLK_LEFT,
1293 0,
1294 format_CStr("!open url:%s",
1295 cstr_String(coverPageUrl_Gempub(d->sourceGempub))) } },
1296 1);
1297 }
1298 else {
1299 /* Navigation buttons. */
1300 iArray *items = collectNew_Array(sizeof(iMenuItem));
1301 const size_t navIndex = navIndex_Gempub(d->sourceGempub, d->mod.url);
1302 if (navIndex != iInvalidPos) {
1303 if (navIndex < navSize_Gempub(d->sourceGempub) - 1) {
1304 pushBack_Array(
1305 items,
1306 &(iMenuItem){
1307 format_CStr(forwardArrow_Icon " %s",
1308 cstr_String(navLinkLabel_Gempub(d->sourceGempub, navIndex + 1))),
1309 SDLK_RIGHT,
1310 0,
1311 format_CStr("!open url:%s",
1312 cstr_String(navLinkUrl_Gempub(d->sourceGempub, navIndex + 1))) });
1313 }
1314 if (navIndex > 0) {
1315 pushBack_Array(
1316 items,
1317 &(iMenuItem){
1318 format_CStr(backArrow_Icon " %s",
1319 cstr_String(navLinkLabel_Gempub(d->sourceGempub, navIndex - 1))),
1320 SDLK_LEFT,
1321 0,
1322 format_CStr("!open url:%s",
1323 cstr_String(navLinkUrl_Gempub(d->sourceGempub, navIndex - 1))) });
1324 }
1325 else if (!equalCase_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {
1326 pushBack_Array(
1327 items,
1328 &(iMenuItem){
1329 format_CStr(book_Icon " %s",
1330 cstr_String(property_Gempub(d->sourceGempub, title_GempubProperty))),
1331 SDLK_LEFT,
1332 0,
1333 format_CStr("!open url:%s",
1334 cstr_String(coverPageUrl_Gempub(d->sourceGempub))) });
1335 }
1336 }
1337 if (!isEmpty_Array(items)) {
1338 makeFooterButtons_DocumentWidget_(d, constData_Array(items), size_Array(items));
1339 }
1167 } 1340 }
1168 if (!isCached && prefs_App()->pinSplit && 1341 if (!isCached && prefs_App()->pinSplit &&
1169 equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) { 1342 equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {
@@ -1227,7 +1400,9 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d,
1227 setRange_String(&d->sourceMime, param); 1400 setRange_String(&d->sourceMime, param);
1228 } 1401 }
1229 else if (startsWith_Rangecc(param, "text/") || 1402 else if (startsWith_Rangecc(param, "text/") ||
1230 equal_Rangecc(param, "application/json")) { 1403 equal_Rangecc(param, "application/json") ||
1404 equal_Rangecc(param, "application/x-pem-file") ||
1405 equal_Rangecc(param, "application/pem-certificate-chain")) {
1231 docFormat = plainText_SourceFormat; 1406 docFormat = plainText_SourceFormat;
1232 setRange_String(&d->sourceMime, param); 1407 setRange_String(&d->sourceMime, param);
1233 } 1408 }
@@ -1400,44 +1575,58 @@ static void cacheDocumentGlyphs_DocumentWidget_(const iDocumentWidget *d) {
1400 } 1575 }
1401} 1576}
1402 1577
1578static void updateFromCachedResponse_DocumentWidget_(iDocumentWidget *d, float normScrollY,
1579 const iGmResponse *resp, iGmDocument *cachedDoc) {
1580 setLinkNumberMode_DocumentWidget_(d, iFalse);
1581 clear_ObjectList(d->media);
1582 delete_Gempub(d->sourceGempub);
1583 d->sourceGempub = NULL;
1584 iRelease(d->doc);
1585 destroy_Widget(d->footerButtons);
1586 d->footerButtons = NULL;
1587 d->doc = new_GmDocument();
1588 resetWideRuns_DocumentWidget_(d);
1589 d->state = fetching_RequestState;
1590 /* Do the fetch. */ {
1591 d->initNormScrollY = normScrollY;
1592 /* Use the cached response data. */
1593 updateTrust_DocumentWidget_(d, resp);
1594 d->sourceTime = resp->when;
1595 d->sourceStatus = success_GmStatusCode;
1596 format_String(&d->sourceHeader, cstr_Lang("pageinfo.header.cached"));
1597 set_Block(&d->sourceContent, &resp->body);
1598 updateDocument_DocumentWidget_(d, resp, cachedDoc, iTrue);
1599 setCachedDocument_History(d->mod.history, d->doc);
1600 }
1601 d->state = ready_RequestState;
1602 postProcessRequestContent_DocumentWidget_(d, iTrue);
1603 init_Anim(&d->altTextOpacity, 0);
1604 reset_SmoothScroll(&d->scrollY);
1605 init_Anim(&d->scrollY.pos, d->initNormScrollY * size_GmDocument(d->doc).y);
1606 updateSideOpacity_DocumentWidget_(d, iFalse);
1607 updateVisible_DocumentWidget_(d);
1608 moveSpan_SmoothScroll(&d->scrollY, 0, 0); /* clamp position to new max */
1609 cacheDocumentGlyphs_DocumentWidget_(d);
1610 d->drawBufs->flags |= updateTimestampBuf_DrawBufsFlag | updateSideBuf_DrawBufsFlag;
1611 d->flags &= ~urlChanged_DocumentWidgetFlag;
1612 postCommandf_Root(
1613 as_Widget(d)->root, "document.changed doc:%p url:%s", d, cstr_String(d->mod.url));
1614}
1615
1403static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) { 1616static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) {
1404 const iRecentUrl *recent = findUrl_History(d->mod.history, d->mod.url); 1617 const iRecentUrl *recent = findUrl_History(d->mod.history, withSpacesEncoded_String(d->mod.url));
1405 if (recent && recent->cachedResponse) { 1618 if (recent && recent->cachedResponse) {
1406 const iGmResponse *resp = recent->cachedResponse; 1619 updateFromCachedResponse_DocumentWidget_(
1407 clear_ObjectList(d->media); 1620 d, recent->normScrollY, recent->cachedResponse, recent->cachedDoc);
1408 delete_Gempub(d->sourceGempub);
1409 d->sourceGempub = NULL;
1410 iRelease(d->doc);
1411 d->doc = new_GmDocument();
1412 resetWideRuns_DocumentWidget_(d);
1413 d->state = fetching_RequestState;
1414 /* Do the fetch. */ {
1415 d->initNormScrollY = recent->normScrollY;
1416 /* Use the cached response data. */
1417 updateTrust_DocumentWidget_(d, resp);
1418 d->sourceTime = resp->when;
1419 d->sourceStatus = success_GmStatusCode;
1420 format_String(&d->sourceHeader, cstr_Lang("pageinfo.header.cached"));
1421 set_Block(&d->sourceContent, &resp->body);
1422 updateDocument_DocumentWidget_(d, resp, recent->cachedDoc, iTrue);
1423 setCachedDocument_History(d->mod.history, d->doc);
1424 }
1425 d->state = ready_RequestState;
1426 postProcessRequestContent_DocumentWidget_(d, iTrue);
1427 init_Anim(&d->altTextOpacity, 0);
1428 reset_SmoothScroll(&d->scrollY);
1429 init_Anim(&d->scrollY.pos, d->initNormScrollY * size_GmDocument(d->doc).y);
1430 updateSideOpacity_DocumentWidget_(d, iFalse);
1431 updateVisible_DocumentWidget_(d);
1432 moveSpan_SmoothScroll(&d->scrollY, 0, 0); /* clamp position to new max */
1433 cacheDocumentGlyphs_DocumentWidget_(d);
1434 d->drawBufs->flags |= updateTimestampBuf_DrawBufsFlag | updateSideBuf_DrawBufsFlag;
1435 postCommandf_Root(as_Widget(d)->root, "document.changed doc:%p url:%s", d, cstr_String(d->mod.url));
1436 return iTrue; 1621 return iTrue;
1437 } 1622 }
1438 else if (!isEmpty_String(d->mod.url)) { 1623 else if (!isEmpty_String(d->mod.url)) {
1439 fetch_DocumentWidget_(d); 1624 fetch_DocumentWidget_(d);
1440 } 1625 }
1626 if (recent) {
1627 /* Retain scroll position in refetched content as well. */
1628 d->initNormScrollY = recent->normScrollY;
1629 }
1441 return iFalse; 1630 return iFalse;
1442} 1631}
1443 1632
@@ -1674,11 +1863,16 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) {
1674 break; 1863 break;
1675 } 1864 }
1676 case categorySuccess_GmStatusCode: 1865 case categorySuccess_GmStatusCode:
1677 //reset_SmoothScroll(&d->scrollY); 1866 if (d->flags & urlChanged_DocumentWidgetFlag) {
1867 /* Keep scroll position when reloading the same page. */
1868 reset_SmoothScroll(&d->scrollY);
1869 }
1678 iRelease(d->doc); /* new content incoming */ 1870 iRelease(d->doc); /* new content incoming */
1679 d->doc = new_GmDocument(); 1871 d->doc = new_GmDocument();
1680 delete_Gempub(d->sourceGempub); 1872 delete_Gempub(d->sourceGempub);
1681 d->sourceGempub = NULL; 1873 d->sourceGempub = NULL;
1874 destroy_Widget(d->footerButtons);
1875 d->footerButtons = NULL;
1682 resetWideRuns_DocumentWidget_(d); 1876 resetWideRuns_DocumentWidget_(d);
1683 updateDocument_DocumentWidget_(d, resp, NULL, iTrue); 1877 updateDocument_DocumentWidget_(d, resp, NULL, iTrue);
1684 break; 1878 break;
@@ -1902,7 +2096,8 @@ static iBool fetchNextUnfetchedImage_DocumentWidget_(iDocumentWidget *d) {
1902 return iFalse; 2096 return iFalse;
1903} 2097}
1904 2098
1905static void saveToDownloads_(const iString *url, const iString *mime, const iBlock *content) { 2099static const iString *saveToDownloads_(const iString *url, const iString *mime, const iBlock *content,
2100 iBool showDialog) {
1906 const iString *savePath = downloadPathForUrl_App(url, mime); 2101 const iString *savePath = downloadPathForUrl_App(url, mime);
1907 /* Write the file. */ { 2102 /* Write the file. */ {
1908 iFile *f = new_File(savePath); 2103 iFile *f = new_File(savePath);
@@ -1914,17 +2109,22 @@ static void saveToDownloads_(const iString *url, const iString *mime, const iBlo
1914#if defined (iPlatformAppleMobile) 2109#if defined (iPlatformAppleMobile)
1915 exportDownloadedFile_iOS(savePath); 2110 exportDownloadedFile_iOS(savePath);
1916#else 2111#else
2112 if (showDialog) {
1917 const iMenuItem items[2] = { 2113 const iMenuItem items[2] = {
1918 { "${dlg.save.opendownload}", 0, 0, 2114 { "${dlg.save.opendownload}", 0, 0,
1919 format_CStr("!open url:%s", cstrCollect_String(makeFileUrl_String(savePath))) }, 2115 format_CStr("!open url:%s", cstrCollect_String(makeFileUrl_String(savePath))) },
1920 { "${dlg.message.ok}", 0, 0, "message.ok" }, 2116 { "${dlg.message.ok}", 0, 0, "message.ok" },
1921 }; 2117 };
1922 makeMessage_Widget(uiHeading_ColorEscape "${heading.save}", 2118 makeMessage_Widget(uiHeading_ColorEscape "${heading.save}",
1923 format_CStr("%s\n${dlg.save.size} %.3f %s", cstr_String(path_File(f)), 2119 format_CStr("%s\n${dlg.save.size} %.3f %s",
2120 cstr_String(path_File(f)),
1924 isMega ? size / 1.0e6f : (size / 1.0e3f), 2121 isMega ? size / 1.0e6f : (size / 1.0e3f),
1925 isMega ? "${mb}" : "${kb}"), 2122 isMega ? "${mb}" : "${kb}"),
1926 items, iElemCount(items)); 2123 items,
2124 iElemCount(items));
2125 }
1927#endif 2126#endif
2127 return savePath;
1928 } 2128 }
1929 else { 2129 else {
1930 makeSimpleMessage_Widget(uiTextCaution_ColorEscape "${heading.save.error}", 2130 makeSimpleMessage_Widget(uiTextCaution_ColorEscape "${heading.save.error}",
@@ -1932,6 +2132,7 @@ static void saveToDownloads_(const iString *url, const iString *mime, const iBlo
1932 } 2132 }
1933 iRelease(f); 2133 iRelease(f);
1934 } 2134 }
2135 return collectNew_String();
1935} 2136}
1936 2137
1937static void addAllLinks_(void *context, const iGmRun *run) { 2138static void addAllLinks_(void *context, const iGmRun *run) {
@@ -2061,6 +2262,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2061 const iBool keepCenter = equal_Command(cmd, "font.changed"); 2262 const iBool keepCenter = equal_Command(cmd, "font.changed");
2062 updateDocumentWidthRetainingScrollPosition_DocumentWidget_(d, keepCenter); 2263 updateDocumentWidthRetainingScrollPosition_DocumentWidget_(d, keepCenter);
2063 d->drawBufs->flags |= updateSideBuf_DrawBufsFlag; 2264 d->drawBufs->flags |= updateSideBuf_DrawBufsFlag;
2265 updateVisible_DocumentWidget_(d);
2064 invalidate_DocumentWidget_(d); 2266 invalidate_DocumentWidget_(d);
2065 dealloc_VisBuf(d->visBuf); 2267 dealloc_VisBuf(d->visBuf);
2066 updateWindowTitle_DocumentWidget_(d); 2268 updateWindowTitle_DocumentWidget_(d);
@@ -2214,7 +2416,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2214 if (!isEmpty_Array(items)) { 2416 if (!isEmpty_Array(items)) {
2215 pushBack_Array(items, &(iMenuItem){ "---", 0, 0, 0 }); 2417 pushBack_Array(items, &(iMenuItem){ "---", 0, 0, 0 });
2216 } 2418 }
2217 pushBack_Array(items, &(iMenuItem){ "${dismiss}", 0, 0, "message.ok" }); 2419 pushBack_Array(items, &(iMenuItem){ "${close}", 0, 0, "message.ok" });
2218 iWidget *dlg = makeQuestion_Widget(uiHeading_ColorEscape "${heading.pageinfo}", 2420 iWidget *dlg = makeQuestion_Widget(uiHeading_ColorEscape "${heading.pageinfo}",
2219 cstr_String(msg), 2421 cstr_String(msg),
2220 data_Array(items), 2422 data_Array(items),
@@ -2334,6 +2536,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2334 if (category_GmStatusCode(status_GmRequest(d->request)) == categorySuccess_GmStatusCode) { 2536 if (category_GmStatusCode(status_GmRequest(d->request)) == categorySuccess_GmStatusCode) {
2335 init_Anim(&d->scrollY.pos, d->initNormScrollY * size_GmDocument(d->doc).y); /* TODO: unless user already scrolled! */ 2537 init_Anim(&d->scrollY.pos, d->initNormScrollY * size_GmDocument(d->doc).y); /* TODO: unless user already scrolled! */
2336 } 2538 }
2539 d->flags &= ~urlChanged_DocumentWidgetFlag;
2337 d->state = ready_RequestState; 2540 d->state = ready_RequestState;
2338 postProcessRequestContent_DocumentWidget_(d, iFalse); 2541 postProcessRequestContent_DocumentWidget_(d, iFalse);
2339 /* The response may be cached. */ 2542 /* The response may be cached. */
@@ -2409,7 +2612,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2409 const iMediaRequest *media = findMediaRequest_DocumentWidget_(d, linkId); 2612 const iMediaRequest *media = findMediaRequest_DocumentWidget_(d, linkId);
2410 if (media) { 2613 if (media) {
2411 saveToDownloads_(url_GmRequest(media->req), meta_GmRequest(media->req), 2614 saveToDownloads_(url_GmRequest(media->req), meta_GmRequest(media->req),
2412 body_GmRequest(media->req)); 2615 body_GmRequest(media->req), iTrue);
2413 } 2616 }
2414 } 2617 }
2415 else if (equal_Command(cmd, "document.save") && document_App() == d) { 2618 else if (equal_Command(cmd, "document.save") && document_App() == d) {
@@ -2418,7 +2621,13 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2418 "${dlg.save.incomplete}"); 2621 "${dlg.save.incomplete}");
2419 } 2622 }
2420 else if (!isEmpty_Block(&d->sourceContent)) { 2623 else if (!isEmpty_Block(&d->sourceContent)) {
2421 saveToDownloads_(d->mod.url, &d->sourceMime, &d->sourceContent); 2624 const iBool doOpen = argLabel_Command(cmd, "open");
2625 const iString *savePath = saveToDownloads_(d->mod.url, &d->sourceMime,
2626 &d->sourceContent, !doOpen);
2627 if (!isEmpty_String(savePath) && doOpen) {
2628 postCommandf_Root(
2629 w->root, "!open url:%s", cstrCollect_String(makeFileUrl_String(savePath)));
2630 }
2422 } 2631 }
2423 return iTrue; 2632 return iTrue;
2424 } 2633 }
@@ -2528,6 +2737,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2528 return iTrue; 2737 return iTrue;
2529 } 2738 }
2530 else if (equal_Command(cmd, "scroll.bottom") && document_App() == d) { 2739 else if (equal_Command(cmd, "scroll.bottom") && document_App() == d) {
2740 updateScrollMax_DocumentWidget_(d); /* scrollY.max might not be fully updated */
2531 init_Anim(&d->scrollY.pos, d->scrollY.max); 2741 init_Anim(&d->scrollY.pos, d->scrollY.max);
2532 invalidate_VisBuf(d->visBuf); 2742 invalidate_VisBuf(d->visBuf);
2533 clampScroll_DocumentWidget_(d); 2743 clampScroll_DocumentWidget_(d);
@@ -2835,7 +3045,7 @@ static size_t linkOrdinalFromKey_DocumentWidget_(const iDocumentWidget *d, int k
2835static iChar linkOrdinalChar_DocumentWidget_(const iDocumentWidget *d, size_t ord) { 3045static iChar linkOrdinalChar_DocumentWidget_(const iDocumentWidget *d, size_t ord) {
2836 if (d->ordinalMode == numbersAndAlphabet_DocumentLinkOrdinalMode) { 3046 if (d->ordinalMode == numbersAndAlphabet_DocumentLinkOrdinalMode) {
2837 if (ord < 9) { 3047 if (ord < 9) {
2838 return 0x278a + ord; 3048 return '1' + ord;
2839 } 3049 }
2840#if defined (iPlatformApple) 3050#if defined (iPlatformApple)
2841 if (ord < 9 + 22) { 3051 if (ord < 9 + 22) {
@@ -2844,17 +3054,17 @@ static iChar linkOrdinalChar_DocumentWidget_(const iDocumentWidget *d, size_t or
2844 if (key >= 'm') key++; 3054 if (key >= 'm') key++;
2845 if (key >= 'q') key++; 3055 if (key >= 'q') key++;
2846 if (key >= 'w') key++; 3056 if (key >= 'w') key++;
2847 return 0x24b6 + key - 'a'; 3057 return 'A' + key - 'a';
2848 } 3058 }
2849#else 3059#else
2850 if (ord < 9 + 26) { 3060 if (ord < 9 + 26) {
2851 return 0x24b6 + ord - 9; 3061 return 'A' + ord - 9;
2852 } 3062 }
2853#endif 3063#endif
2854 } 3064 }
2855 else { 3065 else {
2856 if (ord < iElemCount(homeRowKeys_)) { 3066 if (ord < iElemCount(homeRowKeys_)) {
2857 return 0x24b6 + homeRowKeys_[ord] - 'a'; 3067 return 'A' + homeRowKeys_[ord] - 'a';
2858 } 3068 }
2859 } 3069 }
2860 return 0; 3070 return 0;
@@ -3041,6 +3251,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3041 iArray items; 3251 iArray items;
3042 init_Array(&items, sizeof(iMenuItem)); 3252 init_Array(&items, sizeof(iMenuItem));
3043 if (d->contextLink) { 3253 if (d->contextLink) {
3254 /* Context menu for a link. */
3044 const iString *linkUrl = linkUrl_GmDocument(d->doc, d->contextLink->linkId); 3255 const iString *linkUrl = linkUrl_GmDocument(d->doc, d->contextLink->linkId);
3045// const int linkFlags = linkFlags_GmDocument(d->doc, d->contextLink->linkId); 3256// const int linkFlags = linkFlags_GmDocument(d->doc, d->contextLink->linkId);
3046 const iRangecc scheme = urlScheme_String(linkUrl); 3257 const iRangecc scheme = urlScheme_String(linkUrl);
@@ -3675,6 +3886,7 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
3675 /* Preformatted runs can be scrolled. */ 3886 /* Preformatted runs can be scrolled. */
3676 runOffset_DocumentWidget_(d->widget, run)); 3887 runOffset_DocumentWidget_(d->widget, run));
3677 const iRect visRect = { visPos, run->visBounds.size }; 3888 const iRect visRect = { visPos, run->visBounds.size };
3889#if 0
3678 if (run->flags & footer_GmRunFlag) { 3890 if (run->flags & footer_GmRunFlag) {
3679 iRect footerBack = 3891 iRect footerBack =
3680 (iRect){ visPos, init_I2(width_Rect(d->widgetBounds), run->visBounds.size.y) }; 3892 (iRect){ visPos, init_I2(width_Rect(d->widgetBounds), run->visBounds.size.y) };
@@ -3682,6 +3894,7 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
3682 fillRect_Paint(&d->paint, footerBack, tmBackground_ColorId); 3894 fillRect_Paint(&d->paint, footerBack, tmBackground_ColorId);
3683 return; 3895 return;
3684 } 3896 }
3897#endif
3685 /* Fill the background. */ { 3898 /* Fill the background. */ {
3686 if (run->linkId && linkFlags & isOpen_GmLinkFlag) { 3899 if (run->linkId && linkFlags & isOpen_GmLinkFlag) {
3687 /* Open links get a highlighted background. */ 3900 /* Open links get a highlighted background. */
@@ -3743,10 +3956,19 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
3743 const iChar ordChar = 3956 const iChar ordChar =
3744 linkOrdinalChar_DocumentWidget_(d->widget, ord - d->widget->ordinalBase); 3957 linkOrdinalChar_DocumentWidget_(d->widget, ord - d->widget->ordinalBase);
3745 if (ordChar) { 3958 if (ordChar) {
3746 drawString_Text(run->font, 3959 const char *circle = "\u25ef"; /* Large Circle */
3747 init_I2(d->viewPos.x - gap_UI / 3, visPos.y), 3960 iRect nbArea = { init_I2(d->viewPos.x - gap_UI / 3, visPos.y),
3961 init_I2(3.95f * gap_Text, 1.0f * lineHeight_Text(run->font)) };
3962 drawRange_Text(
3963 run->font, topLeft_Rect(nbArea), tmQuote_ColorId, range_CStr(circle));
3964 iRect circleArea = visualBounds_Text(run->font, range_CStr(circle));
3965 addv_I2(&circleArea.pos, topLeft_Rect(nbArea));
3966 drawCentered_Text(defaultContentSmall_FontId,
3967 circleArea,
3968 iTrue,
3748 tmQuote_ColorId, 3969 tmQuote_ColorId,
3749 collect_String(newUnicodeN_String(&ordChar, 1))); 3970 "%lc",
3971 (int) ordChar);
3750 goto runDrawn; 3972 goto runDrawn;
3751 } 3973 }
3752 } 3974 }
@@ -4424,9 +4646,16 @@ void deserializeState_DocumentWidget(iDocumentWidget *d, iStream *ins) {
4424 updateFromHistory_DocumentWidget_(d); 4646 updateFromHistory_DocumentWidget_(d);
4425} 4647}
4426 4648
4649static void setUrl_DocumentWidget_(iDocumentWidget *d, const iString *url) {
4650 if (!equal_String(d->mod.url, url)) {
4651 d->flags |= urlChanged_DocumentWidgetFlag;
4652 set_String(d->mod.url, url);
4653}
4654}
4655
4427void setUrlFromCache_DocumentWidget(iDocumentWidget *d, const iString *url, iBool isFromCache) { 4656void setUrlFromCache_DocumentWidget(iDocumentWidget *d, const iString *url, iBool isFromCache) {
4428 setLinkNumberMode_DocumentWidget_(d, iFalse); 4657 setLinkNumberMode_DocumentWidget_(d, iFalse);
4429 set_String(d->mod.url, urlFragmentStripped_String(url)); 4658 setUrl_DocumentWidget_(d, urlFragmentStripped_String(url));
4430 /* See if there a username in the URL. */ 4659 /* See if there a username in the URL. */
4431 parseUser_DocumentWidget_(d); 4660 parseUser_DocumentWidget_(d);
4432 if (!isFromCache || !updateFromHistory_DocumentWidget_(d)) { 4661 if (!isFromCache || !updateFromHistory_DocumentWidget_(d)) {
@@ -4434,6 +4663,20 @@ void setUrlFromCache_DocumentWidget(iDocumentWidget *d, const iString *url, iBoo
4434 } 4663 }
4435} 4664}
4436 4665
4666void setUrlAndSource_DocumentWidget(iDocumentWidget *d, const iString *url, const iString *mime,
4667 const iBlock *source) {
4668 setLinkNumberMode_DocumentWidget_(d, iFalse);
4669 setUrl_DocumentWidget_(d, url);
4670 parseUser_DocumentWidget_(d);
4671 iGmResponse *resp = new_GmResponse();
4672 resp->statusCode = success_GmStatusCode;
4673 initCurrent_Time(&resp->when);
4674 set_String(&resp->meta, mime);
4675 set_Block(&resp->body, source);
4676 updateFromCachedResponse_DocumentWidget_(d, 0, resp);
4677 delete_GmResponse(resp);
4678}
4679
4437iDocumentWidget *duplicate_DocumentWidget(const iDocumentWidget *orig) { 4680iDocumentWidget *duplicate_DocumentWidget(const iDocumentWidget *orig) {
4438 iDocumentWidget *d = new_DocumentWidget(); 4681 iDocumentWidget *d = new_DocumentWidget();
4439 delete_History(d->mod.history); 4682 delete_History(d->mod.history);
diff --git a/src/ui/documentwidget.h b/src/ui/documentwidget.h
index 12603437..c038f981 100644
--- a/src/ui/documentwidget.h
+++ b/src/ui/documentwidget.h
@@ -47,6 +47,7 @@ int documentWidth_DocumentWidget (const iDocumentWidget *);
47 47
48void setUrl_DocumentWidget (iDocumentWidget *, const iString *url); 48void setUrl_DocumentWidget (iDocumentWidget *, const iString *url);
49void setUrlFromCache_DocumentWidget (iDocumentWidget *, const iString *url, iBool isFromCache); 49void setUrlFromCache_DocumentWidget (iDocumentWidget *, const iString *url, iBool isFromCache);
50void setUrlAndSource_DocumentWidget (iDocumentWidget *, const iString *url, const iString *mime, const iBlock *source);
50void setInitialScroll_DocumentWidget (iDocumentWidget *, float normScrollY); /* set after content received */ 51void setInitialScroll_DocumentWidget (iDocumentWidget *, float normScrollY); /* set after content received */
51void setRedirectCount_DocumentWidget (iDocumentWidget *, int count); 52void setRedirectCount_DocumentWidget (iDocumentWidget *, int count);
52void setSource_DocumentWidget (iDocumentWidget *, const iString *sourceText); 53void setSource_DocumentWidget (iDocumentWidget *, const iString *sourceText);
diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c
index 0257eda0..cf128017 100644
--- a/src/ui/inputwidget.c
+++ b/src/ui/inputwidget.c
@@ -547,7 +547,9 @@ void setText_InputWidget(iInputWidget *d, const iString *text) {
547 } 547 }
548 clearUndo_InputWidget_(d); 548 clearUndo_InputWidget_(d);
549 clear_Array(&d->text); 549 clear_Array(&d->text);
550 iConstForEach(String, i, text) { 550 iString *nfcText = collect_String(copy_String(text));
551 normalize_String(nfcText);
552 iConstForEach(String, i, nfcText) {
551 pushBack_Array(&d->text, &i.value); 553 pushBack_Array(&d->text, &i.value);
552 } 554 }
553 if (isFocused_Widget(d)) { 555 if (isFocused_Widget(d)) {
diff --git a/src/ui/inputwidget.h b/src/ui/inputwidget.h
index 70553488..f8c5bf1e 100644
--- a/src/ui/inputwidget.h
+++ b/src/ui/inputwidget.h
@@ -65,6 +65,10 @@ void setEatEscape_InputWidget (iInputWidget *, iBool eatEscape);
65iInputWidgetContentPadding contentPadding_InputWidget (const iInputWidget *); 65iInputWidgetContentPadding contentPadding_InputWidget (const iInputWidget *);
66const iString * text_InputWidget (const iInputWidget *); 66const iString * text_InputWidget (const iInputWidget *);
67 67
68iLocalDef const char *cstrText_InputWidget(const iInputWidget *d) {
69 return cstr_String(text_InputWidget(d));
70}
71
68iLocalDef iInputWidget *newHint_InputWidget(size_t maxLen, const char *hint) { 72iLocalDef iInputWidget *newHint_InputWidget(size_t maxLen, const char *hint) {
69 iInputWidget *d = new_InputWidget(maxLen); 73 iInputWidget *d = new_InputWidget(maxLen);
70 setHint_InputWidget(d, hint); 74 setHint_InputWidget(d, hint);
diff --git a/src/ui/keys.c b/src/ui/keys.c
index 42d0d613..9df505e0 100644
--- a/src/ui/keys.c
+++ b/src/ui/keys.c
@@ -188,6 +188,7 @@ static void clear_Keys_(iKeys *d) {
188enum iBindFlag { 188enum iBindFlag {
189 argRepeat_BindFlag = iBit(1), 189 argRepeat_BindFlag = iBit(1),
190 argRelease_BindFlag = iBit(2), 190 argRelease_BindFlag = iBit(2),
191 noDirectTrigger_BindFlag = iBit(3), /* can only be triggered via LabelWidget */
191}; 192};
192 193
193/* TODO: This indirection could be used for localization, although all UI strings 194/* TODO: This indirection could be used for localization, although all UI strings
@@ -227,7 +228,16 @@ static const struct { int id; iMenuItem bind; int flags; } defaultBindings_[] =
227 { 81, { "${keys.tab.next}", nextTab_KeyShortcut, "tabs.next" }, 0 }, 228 { 81, { "${keys.tab.next}", nextTab_KeyShortcut, "tabs.next" }, 0 },
228 { 90, { "${keys.split.menu}", SDLK_j, KMOD_PRIMARY, "splitmenu.open" }, 0 }, 229 { 90, { "${keys.split.menu}", SDLK_j, KMOD_PRIMARY, "splitmenu.open" }, 0 },
229 { 91, { "${keys.split.next}", SDLK_TAB, KMOD_CTRL, "keyroot.next", }, 0 }, 230 { 91, { "${keys.split.next}", SDLK_TAB, KMOD_CTRL, "keyroot.next", }, 0 },
231 { 92, { "${keys.split.item} ${menu.split.merge}", '1', 0, "ui.split arg:0", }, noDirectTrigger_BindFlag },
232 { 93, { "${keys.split.item} ${menu.split.swap}", SDLK_x, 0, "ui.split swap:1", }, noDirectTrigger_BindFlag },
233 { 94, { "${keys.split.item} ${menu.split.horizontal}", '3', 0, "ui.split arg:3 axis:0", }, noDirectTrigger_BindFlag },
234 { 95, { "${keys.split.item} ${menu.split.horizontal} 1:2", SDLK_d, 0, "ui.split arg:1 axis:0", }, noDirectTrigger_BindFlag },
235 { 96, { "${keys.split.item} ${menu.split.horizontal} 2:1", SDLK_e, 0, "ui.split arg:2 axis:0", }, noDirectTrigger_BindFlag },
236 { 97, { "${keys.split.item} ${menu.split.vertical}", '2', 0, "ui.split arg:3 axis:1", }, noDirectTrigger_BindFlag },
237 { 98, { "${keys.split.item} ${menu.split.vertical} 1:2", SDLK_f, 0, "ui.split arg:1 axis:1", }, noDirectTrigger_BindFlag },
238 { 99, { "${keys.split.item} ${menu.split.vertical} 2:1", SDLK_r, 0, "ui.split arg:2 axis:1", }, noDirectTrigger_BindFlag },
230 { 100,{ "${keys.hoverurl}", '/', KMOD_PRIMARY, "prefs.hoverlink.toggle" }, 0 }, 239 { 100,{ "${keys.hoverurl}", '/', KMOD_PRIMARY, "prefs.hoverlink.toggle" }, 0 },
240 { 110,{ "${menu.save.downloads}", SDLK_s, KMOD_PRIMARY, "document.save" }, 0 },
231 /* The following cannot currently be changed (built-in duplicates). */ 241 /* The following cannot currently be changed (built-in duplicates). */
232#if defined (iPlatformApple) 242#if defined (iPlatformApple)
233 { 1002, { NULL, SDLK_LEFTBRACKET, KMOD_PRIMARY, "navigate.back" }, 0 }, 243 { 1002, { NULL, SDLK_LEFTBRACKET, KMOD_PRIMARY, "navigate.back" }, 0 },
@@ -301,7 +311,10 @@ static iBinding *findCommand_Keys_(iKeys *d, const char *command) {
301static void updateLookup_Keys_(iKeys *d) { 311static void updateLookup_Keys_(iKeys *d) {
302 clear_PtrSet(&d->lookup); 312 clear_PtrSet(&d->lookup);
303 iConstForEach(Array, i, &d->bindings) { 313 iConstForEach(Array, i, &d->bindings) {
304 insert_PtrSet(&d->lookup, i.value); 314 const iBinding *bind = i.value;
315 if (~bind->flags & noDirectTrigger_BindFlag) {
316 insert_PtrSet(&d->lookup, i.value);
317 }
305 } 318 }
306} 319}
307 320
diff --git a/src/ui/keys.h b/src/ui/keys.h
index 4cbca3b7..6273027a 100644
--- a/src/ui/keys.h
+++ b/src/ui/keys.h
@@ -26,6 +26,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
26#include <the_Foundation/ptrarray.h> 26#include <the_Foundation/ptrarray.h>
27#include <SDL_events.h> 27#include <SDL_events.h>
28 28
29#define newIdentity_KeyShortcut SDLK_n, KMOD_PRIMARY | KMOD_SHIFT
30
29#if defined (iPlatformApple) 31#if defined (iPlatformApple)
30# define reload_KeyShortcut SDLK_r, KMOD_PRIMARY 32# define reload_KeyShortcut SDLK_r, KMOD_PRIMARY
31# define newTab_KeyShortcut SDLK_t, KMOD_PRIMARY 33# define newTab_KeyShortcut SDLK_t, KMOD_PRIMARY
diff --git a/src/ui/listwidget.c b/src/ui/listwidget.c
index a3406d48..f7c43a93 100644
--- a/src/ui/listwidget.c
+++ b/src/ui/listwidget.c
@@ -148,11 +148,16 @@ void updateVisible_ListWidget(iListWidget *d) {
148 const int contentSize = size_PtrArray(&d->items) * d->itemHeight; 148 const int contentSize = size_PtrArray(&d->items) * d->itemHeight;
149 const iRect bounds = innerBounds_Widget(as_Widget(d)); 149 const iRect bounds = innerBounds_Widget(as_Widget(d));
150 const iBool wasVisible = isVisible_Widget(d->scroll); 150 const iBool wasVisible = isVisible_Widget(d->scroll);
151 if (area_Rect(bounds) == 0) { 151 if (width_Rect(bounds) <= 0 || height_Rect(bounds) <= 0) {
152 return; 152 return;
153 } 153 }
154 /* The scroll widget's visibility depends on it having a valid non-zero size.
155 However, this may be called during arrangement (sizeChanged_ListWidget_),
156 which means the child hasn't been arranged yet. The child cannot update
157 its visibility unless it knows its correct size. */
158 arrange_Widget(as_Widget(d->scroll));
154 setMax_SmoothScroll(&d->scrollY, scrollMax_ListWidget_(d)); 159 setMax_SmoothScroll(&d->scrollY, scrollMax_ListWidget_(d));
155 setRange_ScrollWidget(d->scroll, (iRangei){ 0, d->scrollY.max }); 160 setRange_ScrollWidget(d->scroll, (iRangei){ 0, d->scrollY.max });
156 setThumb_ScrollWidget(d->scroll, 161 setThumb_ScrollWidget(d->scroll,
157 pos_SmoothScroll(&d->scrollY), 162 pos_SmoothScroll(&d->scrollY),
158 contentSize > 0 ? height_Rect(bounds_Widget(as_Widget(d->scroll))) * 163 contentSize > 0 ? height_Rect(bounds_Widget(as_Widget(d->scroll))) *
@@ -369,6 +374,13 @@ static iBool processEvent_ListWidget_(iListWidget *d, const SDL_Event *ev) {
369 return processEvent_Widget(w, ev); 374 return processEvent_Widget(w, ev);
370} 375}
371 376
377iRect itemRect_ListWidget(const iListWidget *d, size_t index) {
378 const iRect bounds = innerBounds_Widget(constAs_Widget(d));
379 const int scrollY = pos_SmoothScroll(&d->scrollY);
380 return (iRect){ addY_I2(topLeft_Rect(bounds), d->itemHeight * (int) index - scrollY),
381 init_I2(width_Rect(bounds), d->itemHeight) };
382}
383
372static void draw_ListWidget_(const iListWidget *d) { 384static void draw_ListWidget_(const iListWidget *d) {
373 const iWidget *w = constAs_Widget(d); 385 const iWidget *w = constAs_Widget(d);
374 const iRect bounds = innerBounds_Widget(w); 386 const iRect bounds = innerBounds_Widget(w);
diff --git a/src/ui/listwidget.h b/src/ui/listwidget.h
index 16adf664..314c183a 100644
--- a/src/ui/listwidget.h
+++ b/src/ui/listwidget.h
@@ -73,6 +73,7 @@ iAnyObject * hoverItem_ListWidget (iListWidget *);
73size_t numItems_ListWidget (const iListWidget *); 73size_t numItems_ListWidget (const iListWidget *);
74int visCount_ListWidget (const iListWidget *); 74int visCount_ListWidget (const iListWidget *);
75size_t itemIndex_ListWidget (const iListWidget *, iInt2 pos); 75size_t itemIndex_ListWidget (const iListWidget *, iInt2 pos);
76iRect itemRect_ListWidget (const iListWidget *, size_t index);
76const iAnyObject * constItem_ListWidget (const iListWidget *, size_t index); 77const iAnyObject * constItem_ListWidget (const iListWidget *, size_t index);
77const iAnyObject * constHoverItem_ListWidget (const iListWidget *); 78const iAnyObject * constHoverItem_ListWidget (const iListWidget *);
78size_t hoverItemIndex_ListWidget (const iListWidget *); 79size_t hoverItemIndex_ListWidget (const iListWidget *);
diff --git a/src/ui/lookupwidget.c b/src/ui/lookupwidget.c
index 3eafd4bd..254aad93 100644
--- a/src/ui/lookupwidget.c
+++ b/src/ui/lookupwidget.c
@@ -127,10 +127,11 @@ static void draw_LookupItem_(iLookupItem *d, iPaint *p, iRect rect, const iListW
127 pos.y = bottom_Rect(rect) - lineHeight_Text(d->font); 127 pos.y = bottom_Rect(rect) - lineHeight_Text(d->font);
128 } 128 }
129 if (!isEmpty_String(&d->icon)) { 129 if (!isEmpty_String(&d->icon)) {
130 const iRect iconRect = { pos, init_I2(gap_UI * 5, height_Rect(rect)) }; 130 const iRect iconRect = { init_I2(pos.x, top_Rect(rect)),
131 const iInt2 iconSize = measureRange_Text(d->font, range_String(&d->icon)); 131 init_I2(gap_UI * 5, height_Rect(rect)) };
132 const iRect iconVis = visualBounds_Text(d->font, range_String(&d->icon));
132 drawRange_Text(d->font, 133 drawRange_Text(d->font,
133 addX_I2(pos, width_Rect(iconRect) / 2 - iconSize.x / 2), 134 sub_I2(mid_Rect(iconRect), mid_Rect(iconVis)),
134 fg, 135 fg,
135 range_String(&d->icon)); 136 range_String(&d->icon));
136 pos.x += width_Rect(iconRect) + gap_UI * 3 / 2; 137 pos.x += width_Rect(iconRect) + gap_UI * 3 / 2;
@@ -301,7 +302,7 @@ static void searchIdentities_LookupJob_(iLookupJob *d) {
301 iLookupResult *res = new_LookupResult(); 302 iLookupResult *res = new_LookupResult();
302 res->type = identity_LookupResultType; 303 res->type = identity_LookupResultType;
303 res->relevance = identityRelevance_LookupJob_(d, identity); 304 res->relevance = identityRelevance_LookupJob_(d, identity);
304 res->icon = identity->icon; 305 res->icon = 0x1f464; /* identity->icon; */
305 iString *cn = subject_TlsCertificate(identity->cert); 306 iString *cn = subject_TlsCertificate(identity->cert);
306 set_String(&res->label, cn); 307 set_String(&res->label, cn);
307 delete_String(cn); 308 delete_String(cn);
@@ -701,6 +702,12 @@ static iBool processEvent_LookupWidget_(iLookupWidget *d, const SDL_Event *ev) {
701 } 702 }
702 return iTrue; 703 return iTrue;
703 } 704 }
705 if (ev->type == SDL_MOUSEMOTION) {
706 if (contains_Widget(w, init_I2(ev->motion.x, ev->motion.y))) {
707 setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_HAND);
708 }
709 return iFalse;
710 }
704 if (ev->type == SDL_KEYDOWN) { 711 if (ev->type == SDL_KEYDOWN) {
705 const int mods = keyMods_Sym(ev->key.keysym.mod); 712 const int mods = keyMods_Sym(ev->key.keysym.mod);
706 const int key = ev->key.keysym.sym; 713 const int key = ev->key.keysym.sym;
diff --git a/src/ui/mediaui.c b/src/ui/mediaui.c
index 24b29cb0..bc417fc3 100644
--- a/src/ui/mediaui.c
+++ b/src/ui/mediaui.c
@@ -86,7 +86,7 @@ static int drawSevenSegmentTime_(iInt2 pos, int color, int align, int seconds) {
86 const int hours = seconds / 3600; 86 const int hours = seconds / 3600;
87 const int mins = (seconds / 60) % 60; 87 const int mins = (seconds / 60) % 60;
88 const int secs = seconds % 60; 88 const int secs = seconds % 60;
89 const int font = uiLabel_FontId; 89 const int font = defaultBig_FontId;
90 iString num; 90 iString num;
91 init_String(&num); 91 init_String(&num);
92 if (hours) { 92 if (hours) {
@@ -102,7 +102,7 @@ static int drawSevenSegmentTime_(iInt2 pos, int color, int align, int seconds) {
102 if (align == right_Alignment) { 102 if (align == right_Alignment) {
103 pos.x -= size.x; 103 pos.x -= size.x;
104 } 104 }
105 drawRange_Text(font, pos, color, range_String(&num)); 105 drawRange_Text(font, addY_I2(pos, -gap_UI / 8), color, range_String(&num));
106 deinit_String(&num); 106 deinit_String(&num);
107 return size.x; 107 return size.x;
108} 108}
@@ -123,7 +123,7 @@ void draw_PlayerUI(iPlayerUI *d, iPaint *p) {
123 drawPlayerButton_( 123 drawPlayerButton_(
124 p, d->volumeRect, volumeChar_(volume_Player(d->player)), uiContentSymbols_FontId); 124 p, d->volumeRect, volumeChar_(volume_Player(d->player)), uiContentSymbols_FontId);
125 } 125 }
126 const int hgt = lineHeight_Text(uiLabel_FontId); 126 const int hgt = lineHeight_Text(defaultBig_FontId);
127 const int yMid = mid_Rect(d->scrubberRect).y; 127 const int yMid = mid_Rect(d->scrubberRect).y;
128 const float playTime = time_Player(d->player); 128 const float playTime = time_Player(d->player);
129 const float totalTime = duration_Player(d->player); 129 const float totalTime = duration_Player(d->player);
@@ -153,7 +153,8 @@ void draw_PlayerUI(iPlayerUI *d, iPaint *p) {
153 const char *dot = "\u23fa"; 153 const char *dot = "\u23fa";
154 const int dotWidth = advance_Text(uiLabel_FontId, dot).x; 154 const int dotWidth = advance_Text(uiLabel_FontId, dot).x;
155 draw_Text(uiLabel_FontId, 155 draw_Text(uiLabel_FontId,
156 init_I2(s1 * (1.0f - normPos) + s2 * normPos - dotWidth / 2, yMid - hgt / 2), 156 init_I2(s1 * (1.0f - normPos) + s2 * normPos - dotWidth / 2,
157 yMid - lineHeight_Text(uiLabel_FontId) / 2),
157 isPaused_Player(d->player) ? dim : bright, 158 isPaused_Player(d->player) ? dim : bright,
158 dot); 159 dot);
159 /* Volume adjustment. */ 160 /* Volume adjustment. */
@@ -186,7 +187,8 @@ void draw_PlayerUI(iPlayerUI *d, iPaint *p) {
186 width_Rect(d->volumeSlider) - volPart, 187 width_Rect(d->volumeSlider) - volPart,
187 dim); 188 dim);
188 draw_Text(uiLabel_FontId, 189 draw_Text(uiLabel_FontId,
189 init_I2(left_Rect(d->volumeSlider) + volPart - dotWidth / 2, yMid - hgt / 2), 190 init_I2(left_Rect(d->volumeSlider) + volPart - dotWidth / 2,
191 yMid - lineHeight_Text(uiLabel_FontId) / 2),
190 volColor, 192 volColor,
191 dot); 193 dot);
192 } 194 }
diff --git a/src/ui/mobile.c b/src/ui/mobile.c
index 6c3a0b32..263fc141 100644
--- a/src/ui/mobile.c
+++ b/src/ui/mobile.c
@@ -381,22 +381,22 @@ void finalizeSheet_Mobile(iWidget *sheet) {
381 } 381 }
382 /* TODO: In portrait, top panel and detail stack are all stacked together. 382 /* TODO: In portrait, top panel and detail stack are all stacked together.
383 383
384 Landscape Layout    Portrait Layout 384 Landscape Layout Portrait Layout
385    385
386 ┌─────────┬──────Detail─Stack─────┐    ┌─────────┬ ─ ─ ─ ─ ┐ 386 ┌─────────┬──────Detail─Stack─────┐ ┌─────────┬ ─ ─ ─ ─ ┐
387 │ │┌───────────────────┐ │    │ │Detail 387 │ │┌───────────────────┐ │ │ │Detail
388 │ ││┌──────────────────┴┐ │    │ │Stack │ 388 │ ││┌──────────────────┴┐ │ │ │Stack │
389 │ │││┌──────────────────┴┐│    │ │┌──────┐ 389 │ │││┌──────────────────┴┐│ │ │┌──────┐
390 │ ││││ ││    │ ││┌─────┴┐│ 390 │ ││││ ││ │ ││┌─────┴┐│
391 │ ││││ ││    │ │││ │ 391 │ ││││ ││ │ │││ │
392 │Top Panel││││ ││    │Top Panel│││ ││ 392 │Top Panel││││ ││ │Top Panel│││ ││
393 │ ││││ Panels ││    │ │││Panels│ 393 │ ││││ Panels ││ │ │││Panels│
394 │ ││││ ││    │ │││ ││ 394 │ ││││ ││ │ │││ ││
395 │ │└┤│ ││    │ │││ │ 395 │ │└┤│ ││ │ │││ │
396 │ │ └┤ ││    │ │└┤ ││ 396 │ │ └┤ ││ │ │└┤ ││
397 │ │ └───────────────────┘│    │ │ └──────┘ 397 │ │ └───────────────────┘│ │ │ └──────┘
398 └─────────┴───────────────────────┘    └─────────┴ ─ ─ ─ ─ ┘ 398 └─────────┴───────────────────────┘ └─────────┴ ─ ─ ─ ─ ┘
399    offscreen 399 offscreen
400 */ 400 */
401 /* Modify the top sheet to act as a fullscreen background. */ 401 /* Modify the top sheet to act as a fullscreen background. */
402 setPadding1_Widget(sheet, 0); 402 setPadding1_Widget(sheet, 0);
@@ -759,6 +759,9 @@ void finalizeSheet_Mobile(iWidget *sheet) {
759 else { 759 else {
760 arrange_Widget(sheet); 760 arrange_Widget(sheet);
761 } 761 }
762 if (!useMobileSheetLayout_()) {
763 setupSheetTransition_Mobile(sheet, iTrue);
764 }
762 postRefresh_App(); 765 postRefresh_App();
763} 766}
764 767
@@ -784,16 +787,28 @@ void setupMenuTransition_Mobile(iWidget *sheet, iBool isIncoming) {
784} 787}
785 788
786void setupSheetTransition_Mobile(iWidget *sheet, iBool isIncoming) { 789void setupSheetTransition_Mobile(iWidget *sheet, iBool isIncoming) {
787 if (isSideBySideLayout_()) { 790 if (!useMobileSheetLayout_()) {
791 if (prefs_App()->uiAnimations) {
792 setFlags_Widget(sheet, horizontalOffset_WidgetFlag, iFalse);
793 if (isIncoming) {
794 setVisualOffset_Widget(sheet, -height_Widget(sheet), 0, 0);
795 setVisualOffset_Widget(sheet, 0, 200, easeOut_AnimFlag | softer_AnimFlag);
796 }
797 else {
798 setVisualOffset_Widget(sheet, -height_Widget(sheet), 200, easeIn_AnimFlag);
799 }
800 }
801 return;
802 }
803 if(isSideBySideLayout_()) {
788 return; 804 return;
789 } 805 }
806 setFlags_Widget(sheet, horizontalOffset_WidgetFlag, iTrue);
790 if (isIncoming) { 807 if (isIncoming) {
791 setFlags_Widget(sheet, horizontalOffset_WidgetFlag, iTrue);
792 setVisualOffset_Widget(sheet, size_Root(sheet->root).x, 0, 0); 808 setVisualOffset_Widget(sheet, size_Root(sheet->root).x, 0, 0);
793 setVisualOffset_Widget(sheet, 0, 200, easeOut_AnimFlag); 809 setVisualOffset_Widget(sheet, 0, 200, easeOut_AnimFlag);
794 } 810 }
795 else { 811 else {
796 setFlags_Widget(sheet, horizontalOffset_WidgetFlag, iTrue);
797 const iBool wasDragged = iAbs(value_Anim(&sheet->visualOffset)) > 0; 812 const iBool wasDragged = iAbs(value_Anim(&sheet->visualOffset)) > 0;
798 setVisualOffset_Widget(sheet, size_Root(sheet->root).x, wasDragged ? 100 : 200, 813 setVisualOffset_Widget(sheet, size_Root(sheet->root).x, wasDragged ? 100 : 200,
799 wasDragged ? 0 : easeIn_AnimFlag); 814 wasDragged ? 0 : easeIn_AnimFlag);
diff --git a/src/ui/root.c b/src/ui/root.c
index 9ed62711..15548e74 100644
--- a/src/ui/root.c
+++ b/src/ui/root.c
@@ -128,7 +128,7 @@ static const iMenuItem phoneNavMenuItems_[] = {
128static const iMenuItem identityButtonMenuItems_[] = { 128static const iMenuItem identityButtonMenuItems_[] = {
129 { "${menu.identity.notactive}", 0, 0, "ident.showactive" }, 129 { "${menu.identity.notactive}", 0, 0, "ident.showactive" },
130 { "---", 0, 0, NULL }, 130 { "---", 0, 0, NULL },
131 { add_Icon " ${menu.identity.new}", SDLK_n, KMOD_PRIMARY | KMOD_SHIFT, "ident.new" }, 131 { add_Icon " ${menu.identity.new}", newIdentity_KeyShortcut, "ident.new" },
132 { "${menu.identity.import}", SDLK_i, KMOD_PRIMARY | KMOD_SHIFT, "ident.import" }, 132 { "${menu.identity.import}", SDLK_i, KMOD_PRIMARY | KMOD_SHIFT, "ident.import" },
133 { "---", 0, 0, NULL }, 133 { "---", 0, 0, NULL },
134 { person_Icon " ${menu.show.identities}", 0, 0, "toolbar.showident" }, 134 { person_Icon " ${menu.show.identities}", 0, 0, "toolbar.showident" },
@@ -138,7 +138,7 @@ static const iMenuItem identityButtonMenuItems_[] = {
138 { "${menu.identity.notactive}", 0, 0, "ident.showactive" }, 138 { "${menu.identity.notactive}", 0, 0, "ident.showactive" },
139 { "---", 0, 0, NULL }, 139 { "---", 0, 0, NULL },
140# if !defined (iPlatformAppleDesktop) 140# if !defined (iPlatformAppleDesktop)
141 { add_Icon " ${menu.identity.new}", SDLK_n, KMOD_PRIMARY | KMOD_SHIFT, "ident.new" }, 141 { add_Icon " ${menu.identity.new}", newIdentity_KeyShortcut, "ident.new" },
142 { "${menu.identity.import}", SDLK_i, KMOD_PRIMARY | KMOD_SHIFT, "ident.import" }, 142 { "${menu.identity.import}", SDLK_i, KMOD_PRIMARY | KMOD_SHIFT, "ident.import" },
143 { "---", 0, 0, NULL }, 143 { "---", 0, 0, NULL },
144 { person_Icon " ${menu.show.identities}", '4', KMOD_PRIMARY, "sidebar.mode arg:3 show:1" }, 144 { person_Icon " ${menu.show.identities}", '4', KMOD_PRIMARY, "sidebar.mode arg:3 show:1" },
@@ -156,10 +156,10 @@ static const char *pageMenuCStr_ = midEllipsis_Icon;
156/* TODO: A preference for these, maybe? */ 156/* TODO: A preference for these, maybe? */
157static const char *stopSeqCStr_[] = { 157static const char *stopSeqCStr_[] = {
158 /* Corners */ 158 /* Corners */
159 uiTextCaution_ColorEscape "\U0000230c", 159 uiTextCaution_ColorEscape "\U0000231c",
160 uiTextCaution_ColorEscape "\U0000230d", 160 uiTextCaution_ColorEscape "\U0000231d",
161 uiTextCaution_ColorEscape "\U0000230f", 161 uiTextCaution_ColorEscape "\U0000231f",
162 uiTextCaution_ColorEscape "\U0000230e", 162 uiTextCaution_ColorEscape "\U0000231e",
163#if 0 163#if 0
164 /* Rotating arrow */ 164 /* Rotating arrow */
165 uiTextCaution_ColorEscape "\U00002b62", 165 uiTextCaution_ColorEscape "\U00002b62",
@@ -276,6 +276,9 @@ void destroyPending_Root(iRoot *d) {
276 if (!isFinished_Anim(&widget->visualOffset)) { 276 if (!isFinished_Anim(&widget->visualOffset)) {
277 continue; 277 continue;
278 } 278 }
279 if (widget->flags & keepOnTop_WidgetFlag) {
280 removeOne_PtrArray(onTop_Root(widget->root), widget);
281 }
279 if (widget->parent) { 282 if (widget->parent) {
280 removeChild_Widget(widget->parent, widget); 283 removeChild_Widget(widget->parent, widget);
281 } 284 }
@@ -435,11 +438,11 @@ static void updateNavBarIdentity_(iWidget *navBar) {
435 setFlags_Widget(tool, selected_WidgetFlag, ident != NULL); 438 setFlags_Widget(tool, selected_WidgetFlag, ident != NULL);
436 /* Update menu. */ 439 /* Update menu. */
437 iLabelWidget *idItem = child_Widget(findChild_Widget(button, "menu"), 0); 440 iLabelWidget *idItem = child_Widget(findChild_Widget(button, "menu"), 0);
441 const iString *subjectName = ident ? name_GmIdentity(ident) : NULL;
438 setTextCStr_LabelWidget( 442 setTextCStr_LabelWidget(
439 idItem, 443 idItem,
440 ident ? format_CStr(uiTextAction_ColorEscape "%s", 444 subjectName ? format_CStr(uiTextAction_ColorEscape "%s", cstr_String(subjectName))
441 cstrCollect_String(subject_TlsCertificate(ident->cert))) 445 : "${menu.identity.notactive}");
442 : "${menu.identity.notactive}");
443 setFlags_Widget(as_Widget(idItem), disabled_WidgetFlag, !ident); 446 setFlags_Widget(as_Widget(idItem), disabled_WidgetFlag, !ident);
444} 447}
445 448
@@ -1046,7 +1049,7 @@ void createUserInterface_Root(iRoot *d) {
1046 moveToParentRightEdge_WidgetFlag); 1049 moveToParentRightEdge_WidgetFlag);
1047 /* Feeds refresh indicator is inside the input field. */ { 1050 /* Feeds refresh indicator is inside the input field. */ {
1048 iLabelWidget *queryInd = 1051 iLabelWidget *queryInd =
1049 new_LabelWidget(uiTextAction_ColorEscape "${status.query} \u21a9", NULL); 1052 new_LabelWidget(uiTextAction_ColorEscape "${status.query} " return_Icon, NULL);
1050 setId_Widget(as_Widget(queryInd), "input.indicator.search"); 1053 setId_Widget(as_Widget(queryInd), "input.indicator.search");
1051 setBackgroundColor_Widget(as_Widget(queryInd), uiBackground_ColorId); 1054 setBackgroundColor_Widget(as_Widget(queryInd), uiBackground_ColorId);
1052 setFrameColor_Widget(as_Widget(queryInd), uiTextAction_ColorId); 1055 setFrameColor_Widget(as_Widget(queryInd), uiTextAction_ColorId);
@@ -1305,11 +1308,11 @@ void createUserInterface_Root(iRoot *d) {
1305 { "${menu.split.merge}", '1', 0, "ui.split arg:0" }, 1308 { "${menu.split.merge}", '1', 0, "ui.split arg:0" },
1306 { "${menu.split.swap}", SDLK_x, 0, "ui.split swap:1" }, 1309 { "${menu.split.swap}", SDLK_x, 0, "ui.split swap:1" },
1307 { "---", 0, 0, NULL }, 1310 { "---", 0, 0, NULL },
1308 { "${menu.split.horizontal}", '2', 0, "ui.split arg:3 axis:0" }, 1311 { "${menu.split.horizontal}", '3', 0, "ui.split arg:3 axis:0" },
1309 { "${menu.split.horizontal} 1:2", SDLK_d, 0, "ui.split arg:1 axis:0" }, 1312 { "${menu.split.horizontal} 1:2", SDLK_d, 0, "ui.split arg:1 axis:0" },
1310 { "${menu.split.horizontal} 2:1", SDLK_e, 0, "ui.split arg:2 axis:0" }, 1313 { "${menu.split.horizontal} 2:1", SDLK_e, 0, "ui.split arg:2 axis:0" },
1311 { "---", 0, 0, NULL }, 1314 { "---", 0, 0, NULL },
1312 { "${menu.split.vertical}", '3', 0, "ui.split arg:3 axis:1" }, 1315 { "${menu.split.vertical}", '2', 0, "ui.split arg:3 axis:1" },
1313 { "${menu.split.vertical} 1:2", SDLK_f, 0, "ui.split arg:1 axis:1" }, 1316 { "${menu.split.vertical} 1:2", SDLK_f, 0, "ui.split arg:1 axis:1" },
1314 { "${menu.split.vertical} 2:1", SDLK_r, 0, "ui.split arg:2 axis:1" }, 1317 { "${menu.split.vertical} 2:1", SDLK_r, 0, "ui.split arg:2 axis:1" },
1315 }, 10); 1318 }, 10);
diff --git a/src/ui/scrollwidget.c b/src/ui/scrollwidget.c
index ff5144b2..0bab601a 100644
--- a/src/ui/scrollwidget.c
+++ b/src/ui/scrollwidget.c
@@ -54,7 +54,8 @@ struct Impl_ScrollWidget {
54}; 54};
55 55
56static void updateMetrics_ScrollWidget_(iScrollWidget *d) { 56static void updateMetrics_ScrollWidget_(iScrollWidget *d) {
57 as_Widget(d)->rect.size.x = gap_UI * 3; 57 iWidget *w = as_Widget(d);
58 w->rect.size.x = gap_UI * 3;
58} 59}
59 60
60static void animateOpacity_ScrollWidget_(void *ptr) { 61static void animateOpacity_ScrollWidget_(void *ptr) {
diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c
index 86410d11..27646b22 100644
--- a/src/ui/sidebarwidget.c
+++ b/src/ui/sidebarwidget.c
@@ -148,6 +148,65 @@ static iLabelWidget *addActionButton_SidebarWidget_(iSidebarWidget *d, const cha
148 return btn; 148 return btn;
149} 149}
150 150
151static iGmIdentity *menuIdentity_SidebarWidget_(const iSidebarWidget *d) {
152 if (d->mode == identities_SidebarMode) {
153 if (d->contextItem) {
154 return identity_GmCerts(certs_App(), d->contextItem->id);
155 }
156 }
157 return NULL;
158}
159
160static void updateContextMenu_SidebarWidget_(iSidebarWidget *d) {
161 if (d->mode != identities_SidebarMode) {
162 return;
163 }
164 iArray *items = collectNew_Array(sizeof(iMenuItem));
165 pushBackN_Array(items, (iMenuItem[]){
166 { person_Icon " ${ident.use}", 0, 0, "ident.use arg:1" },
167 { close_Icon " ${ident.stopuse}", 0, 0, "ident.use arg:0" },
168 { close_Icon " ${ident.stopuse.all}", 0, 0, "ident.use arg:0 clear:1" },
169 { "---", 0, 0, NULL },
170 { edit_Icon " ${menu.edit.notes}", 0, 0, "ident.edit" },
171 { "${ident.fingerprint}", 0, 0, "ident.fingerprint" },
172 { export_Icon " ${ident.export}", 0, 0, "ident.export" },
173 { "---", 0, 0, NULL },
174 { delete_Icon " " uiTextCaution_ColorEscape "${ident.delete}", 0, 0, "ident.delete confirm:1" },
175 }, 9);
176 /* Used URLs. */
177 const iGmIdentity *ident = menuIdentity_SidebarWidget_(d);
178 if (ident) {
179 size_t insertPos = 3;
180 if (!isEmpty_StringSet(ident->useUrls)) {
181 insert_Array(items, insertPos++, &(iMenuItem){ "---", 0, 0, NULL });
182 }
183 const iString *docUrl = url_DocumentWidget(document_App());
184 iBool usedOnCurrentPage = iFalse;
185 iConstForEach(StringSet, i, ident->useUrls) {
186 const iString *url = i.value;
187 usedOnCurrentPage |= equalCase_String(docUrl, url);
188 iRangecc urlStr = range_String(url);
189 if (startsWith_Rangecc(urlStr, "gemini://")) {
190 urlStr.start += 9; /* omit the default scheme */
191 }
192 if (endsWith_Rangecc(urlStr, "/")) {
193 urlStr.end--; /* looks cleaner */
194 }
195 insert_Array(items,
196 insertPos++,
197 &(iMenuItem){ format_CStr(globe_Icon " %s", cstr_Rangecc(urlStr)),
198 0,
199 0,
200 format_CStr("!open url:%s", cstr_String(url)) });
201 }
202 if (!usedOnCurrentPage) {
203 remove_Array(items, 1);
204 }
205 }
206 destroy_Widget(d->menu);
207 d->menu = makeMenu_Widget(as_Widget(d), data_Array(items), size_Array(items));
208}
209
151static void updateItems_SidebarWidget_(iSidebarWidget *d) { 210static void updateItems_SidebarWidget_(iSidebarWidget *d) {
152 clear_ListWidget(d->list); 211 clear_ListWidget(d->list);
153 releaseChildren_Widget(d->blank); 212 releaseChildren_Widget(d->blank);
@@ -375,13 +434,14 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {
375 } 434 }
376 case identities_SidebarMode: { 435 case identities_SidebarMode: {
377 const iString *tabUrl = url_DocumentWidget(document_App()); 436 const iString *tabUrl = url_DocumentWidget(document_App());
437 const iRangecc tabHost = urlHost_String(tabUrl);
378 isEmpty = iTrue; 438 isEmpty = iTrue;
379 iConstForEach(PtrArray, i, identities_GmCerts(certs_App())) { 439 iConstForEach(PtrArray, i, identities_GmCerts(certs_App())) {
380 const iGmIdentity *ident = i.ptr; 440 const iGmIdentity *ident = i.ptr;
381 iSidebarItem *item = new_SidebarItem(); 441 iSidebarItem *item = new_SidebarItem();
382 item->id = (uint32_t) index_PtrArrayConstIterator(&i); 442 item->id = (uint32_t) index_PtrArrayConstIterator(&i);
383 item->icon = ident->icon; 443 item->icon = 0x1f464; /* person */
384 set_String(&item->label, collect_String(subject_TlsCertificate(ident->cert))); 444 set_String(&item->label, name_GmIdentity(ident));
385 iDate until; 445 iDate until;
386 validUntil_TlsCertificate(ident->cert, &until); 446 validUntil_TlsCertificate(ident->cert, &until);
387 const iBool isActive = isUsedOn_GmIdentity(ident, tabUrl); 447 const iBool isActive = isUsedOn_GmIdentity(ident, tabUrl);
@@ -406,6 +466,9 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {
406 cstr_String(&ident->notes)); 466 cstr_String(&ident->notes));
407 } 467 }
408 item->listItem.isSelected = isActive; 468 item->listItem.isSelected = isActive;
469 if (isUsedOnDomain_GmIdentity(ident, tabHost)) {
470 item->indent = 1; /* will be highlighted */
471 }
409 addItem_ListWidget(d->list, item); 472 addItem_ListWidget(d->list, item);
410 iRelease(item); 473 iRelease(item);
411 isEmpty = iFalse; 474 isEmpty = iFalse;
@@ -415,11 +478,11 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {
415 addActionButton_SidebarWidget_(d, add_Icon " ${sidebar.action.ident.new}", "ident.new", 0); 478 addActionButton_SidebarWidget_(d, add_Icon " ${sidebar.action.ident.new}", "ident.new", 0);
416 addActionButton_SidebarWidget_(d, "${sidebar.action.ident.import}", "ident.import", 0); 479 addActionButton_SidebarWidget_(d, "${sidebar.action.ident.import}", "ident.import", 0);
417 } 480 }
481 /*
418 const iMenuItem menuItems[] = { 482 const iMenuItem menuItems[] = {
419 { person_Icon " ${ident.use}", 0, 0, "ident.use arg:1" }, 483 { person_Icon " ${ident.use}", 0, 0, "ident.use arg:1" },
420 { close_Icon " ${ident.stopuse}", 0, 0, "ident.use arg:0" }, 484 { close_Icon " ${ident.stopuse}", 0, 0, "ident.use arg:0" },
421 { close_Icon " ${ident.stopuse.all}", 0, 0, "ident.use arg:0 clear:1" }, 485 { close_Icon " ${ident.stopuse.all}", 0, 0, "ident.use arg:0 clear:1" },
422 { "${ident.showuse}", 0, 0, "ident.showuse" },
423 { "---", 0, 0, NULL }, 486 { "---", 0, 0, NULL },
424 { edit_Icon " ${menu.edit.notes}", 0, 0, "ident.edit" }, 487 { edit_Icon " ${menu.edit.notes}", 0, 0, "ident.edit" },
425 { "${ident.fingerprint}", 0, 0, "ident.fingerprint" }, 488 { "${ident.fingerprint}", 0, 0, "ident.fingerprint" },
@@ -429,6 +492,7 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {
429 { delete_Icon " " uiTextCaution_ColorEscape "${ident.delete}", 0, 0, "ident.delete confirm:1" }, 492 { delete_Icon " " uiTextCaution_ColorEscape "${ident.delete}", 0, 0, "ident.delete confirm:1" },
430 }; 493 };
431 d->menu = makeMenu_Widget(as_Widget(d), menuItems, iElemCount(menuItems)); 494 d->menu = makeMenu_Widget(as_Widget(d), menuItems, iElemCount(menuItems));
495 */
432 break; 496 break;
433 } 497 }
434 default: 498 default:
@@ -697,20 +761,11 @@ static const iGmIdentity *constHoverIdentity_SidebarWidget_(const iSidebarWidget
697 return NULL; 761 return NULL;
698} 762}
699 763
700static iGmIdentity *menuIdentity_SidebarWidget_(const iSidebarWidget *d) {
701 if (d->mode == identities_SidebarMode) {
702 if (d->contextItem) {
703 return identity_GmCerts(certs_App(), d->contextItem->id);
704 }
705 }
706 return NULL;
707}
708
709static iGmIdentity *hoverIdentity_SidebarWidget_(const iSidebarWidget *d) { 764static iGmIdentity *hoverIdentity_SidebarWidget_(const iSidebarWidget *d) {
710 return iConstCast(iGmIdentity *, constHoverIdentity_SidebarWidget_(d)); 765 return iConstCast(iGmIdentity *, constHoverIdentity_SidebarWidget_(d));
711} 766}
712 767
713static void itemClicked_SidebarWidget_(iSidebarWidget *d, const iSidebarItem *item) { 768static void itemClicked_SidebarWidget_(iSidebarWidget *d, iSidebarItem *item, size_t itemIndex) {
714 setFocus_Widget(NULL); 769 setFocus_Widget(NULL);
715 switch (d->mode) { 770 switch (d->mode) {
716 case documentOutline_SidebarMode: { 771 case documentOutline_SidebarMode: {
@@ -735,17 +790,19 @@ static void itemClicked_SidebarWidget_(iSidebarWidget *d, const iSidebarItem *it
735 break; 790 break;
736 } 791 }
737 case identities_SidebarMode: { 792 case identities_SidebarMode: {
738 iGmIdentity *ident = hoverIdentity_SidebarWidget_(d); 793 d->contextItem = item;
739 if (ident) { 794 if (d->contextIndex != iInvalidPos) {
740 const iString *tabUrl = url_DocumentWidget(document_App()); 795 invalidateItem_ListWidget(d->list, d->contextIndex);
741 if (isUsedOn_GmIdentity(ident, tabUrl)) { 796 }
742 signOut_GmCerts(certs_App(), tabUrl); 797 d->contextIndex = itemIndex;
743 } 798 if (itemIndex < numItems_ListWidget(d->list)) {
744 else { 799 updateContextMenu_SidebarWidget_(d);
745 signIn_GmCerts(certs_App(), ident, tabUrl); 800 arrange_Widget(d->menu);
746 } 801 openMenu_Widget(d->menu,
747 updateItems_SidebarWidget_(d); 802 d->side == left_SideBarSide
748 updateMouseHover_ListWidget(d->list); 803 ? topRight_Rect(itemRect_ListWidget(d->list, itemIndex))
804 : addX_I2(topLeft_Rect(itemRect_ListWidget(d->list, itemIndex)),
805 -width_Widget(d->menu)));
749 } 806 }
750 break; 807 break;
751 } 808 }
@@ -848,6 +905,7 @@ iBool handleBookmarkEditorCommands_SidebarWidget_(iWidget *editor, const char *c
848 isSelected_Widget(findChild_Widget(editor, "bmed.tag.linksplit"))); 905 isSelected_Widget(findChild_Widget(editor, "bmed.tag.linksplit")));
849 postCommand_App("bookmarks.changed"); 906 postCommand_App("bookmarks.changed");
850 } 907 }
908 setupSheetTransition_Mobile(editor, iFalse);
851 destroy_Widget(editor); 909 destroy_Widget(editor);
852 return iTrue; 910 return iTrue;
853 } 911 }
@@ -878,7 +936,8 @@ static iBool handleSidebarCommand_SidebarWidget_(iSidebarWidget *d, const char *
878 if (arg_Command(cmd) && isVisible_Widget(w)) { 936 if (arg_Command(cmd) && isVisible_Widget(w)) {
879 return iTrue; 937 return iTrue;
880 } 938 }
881 const iBool isAnimated = argLabel_Command(cmd, "noanim") == 0 && 939 const iBool isAnimated = prefs_App()->uiAnimations &&
940 argLabel_Command(cmd, "noanim") == 0 &&
882 (deviceType_App() != phone_AppDeviceType); 941 (deviceType_App() != phone_AppDeviceType);
883 int visX = 0; 942 int visX = 0;
884 if (isVisible_Widget(w)) { 943 if (isVisible_Widget(w)) {
@@ -1008,7 +1067,8 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1008 return iTrue; 1067 return iTrue;
1009 } 1068 }
1010 else if (isCommand_Widget(w, ev, "list.clicked")) { 1069 else if (isCommand_Widget(w, ev, "list.clicked")) {
1011 itemClicked_SidebarWidget_(d, pointerLabel_Command(cmd, "item")); 1070 itemClicked_SidebarWidget_(
1071 d, pointerLabel_Command(cmd, "item"), argU32Label_Command(cmd, "arg"));
1012 return iTrue; 1072 return iTrue;
1013 } 1073 }
1014 else if (isCommand_Widget(w, ev, "menu.closed")) { 1074 else if (isCommand_Widget(w, ev, "menu.closed")) {
@@ -1190,23 +1250,17 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1190 } 1250 }
1191 else if (arg_Command(cmd)) { 1251 else if (arg_Command(cmd)) {
1192 signIn_GmCerts(certs_App(), ident, tabUrl); 1252 signIn_GmCerts(certs_App(), ident, tabUrl);
1253 postCommand_App("navigate.reload");
1193 } 1254 }
1194 else { 1255 else {
1195 signOut_GmCerts(certs_App(), tabUrl); 1256 signOut_GmCerts(certs_App(), tabUrl);
1257 postCommand_App("navigate.reload");
1196 } 1258 }
1197 saveIdentities_GmCerts(certs_App()); 1259 saveIdentities_GmCerts(certs_App());
1198 updateItems_SidebarWidget_(d); 1260 updateItems_SidebarWidget_(d);
1199 } 1261 }
1200 return iTrue; 1262 return iTrue;
1201 } 1263 }
1202 else if (isCommand_Widget(w, ev, "ident.showuse")) {
1203 const iGmIdentity *ident = menuIdentity_SidebarWidget_(d);
1204 if (ident) {
1205 makeSimpleMessage_Widget(uiHeading_ColorEscape "${heading.ident.use}",
1206 cstrCollect_String(joinCStr_StringSet(ident->useUrls, "\n")));
1207 }
1208 return iTrue;
1209 }
1210 else if (isCommand_Widget(w, ev, "ident.edit")) { 1264 else if (isCommand_Widget(w, ev, "ident.edit")) {
1211 const iGmIdentity *ident = menuIdentity_SidebarWidget_(d); 1265 const iGmIdentity *ident = menuIdentity_SidebarWidget_(d);
1212 if (ident) { 1266 if (ident) {
@@ -1228,6 +1282,20 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1228 } 1282 }
1229 return iTrue; 1283 return iTrue;
1230 } 1284 }
1285 else if (isCommand_Widget(w, ev, "ident.export")) {
1286 const iGmIdentity *ident = menuIdentity_SidebarWidget_(d);
1287 if (ident) {
1288 iString *pem = collect_String(pem_TlsCertificate(ident->cert));
1289 append_String(pem, collect_String(privateKeyPem_TlsCertificate(ident->cert)));
1290 iDocumentWidget *expTab = newTab_App(NULL, iTrue);
1291 setUrlAndSource_DocumentWidget(
1292 expTab,
1293 collectNewFormat_String("file:%s.pem", cstr_String(name_GmIdentity(ident))),
1294 collectNewCStr_String("text/plain"),
1295 utf8_String(pem));
1296 }
1297 return iTrue;
1298 }
1231 else if (isCommand_Widget(w, ev, "ident.setnotes")) { 1299 else if (isCommand_Widget(w, ev, "ident.setnotes")) {
1232 iGmIdentity *ident = pointerLabel_Command(cmd, "ident"); 1300 iGmIdentity *ident = pointerLabel_Command(cmd, "ident");
1233 if (ident) { 1301 if (ident) {
@@ -1336,20 +1404,21 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1336 d->contextIndex = iInvalidPos; 1404 d->contextIndex = iInvalidPos;
1337 } 1405 }
1338 } 1406 }
1339 if (d->menu && ev->type == SDL_MOUSEBUTTONDOWN) { 1407 if ((d->menu || d->mode == identities_SidebarMode )&& ev->type == SDL_MOUSEBUTTONDOWN) {
1340 if (ev->button.button == SDL_BUTTON_RIGHT) { 1408 if (ev->button.button == SDL_BUTTON_RIGHT) {
1341 d->contextItem = NULL; 1409 d->contextItem = NULL;
1342 if (!isVisible_Widget(d->menu)) { 1410 if (!isVisible_Widget(d->menu)) {
1343 updateMouseHover_ListWidget(d->list); 1411 updateMouseHover_ListWidget(d->list);
1344 } 1412 }
1345 if (constHoverItem_ListWidget(d->list) || isVisible_Widget(d->menu)) { 1413 if (constHoverItem_ListWidget(d->list) || isVisible_Widget(d->menu)) {
1346 d->contextItem = hoverItem_ListWidget(d->list); 1414 d->contextItem = hoverItem_ListWidget(d->list);
1347 /* Context is drawn in hover state. */ 1415 /* Context is drawn in hover state. */
1348 if (d->contextIndex != iInvalidPos) { 1416 if (d->contextIndex != iInvalidPos) {
1349 invalidateItem_ListWidget(d->list, d->contextIndex); 1417 invalidateItem_ListWidget(d->list, d->contextIndex);
1350 } 1418 }
1351 d->contextIndex = hoverItemIndex_ListWidget(d->list); 1419 d->contextIndex = hoverItemIndex_ListWidget(d->list);
1352 /* Update menu items. */ 1420 /* Update menu items. */
1421 updateContextMenu_SidebarWidget_(d);
1353 /* TODO: Some callback-based mechanism would be nice for updating menus right 1422 /* TODO: Some callback-based mechanism would be nice for updating menus right
1354 before they open? */ 1423 before they open? */
1355 if (d->mode == bookmarks_SidebarMode && d->contextItem) { 1424 if (d->mode == bookmarks_SidebarMode && d->contextItem) {
@@ -1407,11 +1476,13 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1407 (!cmdClear && cmdUse && isUsedOn_GmIdentity(ident, docUrl)) || 1476 (!cmdClear && cmdUse && isUsedOn_GmIdentity(ident, docUrl)) ||
1408 (!cmdClear && !cmdUse && !isUsedOn_GmIdentity(ident, docUrl))); 1477 (!cmdClear && !cmdUse && !isUsedOn_GmIdentity(ident, docUrl)));
1409 } 1478 }
1479 /*
1410 else if (equal_Command(cmdItem, "ident.showuse")) { 1480 else if (equal_Command(cmdItem, "ident.showuse")) {
1411 setFlags_Widget(as_Widget(menuItem), 1481 setFlags_Widget(as_Widget(menuItem),
1412 disabled_WidgetFlag, 1482 disabled_WidgetFlag,
1413 !isUsed_GmIdentity(ident)); 1483 !isUsed_GmIdentity(ident));
1414 } 1484 }
1485 */
1415 } 1486 }
1416 } 1487 }
1417 } 1488 }
@@ -1423,10 +1494,7 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1423 const int kmods = keyMods_Sym(ev->key.keysym.mod); 1494 const int kmods = keyMods_Sym(ev->key.keysym.mod);
1424 /* Hide the sidebar when Escape is pressed. */ 1495 /* Hide the sidebar when Escape is pressed. */
1425 if (kmods == 0 && key == SDLK_ESCAPE && isVisible_Widget(d)) { 1496 if (kmods == 0 && key == SDLK_ESCAPE && isVisible_Widget(d)) {
1426 setFlags_Widget(w, hidden_WidgetFlag, iTrue); 1497 postCommand_Widget(d, "%s.toggle", cstr_String(id_Widget(w)));
1427 arrange_Widget(w->parent);
1428 updateSize_DocumentWidget(document_App());
1429 refresh_Widget(w->parent);
1430 return iTrue; 1498 return iTrue;
1431 } 1499 }
1432 } 1500 }
@@ -1460,7 +1528,8 @@ static void draw_SidebarWidget_(const iSidebarWidget *d) {
1460 const iRect bounds = bounds_Widget(w); 1528 const iRect bounds = bounds_Widget(w);
1461 iPaint p; 1529 iPaint p;
1462 init_Paint(&p); 1530 init_Paint(&p);
1463 if (flags_Widget(w) & visualOffset_WidgetFlag && isVisible_Widget(w)) { 1531 if (flags_Widget(w) & visualOffset_WidgetFlag &&
1532 flags_Widget(w) & horizontalOffset_WidgetFlag && isVisible_Widget(w)) {
1464 fillRect_Paint(&p, boundsWithoutVisualOffset_Widget(w), tmBackground_ColorId); 1533 fillRect_Paint(&p, boundsWithoutVisualOffset_Widget(w), tmBackground_ColorId);
1465 } 1534 }
1466 draw_Widget(w); 1535 draw_Widget(w);
@@ -1493,6 +1562,7 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,
1493 const int itemHeight = height_Rect(itemRect); 1562 const int itemHeight = height_Rect(itemRect);
1494 const int iconColor = isHover ? (isPressing ? uiTextPressed_ColorId : uiIconHover_ColorId) 1563 const int iconColor = isHover ? (isPressing ? uiTextPressed_ColorId : uiIconHover_ColorId)
1495 : uiIcon_ColorId; 1564 : uiIcon_ColorId;
1565 const int altIconColor = isPressing ? uiTextPressed_ColorId : uiTextCaution_ColorId;
1496 const int font = sidebar->itemFonts[d->isBold ? 1 : 0]; 1566 const int font = sidebar->itemFonts[d->isBold ? 1 : 0];
1497 int bg = uiBackgroundSidebar_ColorId; 1567 int bg = uiBackgroundSidebar_ColorId;
1498 if (isHover) { 1568 if (isHover) {
@@ -1611,7 +1681,7 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,
1611 : uiText_ColorId; 1681 : uiText_ColorId;
1612 iString str; 1682 iString str;
1613 init_String(&str); 1683 init_String(&str);
1614 appendChar_String(&str, d->icon ? d->icon : 0x1f588); 1684 appendChar_String(&str, d->icon ? d->icon : 0x1f588);
1615 const iRect iconArea = { addX_I2(pos, gap_UI), 1685 const iRect iconArea = { addX_I2(pos, gap_UI),
1616 init_I2(1.75f * lineHeight_Text(font), itemHeight) }; 1686 init_I2(1.75f * lineHeight_Text(font), itemHeight) };
1617 drawCentered_Text(font, 1687 drawCentered_Text(font,
@@ -1625,9 +1695,13 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,
1625 deinit_String(&str); 1695 deinit_String(&str);
1626 const iInt2 textPos = addY_I2(topRight_Rect(iconArea), (itemHeight - lineHeight_Text(font)) / 2); 1696 const iInt2 textPos = addY_I2(topRight_Rect(iconArea), (itemHeight - lineHeight_Text(font)) / 2);
1627 drawRange_Text(font, textPos, fg, range_String(&d->label)); 1697 drawRange_Text(font, textPos, fg, range_String(&d->label));
1698 const int metaFont = default_FontId;
1699 const int metaIconWidth = 4.5f * gap_UI;
1628 const iInt2 metaPos = 1700 const iInt2 metaPos =
1629 init_I2(right_Rect(itemRect) - advanceRange_Text(font, range_String(&d->meta)).x - 1701 init_I2(right_Rect(itemRect) -
1630 2 * gap_UI - (scrollBarWidth ? scrollBarWidth - gap_UI : 0), 1702 length_String(&d->meta) *
1703 metaIconWidth
1704 - 2 * gap_UI - (blankWidth ? blankWidth - 1.5f * gap_UI : (gap_UI / 2)),
1631 textPos.y); 1705 textPos.y);
1632 fillRect_Paint(p, 1706 fillRect_Paint(p,
1633 init_Rect(metaPos.x, 1707 init_Rect(metaPos.x,
@@ -1635,10 +1709,22 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,
1635 right_Rect(itemRect) - metaPos.x, 1709 right_Rect(itemRect) - metaPos.x,
1636 height_Rect(itemRect)), 1710 height_Rect(itemRect)),
1637 bg); 1711 bg);
1638 drawRange_Text(font, 1712 iInt2 mpos = metaPos;
1639 metaPos, 1713 iStringConstIterator iter;
1640 isHover && isPressing ? fg : uiTextCaution_ColorId, 1714 init_StringConstIterator(&iter, &d->meta);
1641 range_String(&d->meta)); 1715 iRangecc range = { cstr_String(&d->meta), iter.pos };
1716 while (iter.value) {
1717 next_StringConstIterator(&iter);
1718 range.end = iter.pos;
1719 iRect iconArea = { mpos, init_I2(metaIconWidth, lineHeight_Text(metaFont)) };
1720 iRect visBounds = visualBounds_Text(metaFont, range);
1721 drawRange_Text(metaFont,
1722 sub_I2(mid_Rect(iconArea), mid_Rect(visBounds)),
1723 isHover && isPressing ? fg : uiTextCaution_ColorId,
1724 range);
1725 mpos.x += metaIconWidth;
1726 range.start = range.end;
1727 }
1642 } 1728 }
1643 else if (sidebar->mode == history_SidebarMode) { 1729 else if (sidebar->mode == history_SidebarMode) {
1644 iBeginCollect(); 1730 iBeginCollect();
@@ -1683,6 +1769,7 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,
1683 else if (sidebar->mode == identities_SidebarMode) { 1769 else if (sidebar->mode == identities_SidebarMode) {
1684 const int fg = isHover ? (isPressing ? uiTextPressed_ColorId : uiTextFramelessHover_ColorId) 1770 const int fg = isHover ? (isPressing ? uiTextPressed_ColorId : uiTextFramelessHover_ColorId)
1685 : uiTextStrong_ColorId; 1771 : uiTextStrong_ColorId;
1772 const iBool isUsedOnDomain = (d->indent != 0);
1686 iString icon; 1773 iString icon;
1687 initUnicodeN_String(&icon, &d->icon, 1); 1774 initUnicodeN_String(&icon, &d->icon, 1);
1688 iInt2 cPos = topLeft_Rect(itemRect); 1775 iInt2 cPos = topLeft_Rect(itemRect);
@@ -1694,8 +1781,21 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,
1694 const int metaFg = isHover ? permanent_ColorId | (isPressing ? uiTextPressed_ColorId 1781 const int metaFg = isHover ? permanent_ColorId | (isPressing ? uiTextPressed_ColorId
1695 : uiTextFramelessHover_ColorId) 1782 : uiTextFramelessHover_ColorId)
1696 : uiTextDim_ColorId; 1783 : uiTextDim_ColorId;
1697 drawRange_Text( 1784 if (!d->listItem.isSelected && !isUsedOnDomain) {
1698 font, cPos, d->listItem.isSelected ? iconColor : metaFg, range_String(&icon)); 1785 /* Draw an outline of the icon. */
1786 for (int off = 0; off < 4; ++off) {
1787 drawRange_Text(font,
1788 add_I2(cPos, init_I2(off % 2 == 0 ? -1 : 1, off / 2 == 0 ? -1 : 1)),
1789 metaFg,
1790 range_String(&icon));
1791 }
1792 }
1793 drawRange_Text(font,
1794 cPos,
1795 d->listItem.isSelected ? iconColor
1796 : isUsedOnDomain ? altIconColor
1797 : uiBackgroundSidebar_ColorId,
1798 range_String(&icon));
1699 deinit_String(&icon); 1799 deinit_String(&icon);
1700 drawRange_Text(d->listItem.isSelected ? sidebar->itemFonts[1] : font, 1800 drawRange_Text(d->listItem.isSelected ? sidebar->itemFonts[1] : font,
1701 add_I2(cPos, init_I2(indent, 0)), 1801 add_I2(cPos, init_I2(indent, 0)),
diff --git a/src/ui/text.c b/src/ui/text.c
index 889aa2e4..ffe08fca 100644
--- a/src/ui/text.c
+++ b/src/ui/text.c
@@ -32,6 +32,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
32 32
33#include <the_Foundation/array.h> 33#include <the_Foundation/array.h>
34#include <the_Foundation/file.h> 34#include <the_Foundation/file.h>
35#include <the_Foundation/fileinfo.h>
35#include <the_Foundation/hash.h> 36#include <the_Foundation/hash.h>
36#include <the_Foundation/math.h> 37#include <the_Foundation/math.h>
37#include <the_Foundation/stringlist.h> 38#include <the_Foundation/stringlist.h>
@@ -124,10 +125,6 @@ struct Impl_Font {
124 iBool isMonospaced; 125 iBool isMonospaced;
125 iBool manualKernOnly; 126 iBool manualKernOnly;
126 enum iFontSize sizeId; /* used to look up different fonts of matching size */ 127 enum iFontSize sizeId; /* used to look up different fonts of matching size */
127// enum iFontId
128// enum iFontId japaneseFont; /* font to use for Japanese glyphs */
129// enum iFontId chineseFont; /* font to use for Simplified Chinese glyphs */
130// enum iFontId koreanFont; /* font to use for Korean glyphs */
131 uint32_t indexTable[128 - 32]; /* quick ASCII lookup */ 128 uint32_t indexTable[128 - 32]; /* quick ASCII lookup */
132}; 129};
133 130
@@ -155,13 +152,17 @@ static void init_Font(iFont *d, const iBlock *data, int height, float scale,
155 d->xScale *= floorf(advance) / advance; 152 d->xScale *= floorf(advance) / advance;
156 } 153 }
157 } 154 }
158 d->vertOffset = height * (1.0f - scale) / 2; 155 d->baseline = ascent * d->yScale;
159 d->baseline = ascent * d->yScale; 156 d->vertOffset = height * (1.0f - scale) / 2;
160 d->sizeId = sizeId; 157 /* Custom tweaks. */
161// d->symbolsFont = symbolsFont; 158 if (data == &fontNotoSansSymbolsRegular_Embedded ||
162// d->japaneseFont = regularJapanese_FontId; 159 data == &fontNotoSansSymbols2Regular_Embedded) {
163// d->chineseFont = regularChinese_FontId; 160 d->vertOffset /= 2;
164// d->koreanFont = regularKorean_FontId; 161 }
162 else if (data == &fontNotoEmojiRegular_Embedded) {
163 //d->vertOffset -= height / 30;
164 }
165 d->sizeId = sizeId;
165 memset(d->indexTable, 0xff, sizeof(d->indexTable)); 166 memset(d->indexTable, 0xff, sizeof(d->indexTable));
166} 167}
167 168
@@ -215,7 +216,8 @@ struct Impl_Text {
215 iRegExp * ansiEscape; 216 iRegExp * ansiEscape;
216}; 217};
217 218
218static iText text_; 219static iText text_;
220static iBlock *userFont_;
219 221
220static void initFonts_Text_(iText *d) { 222static void initFonts_Text_(iText *d) {
221 const float textSize = fontSize_UI * d->contentFontSize; 223 const float textSize = fontSize_UI * d->contentFontSize;
@@ -321,27 +323,31 @@ static void initFonts_Text_(iText *d) {
321 { &fontIosevkaTermExtended_Embedded, smallMonoSize, 1.0f, contentMonoSmall_FontSize }, 323 { &fontIosevkaTermExtended_Embedded, smallMonoSize, 1.0f, contentMonoSmall_FontSize },
322 { &fontIosevkaTermExtended_Embedded, monoSize, 1.0f, contentMono_FontSize }, 324 { &fontIosevkaTermExtended_Embedded, monoSize, 1.0f, contentMono_FontSize },
323 /* extra content fonts */ 325 /* extra content fonts */
324 { &fontSourceSans3Regular_Embedded, textSize, scaling, contentRegular_FontSize }, 326 { &fontSourceSans3Regular_Embedded, textSize, scaling, contentRegular_FontSize },
325 { &fontIosevkaTermExtended_Embedded, textSize, 0.866f, contentRegular_FontSize }, 327 { &fontSourceSans3Regular_Embedded, textSize * 0.80f, scaling, contentRegular_FontSize },
326 /* symbols and scripts */ 328 /* symbols and scripts */
327#define DEFINE_FONT_SET(data) \ 329#define DEFINE_FONT_SET(data, glyphScale) \
328 { &data, uiSize, 1.0f, uiNormal_FontSize }, \ 330 { (data), uiSize, glyphScale, uiNormal_FontSize }, \
329 { &data, uiSize * 1.125f, 1.0f, uiMedium_FontSize }, \ 331 { (data), uiSize * 1.125f, glyphScale, uiMedium_FontSize }, \
330 { &data, uiSize * 1.333f, 1.0f, uiBig_FontSize }, \ 332 { (data), uiSize * 1.333f, glyphScale, uiBig_FontSize }, \
331 { &data, uiSize * 1.666f, 1.0f, uiLarge_FontSize }, \ 333 { (data), uiSize * 1.666f, glyphScale, uiLarge_FontSize }, \
332 { &data, textSize, 1.0f, contentRegular_FontSize }, \ 334 { (data), textSize, glyphScale, contentRegular_FontSize }, \
333 { &data, textSize * 1.200f, 1.0f, contentMedium_FontSize }, \ 335 { (data), textSize * 1.200f, glyphScale, contentMedium_FontSize }, \
334 { &data, textSize * 1.333f, 1.0f, contentBig_FontSize }, \ 336 { (data), textSize * 1.333f, glyphScale, contentBig_FontSize }, \
335 { &data, textSize * 1.666f, 1.0f, contentLarge_FontSize }, \ 337 { (data), textSize * 1.666f, glyphScale, contentLarge_FontSize }, \
336 { &data, textSize * 2.000f, 1.0f, contentHuge_FontSize }, \ 338 { (data), textSize * 2.000f, glyphScale, contentHuge_FontSize }, \
337 { &data, smallMonoSize, 1.0f, contentMonoSmall_FontSize }, \ 339 { (data), smallMonoSize, glyphScale, contentMonoSmall_FontSize }, \
338 { &data, monoSize, 1.0f, contentMono_FontSize } 340 { (data), monoSize, glyphScale, contentMono_FontSize }
339 DEFINE_FONT_SET(fontSymbola_Embedded), 341 DEFINE_FONT_SET(userFont_ ? userFont_ : &fontIosevkaTermExtended_Embedded, 1.0f),
340 DEFINE_FONT_SET(fontNotoEmojiRegular_Embedded), 342 DEFINE_FONT_SET(&fontIosevkaTermExtended_Embedded, 0.866f),
341 DEFINE_FONT_SET(fontNotoSansJPRegular_Embedded), 343 DEFINE_FONT_SET(&fontNotoSansSymbolsRegular_Embedded, 1.45f),
342 DEFINE_FONT_SET(fontNotoSansSCRegular_Embedded), 344 DEFINE_FONT_SET(&fontNotoSansSymbols2Regular_Embedded, 1.45f),
343 DEFINE_FONT_SET(fontNanumGothicRegular_Embedded), /* TODO: should use Noto Sans here, too */ 345 DEFINE_FONT_SET(&fontSmolEmojiRegular_Embedded, 1.0f),
344 DEFINE_FONT_SET(fontNotoSansArabicUIRegular_Embedded), 346 DEFINE_FONT_SET(&fontNotoEmojiRegular_Embedded, 1.10f),
347 DEFINE_FONT_SET(&fontNotoSansJPRegular_Embedded, 1.0f),
348 DEFINE_FONT_SET(&fontNotoSansSCRegular_Embedded, 1.0f),
349 DEFINE_FONT_SET(&fontNanumGothicRegular_Embedded, 1.0f), /* TODO: should use Noto Sans here, too */
350 DEFINE_FONT_SET(&fontNotoSansArabicUIRegular_Embedded, 1.0f),
345 }; 351 };
346 iForIndices(i, fontData) { 352 iForIndices(i, fontData) {
347 iFont *font = &d->fonts[i]; 353 iFont *font = &d->fonts[i];
@@ -401,8 +407,28 @@ static void deinitCache_Text_(iText *d) {
401 SDL_DestroyTexture(d->cache); 407 SDL_DestroyTexture(d->cache);
402} 408}
403 409
410void loadUserFonts_Text(void) {
411 if (userFont_) {
412 delete_Block(userFont_);
413 userFont_ = NULL;
414 }
415 /* Load the system font. */
416 const iPrefs *prefs = prefs_App();
417 if (!isEmpty_String(&prefs->symbolFontPath)) {
418 iFile *f = new_File(&prefs->symbolFontPath);
419 if (open_File(f, readOnly_FileMode)) {
420 userFont_ = readAll_File(f);
421 }
422 else {
423 fprintf(stderr, "[Text] failed to open: %s\n", cstr_String(&prefs->symbolFontPath));
424 }
425 iRelease(f);
426 }
427}
428
404void init_Text(SDL_Renderer *render) { 429void init_Text(SDL_Renderer *render) {
405 iText *d = &text_; 430 iText *d = &text_;
431 loadUserFonts_Text();
406 d->contentFont = nunito_TextFont; 432 d->contentFont = nunito_TextFont;
407 d->headingFont = nunito_TextFont; 433 d->headingFont = nunito_TextFont;
408 d->contentFontSize = contentScale_Text_; 434 d->contentFontSize = contentScale_Text_;
@@ -542,14 +568,36 @@ static void allocate_Font_(iFont *d, iGlyph *glyph, int hoff) {
542} 568}
543 569
544iLocalDef iFont *characterFont_Font_(iFont *d, iChar ch, uint32_t *glyphIndex) { 570iLocalDef iFont *characterFont_Font_(iFont *d, iChar ch, uint32_t *glyphIndex) {
571 if (isVariationSelector_Char(ch)) {
572 return d;
573 }
574 /* Smol Emoji overrides all other fonts. */
575 if (ch != 0x20) {
576 iFont *smol = font_Text_(smolEmoji_FontId + d->sizeId);
577 if (smol != d && (*glyphIndex = glyphIndex_Font_(smol, ch)) != 0) {
578 return smol;
579 }
580 }
581 /* Manual exceptions. */ {
582 if (ch >= 0x2190 && ch <= 0x2193 /* arrows */) {
583 d = font_Text_(iosevka_FontId + d->sizeId);
584 *glyphIndex = glyphIndex_Font_(d, ch);
585 return d;
586 }
587 }
545 if ((*glyphIndex = glyphIndex_Font_(d, ch)) != 0) { 588 if ((*glyphIndex = glyphIndex_Font_(d, ch)) != 0) {
546 return d; 589 return d;
547 } 590 }
548 /* Not defined in current font, try Noto Emoji (for selected characters). */ 591 const int fallbacks[] = {
549 if ((ch >= 0x1f300 && ch < 0x1f600) || (ch >= 0x1f680 && ch <= 0x1f6c5)) { 592 notoEmoji_FontId,
550 iFont *emoji = font_Text_(emoji_FontId + d->sizeId); 593 symbols2_FontId,
551 if (emoji != d && (*glyphIndex = glyphIndex_Font_(emoji, ch)) != 0) { 594 symbols_FontId
552 return emoji; 595 };
596 /* First fallback is Smol Emoji. */
597 iForIndices(i, fallbacks) {
598 iFont *fallback = font_Text_(fallbacks[i] + d->sizeId);
599 if (fallback != d && (*glyphIndex = glyphIndex_Font_(fallback, ch)) != 0) {
600 return fallback;
553 } 601 }
554 } 602 }
555 /* Try Simplified Chinese. */ 603 /* Try Simplified Chinese. */
@@ -584,17 +632,25 @@ iLocalDef iFont *characterFont_Font_(iFont *d, iChar ch, uint32_t *glyphIndex) {
584 /* White up arrow is used for the Shift key on macOS. Symbola's glyph is not a great 632 /* White up arrow is used for the Shift key on macOS. Symbola's glyph is not a great
585 match to the other text, so use the UI font instead. */ 633 match to the other text, so use the UI font instead. */
586 if ((ch == 0x2318 || ch == 0x21e7) && d == font_Text_(regular_FontId)) { 634 if ((ch == 0x2318 || ch == 0x21e7) && d == font_Text_(regular_FontId)) {
587 *glyphIndex = glyphIndex_Font_(d = font_Text_(defaultContentSized_FontId), ch); 635 *glyphIndex = glyphIndex_Font_(d = font_Text_(defaultContentRegular_FontId), ch);
588 return d; 636 return d;
589 } 637 }
590#endif 638#endif
591 /* Fall back to Symbola for anything else. */ 639 /* User's symbols font. */ {
592 iFont *font = font_Text_(symbols_FontId + d->sizeId); 640 iFont *sys = font_Text_(userSymbols_FontId + d->sizeId);
593 *glyphIndex = glyphIndex_Font_(font, ch); 641 if (sys != d && (*glyphIndex = glyphIndex_Font_(sys, ch)) != 0) {
594// if (!*glyphIndex) { 642 return sys;
595// fprintf(stderr, "failed to find %08x (%lc)\n", ch, ch); fflush(stderr); 643 }
596// } 644 }
597 return font; 645 /* Final fallback. */
646 iFont *font = font_Text_(iosevka_FontId + d->sizeId);
647 if (d != font) {
648 *glyphIndex = glyphIndex_Font_(font, ch);
649 }
650 if (!*glyphIndex) {
651 fprintf(stderr, "failed to find %08x (%lc)\n", ch, (int)ch); fflush(stderr);
652 }
653 return d;
598} 654}
599 655
600static iGlyph *glyph_Font_(iFont *d, iChar ch) { 656static iGlyph *glyph_Font_(iFont *d, iChar ch) {
@@ -1190,7 +1246,7 @@ iInt2 advanceN_Text(int fontId, const char *text, size_t n) {
1190 return init_I2(advance, lineHeight_Text(fontId)); 1246 return init_I2(advance, lineHeight_Text(fontId));
1191} 1247}
1192 1248
1193static void drawBounded_Text_(int fontId, iInt2 pos, int xposBound, int color, iRangecc text) { 1249static void drawBoundedN_Text_(int fontId, iInt2 pos, int xposBound, int color, iRangecc text, size_t maxLen) {
1194 iText *d = &text_; 1250 iText *d = &text_;
1195 iFont *font = font_Text_(fontId); 1251 iFont *font = font_Text_(fontId);
1196 const iColor clr = get_Color(color & mask_ColorId); 1252 const iColor clr = get_Color(color & mask_ColorId);
@@ -1201,11 +1257,16 @@ static void drawBounded_Text_(int fontId, iInt2 pos, int xposBound, int color, i
1201 (color & fillBackground_ColorId ? fillBackground_RunMode : 0) | 1257 (color & fillBackground_ColorId ? fillBackground_RunMode : 0) |
1202 runFlagsFromId_(fontId), 1258 runFlagsFromId_(fontId),
1203 .text = text, 1259 .text = text,
1260 .maxLen = maxLen,
1204 .pos = pos, 1261 .pos = pos,
1205 .xposLayoutBound = xposBound, 1262 .xposLayoutBound = xposBound,
1206 .color = color & mask_ColorId }); 1263 .color = color & mask_ColorId });
1207} 1264}
1208 1265
1266static void drawBounded_Text_(int fontId, iInt2 pos, int xposBound, int color, iRangecc text) {
1267 drawBoundedN_Text_(fontId, pos, xposBound, color, text, 0);
1268}
1269
1209static void draw_Text_(int fontId, iInt2 pos, int color, iRangecc text) { 1270static void draw_Text_(int fontId, iInt2 pos, int color, iRangecc text) {
1210 drawBounded_Text_(fontId, pos, 0, color, text); 1271 drawBounded_Text_(fontId, pos, 0, color, text);
1211} 1272}
@@ -1248,6 +1309,10 @@ void drawRange_Text(int fontId, iInt2 pos, int color, iRangecc text) {
1248 draw_Text_(fontId, pos, color, text); 1309 draw_Text_(fontId, pos, color, text);
1249} 1310}
1250 1311
1312void drawRangeN_Text(int fontId, iInt2 pos, int color, iRangecc text, size_t maxChars) {
1313 drawBoundedN_Text_(fontId, pos, 0, color, text, maxChars);
1314}
1315
1251iInt2 advanceWrapRange_Text(int fontId, int maxWidth, iRangecc text) { 1316iInt2 advanceWrapRange_Text(int fontId, int maxWidth, iRangecc text) {
1252 iInt2 size = zero_I2(); 1317 iInt2 size = zero_I2();
1253 const char *endp; 1318 const char *endp;
@@ -1285,13 +1350,16 @@ void drawCentered_Text(int fontId, iRect rect, iBool alignVisual, int color, con
1285 vprintf_Block(&chars, format, args); 1350 vprintf_Block(&chars, format, args);
1286 va_end(args); 1351 va_end(args);
1287 } 1352 }
1288 const iRangecc text = range_Block(&chars); 1353 drawCenteredRange_Text(fontId, rect, alignVisual, color, range_Block(&chars));
1289 iRect textBounds = alignVisual ? visualBounds_Text(fontId, text) 1354 deinit_Block(&chars);
1355}
1356
1357void drawCenteredRange_Text(int fontId, iRect rect, iBool alignVisual, int color, iRangecc text) {
1358 iRect textBounds = alignVisual ? visualBounds_Text(fontId, text)
1290 : (iRect){ zero_I2(), advanceRange_Text(fontId, text) }; 1359 : (iRect){ zero_I2(), advanceRange_Text(fontId, text) };
1291 textBounds.pos = sub_I2(mid_Rect(rect), mid_Rect(textBounds)); 1360 textBounds.pos = sub_I2(mid_Rect(rect), mid_Rect(textBounds));
1292 textBounds.pos.x = iMax(textBounds.pos.x, left_Rect(rect)); /* keep left edge visible */ 1361 textBounds.pos.x = iMax(textBounds.pos.x, left_Rect(rect)); /* keep left edge visible */
1293 draw_Text_(fontId, textBounds.pos, color, text); 1362 draw_Text_(fontId, textBounds.pos, color, text);
1294 deinit_Block(&chars);
1295} 1363}
1296 1364
1297SDL_Texture *glyphCache_Text(void) { 1365SDL_Texture *glyphCache_Text(void) {
diff --git a/src/ui/text.h b/src/ui/text.h
index 044ddd32..2f2bcf3a 100644
--- a/src/ui/text.h
+++ b/src/ui/text.h
@@ -67,12 +67,16 @@ enum iFontId {
67 monospaceSmall_FontId, 67 monospaceSmall_FontId,
68 monospace_FontId, 68 monospace_FontId,
69 /* extra content fonts */ 69 /* extra content fonts */
70 defaultContentSized_FontId, /* UI font but sized to regular_FontId */ 70 defaultContentRegular_FontId, /* UI font but sized to regular_FontId */
71 regularMonospace_FontId, 71 defaultContentSmall_FontId, /* UI font but sized smaller */
72 /* symbols and scripts */ 72 /* symbols and scripts */
73 symbols_FontId, 73 userSymbols_FontId,
74 emoji_FontId = symbols_FontId + max_FontSize, 74 iosevka_FontId = userSymbols_FontId + max_FontSize,
75 japanese_FontId = emoji_FontId + max_FontSize, 75 symbols_FontId = iosevka_FontId + max_FontSize,
76 symbols2_FontId = symbols_FontId + max_FontSize,
77 smolEmoji_FontId = symbols2_FontId + max_FontSize,
78 notoEmoji_FontId = smolEmoji_FontId + max_FontSize,
79 japanese_FontId = notoEmoji_FontId + max_FontSize,
76 chineseSimplified_FontId = japanese_FontId + max_FontSize, 80 chineseSimplified_FontId = japanese_FontId + max_FontSize,
77 korean_FontId = chineseSimplified_FontId + max_FontSize, 81 korean_FontId = chineseSimplified_FontId + max_FontSize,
78 arabic_FontId = korean_FontId + max_FontSize, 82 arabic_FontId = korean_FontId + max_FontSize,
@@ -91,7 +95,7 @@ enum iFontId {
91 uiInput_FontId = defaultMedium_FontId, 95 uiInput_FontId = defaultMedium_FontId,
92 uiContent_FontId = defaultMedium_FontId, 96 uiContent_FontId = defaultMedium_FontId,
93 uiContentBold_FontId = defaultMediumBold_FontId, 97 uiContentBold_FontId = defaultMediumBold_FontId,
94 uiContentSymbols_FontId = symbols_FontId + uiMedium_FontSize, 98 uiContentSymbols_FontId = symbols_FontId + uiMedium_FontSize,
95 /* Document fonts: */ 99 /* Document fonts: */
96 paragraph_FontId = regular_FontId, 100 paragraph_FontId = regular_FontId,
97 firstParagraph_FontId = medium_FontId, 101 firstParagraph_FontId = medium_FontId,
@@ -102,6 +106,7 @@ enum iFontId {
102 heading2_FontId = largeBold_FontId, 106 heading2_FontId = largeBold_FontId,
103 heading3_FontId = big_FontId, 107 heading3_FontId = big_FontId,
104 banner_FontId = largeLight_FontId, 108 banner_FontId = largeLight_FontId,
109 regularMonospace_FontId = iosevka_FontId + contentRegular_FontSize
105}; 110};
106 111
107iLocalDef iBool isJapanese_FontId(enum iFontId id) { 112iLocalDef iBool isJapanese_FontId(enum iFontId id) {
@@ -124,6 +129,8 @@ extern int gap_Text; /* affected by content font size */
124void init_Text (SDL_Renderer *); 129void init_Text (SDL_Renderer *);
125void deinit_Text (void); 130void deinit_Text (void);
126 131
132void loadUserFonts_Text (void); /* based on Prefs */
133
127void setContentFont_Text (enum iTextFont font); 134void setContentFont_Text (enum iTextFont font);
128void setHeadingFont_Text (enum iTextFont font); 135void setHeadingFont_Text (enum iTextFont font);
129void setContentFontSize_Text (float fontSizeFactor); /* affects all except `default*` fonts */ 136void setContentFontSize_Text (float fontSizeFactor); /* affects all except `default*` fonts */
@@ -151,13 +158,15 @@ void setOpacity_Text (float opacity);
151 158
152void cache_Text (int fontId, iRangecc text); /* pre-render glyphs */ 159void cache_Text (int fontId, iRangecc text); /* pre-render glyphs */
153 160
154void draw_Text (int fontId, iInt2 pos, int color, const char *text, ...); 161void draw_Text (int fontId, iInt2 pos, int color, const char *text, ...);
155void drawAlign_Text (int fontId, iInt2 pos, int color, enum iAlignment align, const char *text, ...); 162void drawAlign_Text (int fontId, iInt2 pos, int color, enum iAlignment align, const char *text, ...);
156void drawCentered_Text (int fontId, iRect rect, iBool alignVisual, int color, const char *text, ...); 163void drawCentered_Text (int fontId, iRect rect, iBool alignVisual, int color, const char *text, ...);
157void drawString_Text (int fontId, iInt2 pos, int color, const iString *text); 164void drawCenteredRange_Text (int fontId, iRect rect, iBool alignVisual, int color, iRangecc text);
158void drawRange_Text (int fontId, iInt2 pos, int color, iRangecc text); 165void drawString_Text (int fontId, iInt2 pos, int color, const iString *text);
159void drawBoundRange_Text (int fontId, iInt2 pos, int boundWidth, int color, iRangecc text); /* bound does not wrap */ 166void drawRange_Text (int fontId, iInt2 pos, int color, iRangecc text);
160int drawWrapRange_Text (int fontId, iInt2 pos, int maxWidth, int color, iRangecc text); /* returns new Y */ 167void drawRangeN_Text (int fontId, iInt2 pos, int color, iRangecc text, size_t maxLen);
168void drawBoundRange_Text (int fontId, iInt2 pos, int boundWidth, int color, iRangecc text); /* bound does not wrap */
169int drawWrapRange_Text (int fontId, iInt2 pos, int maxWidth, int color, iRangecc text); /* returns new Y */
161 170
162SDL_Texture * glyphCache_Text (void); 171SDL_Texture * glyphCache_Text (void);
163 172
diff --git a/src/ui/translation.c b/src/ui/translation.c
index 4102fcb9..88edc48b 100644
--- a/src/ui/translation.c
+++ b/src/ui/translation.c
@@ -460,6 +460,7 @@ iBool handleCommand_Translation(iTranslation *d, const char *cmd) {
460 if (equalWidget_Command(cmd, w, "translation.finished")) { 460 if (equalWidget_Command(cmd, w, "translation.finished")) {
461 if (!isFinished_Translation(d)) { 461 if (!isFinished_Translation(d)) {
462 if (processResult_Translation_(d)) { 462 if (processResult_Translation_(d)) {
463 setupSheetTransition_Mobile(d->dlg, iFalse);
463 destroy_Widget(d->dlg); 464 destroy_Widget(d->dlg);
464 d->dlg = NULL; 465 d->dlg = NULL;
465 } 466 }
@@ -472,10 +473,11 @@ iBool handleCommand_Translation(iTranslation *d, const char *cmd) {
472 updateTextCStr_LabelWidget( 473 updateTextCStr_LabelWidget(
473 findMenuItem_Widget(findChild_Widget(d->dlg, "dialogbuttons"), 474 findMenuItem_Widget(findChild_Widget(d->dlg, "dialogbuttons"),
474 "translation.cancel"), 475 "translation.cancel"),
475 "${dismiss}"); 476 "${close}");
476 cancel_TlsRequest(d->request); 477 cancel_TlsRequest(d->request);
477 } 478 }
478 else { 479 else {
480 setupSheetTransition_Mobile(d->dlg, iFalse);
479 destroy_Widget(d->dlg); 481 destroy_Widget(d->dlg);
480 d->dlg = NULL; 482 d->dlg = NULL;
481 } 483 }
diff --git a/src/ui/util.c b/src/ui/util.c
index 4b35f8f7..c4fb8886 100644
--- a/src/ui/util.c
+++ b/src/ui/util.c
@@ -1176,6 +1176,7 @@ iBool valueInputHandler_(iWidget *dlg, const char *cmd) {
1176 postCommandf_App("valueinput.cancelled id:%s", cstr_String(id_Widget(dlg))); 1176 postCommandf_App("valueinput.cancelled id:%s", cstr_String(id_Widget(dlg)));
1177 setId_Widget(dlg, ""); /* no further commands to emit */ 1177 setId_Widget(dlg, ""); /* no further commands to emit */
1178 } 1178 }
1179 setupSheetTransition_Mobile(dlg, iFalse);
1179 destroy_Widget(dlg); 1180 destroy_Widget(dlg);
1180 return iTrue; 1181 return iTrue;
1181 } 1182 }
@@ -1184,11 +1185,13 @@ iBool valueInputHandler_(iWidget *dlg, const char *cmd) {
1184 else if (equal_Command(cmd, "cancel")) { 1185 else if (equal_Command(cmd, "cancel")) {
1185 postCommandf_App("valueinput.cancelled id:%s", cstr_String(id_Widget(dlg))); 1186 postCommandf_App("valueinput.cancelled id:%s", cstr_String(id_Widget(dlg)));
1186 setId_Widget(dlg, ""); /* no further commands to emit */ 1187 setId_Widget(dlg, ""); /* no further commands to emit */
1188 setupSheetTransition_Mobile(dlg, iFalse);
1187 destroy_Widget(dlg); 1189 destroy_Widget(dlg);
1188 return iTrue; 1190 return iTrue;
1189 } 1191 }
1190 else if (equal_Command(cmd, "valueinput.accept")) { 1192 else if (equal_Command(cmd, "valueinput.accept")) {
1191 acceptValueInput_(dlg); 1193 acceptValueInput_(dlg);
1194 setupSheetTransition_Mobile(dlg, iFalse);
1192 destroy_Widget(dlg); 1195 destroy_Widget(dlg);
1193 return iTrue; 1196 return iTrue;
1194 } 1197 }
@@ -1324,6 +1327,7 @@ static iBool messageHandler_(iWidget *msg, const char *cmd) {
1324 equal_Command(cmd, "scrollbar.fade") || 1327 equal_Command(cmd, "scrollbar.fade") ||
1325 equal_Command(cmd, "widget.overflow") || 1328 equal_Command(cmd, "widget.overflow") ||
1326 startsWith_CStr(cmd, "window."))) { 1329 startsWith_CStr(cmd, "window."))) {
1330 setupSheetTransition_Mobile(msg, iFalse);
1327 destroy_Widget(msg); 1331 destroy_Widget(msg);
1328 } 1332 }
1329 return iFalse; 1333 return iFalse;
@@ -1521,6 +1525,7 @@ void updatePreferencesLayout_Widget(iWidget *prefs) {
1521 static const char *inputIds[] = { 1525 static const char *inputIds[] = {
1522 "prefs.searchurl", 1526 "prefs.searchurl",
1523 "prefs.downloads", 1527 "prefs.downloads",
1528 "prefs.userfont",
1524 "prefs.ca.file", 1529 "prefs.ca.file",
1525 "prefs.ca.path", 1530 "prefs.ca.path",
1526 "prefs.proxy.gemini", 1531 "prefs.proxy.gemini",
@@ -1547,8 +1552,8 @@ void updatePreferencesLayout_Widget(iWidget *prefs) {
1547 } 1552 }
1548} 1553}
1549 1554
1550static void addDialogInputWithHeading_(iWidget *headings, iWidget *values, const char *labelText, 1555static void addDialogInputWithHeadingAndFlags_(iWidget *headings, iWidget *values, const char *labelText,
1551 const char *inputId, iInputWidget *input) { 1556 const char *inputId, iInputWidget *input, int64_t flags) {
1552 iLabelWidget *head = addChild_Widget(headings, iClob(makeHeading_Widget(labelText))); 1557 iLabelWidget *head = addChild_Widget(headings, iClob(makeHeading_Widget(labelText)));
1553#if defined (iPlatformMobile) 1558#if defined (iPlatformMobile)
1554 /* On mobile, inputs have 2 gaps of extra padding. */ 1559 /* On mobile, inputs have 2 gaps of extra padding. */
@@ -1560,6 +1565,13 @@ static void addDialogInputWithHeading_(iWidget *headings, iWidget *values, const
1560 /* Ensure that the label has the same height as the input widget. */ 1565 /* Ensure that the label has the same height as the input widget. */
1561 as_Widget(head)->sizeRef = as_Widget(input); 1566 as_Widget(head)->sizeRef = as_Widget(input);
1562 } 1567 }
1568 setFlags_Widget(as_Widget(head), flags, iTrue);
1569 setFlags_Widget(as_Widget(input), flags, iTrue);
1570}
1571
1572static void addDialogInputWithHeading_(iWidget *headings, iWidget *values, const char *labelText,
1573 const char *inputId, iInputWidget *input) {
1574 addDialogInputWithHeadingAndFlags_(headings, values, labelText, inputId, input, 0);
1563} 1575}
1564 1576
1565iInputWidget *addTwoColumnDialogInputField_Widget(iWidget *headings, iWidget *values, 1577iInputWidget *addTwoColumnDialogInputField_Widget(iWidget *headings, iWidget *values,
@@ -1686,6 +1698,8 @@ iWidget *makePreferences_Widget(void) {
1686 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.customframe}"))); 1698 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.customframe}")));
1687 addChild_Widget(values, iClob(makeToggle_Widget("prefs.customframe"))); 1699 addChild_Widget(values, iClob(makeToggle_Widget("prefs.customframe")));
1688#endif 1700#endif
1701 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.animate}")));
1702 addChild_Widget(values, iClob(makeToggle_Widget("prefs.animate")));
1689 makeTwoColumnHeading_("${heading.prefs.scrolling}", headings, values); 1703 makeTwoColumnHeading_("${heading.prefs.scrolling}", headings, values);
1690 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.smoothscroll}"))); 1704 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.smoothscroll}")));
1691 addChild_Widget(values, iClob(makeToggle_Widget("prefs.smoothscroll"))); 1705 addChild_Widget(values, iClob(makeToggle_Widget("prefs.smoothscroll")));
@@ -1775,6 +1789,7 @@ iWidget *makePreferences_Widget(void) {
1775 updateSize_LabelWidget((iLabelWidget *) tog); 1789 updateSize_LabelWidget((iLabelWidget *) tog);
1776 } 1790 }
1777 addChildFlags_Widget(values, iClob(boldLink), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag); 1791 addChildFlags_Widget(values, iClob(boldLink), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag);
1792 addPrefsInputWithHeading_(headings, values, "prefs.userfont", iClob(new_InputWidget(0)));
1778 } 1793 }
1779 makeTwoColumnHeading_("${heading.prefs.paragraph}", headings, values); 1794 makeTwoColumnHeading_("${heading.prefs.paragraph}", headings, values);
1780 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.linewidth}"))); 1795 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.linewidth}")));
@@ -1834,7 +1849,7 @@ iWidget *makePreferences_Widget(void) {
1834 updatePreferencesLayout_Widget(dlg); 1849 updatePreferencesLayout_Widget(dlg);
1835 addChild_Widget(dlg, 1850 addChild_Widget(dlg,
1836 iClob(makeDialogButtons_Widget( 1851 iClob(makeDialogButtons_Widget(
1837 (iMenuItem[]){ { "${dismiss}", SDLK_ESCAPE, 0, "prefs.dismiss" } }, 1))); 1852 (iMenuItem[]){ { "${close}", SDLK_ESCAPE, 0, "prefs.dismiss" } }, 1)));
1838 addChild_Widget(dlg->root->widget, iClob(dlg)); 1853 addChild_Widget(dlg->root->widget, iClob(dlg));
1839 finalizeSheet_Mobile(dlg); 1854 finalizeSheet_Mobile(dlg);
1840 setupSheetTransition_Mobile(dlg, iTrue); 1855 setupSheetTransition_Mobile(dlg, iTrue);
@@ -1908,6 +1923,7 @@ static iBool handleBookmarkCreationCommands_SidebarWidget_(iWidget *editor, cons
1908 } 1923 }
1909 postCommand_App("bookmarks.changed"); 1924 postCommand_App("bookmarks.changed");
1910 } 1925 }
1926 setupSheetTransition_Mobile(editor, iFalse);
1911 destroy_Widget(editor); 1927 destroy_Widget(editor);
1912 return iTrue; 1928 return iTrue;
1913 } 1929 }
@@ -1937,6 +1953,7 @@ iWidget *makeBookmarkCreation_Widget(const iString *url, const iString *title, i
1937 1953
1938static iBool handleFeedSettingCommands_(iWidget *dlg, const char *cmd) { 1954static iBool handleFeedSettingCommands_(iWidget *dlg, const char *cmd) {
1939 if (equal_Command(cmd, "cancel")) { 1955 if (equal_Command(cmd, "cancel")) {
1956 setupSheetTransition_Mobile(dlg, iFalse);
1940 destroy_Widget(dlg); 1957 destroy_Widget(dlg);
1941 return iTrue; 1958 return iTrue;
1942 } 1959 }
@@ -1971,6 +1988,7 @@ static iBool handleFeedSettingCommands_(iWidget *dlg, const char *cmd) {
1971 } 1988 }
1972 } 1989 }
1973 postCommand_App("bookmarks.changed"); 1990 postCommand_App("bookmarks.changed");
1991 setupSheetTransition_Mobile(dlg, iFalse);
1974 destroy_Widget(dlg); 1992 destroy_Widget(dlg);
1975 return iTrue; 1993 return iTrue;
1976 } 1994 }
@@ -2042,7 +2060,21 @@ iWidget *makeIdentityCreation_Widget(void) {
2042 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); 2060 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
2043 iWidget *values = addChildFlags_Widget( 2061 iWidget *values = addChildFlags_Widget(
2044 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); 2062 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
2063 setId_Widget(headings, "headings");
2064 setId_Widget(values, "values");
2045 iInputWidget *inputs[6]; 2065 iInputWidget *inputs[6];
2066 /* Where will the new identity be active on? */ {
2067 addChild_Widget(headings, iClob(makeHeading_Widget("${dlg.newident.scope}")));
2068 const iMenuItem items[] = {
2069 { "${dlg.newident.scope.domain}", 0, 0, "ident.scope arg:0" },
2070 { "${dlg.newident.scope.page}", 0, 0, "ident.scope arg:1" },
2071 { "${dlg.newident.scope.none}", 0, 0, "ident.scope arg:2" },
2072 };
2073 setId_Widget(addChild_Widget(values,
2074 iClob(makeMenuButton_LabelWidget(
2075 items[0].label, items, iElemCount(items)))),
2076 "ident.scope");
2077 }
2046 addDialogInputWithHeading_(headings, 2078 addDialogInputWithHeading_(headings,
2047 values, 2079 values,
2048 "${dlg.newident.until}", 2080 "${dlg.newident.until}",
@@ -2059,32 +2091,35 @@ iWidget *makeIdentityCreation_Widget(void) {
2059 setFlags_Widget(tmpGroup, arrangeSize_WidgetFlag | arrangeHorizontal_WidgetFlag, iTrue); 2091 setFlags_Widget(tmpGroup, arrangeSize_WidgetFlag | arrangeHorizontal_WidgetFlag, iTrue);
2060 addChild_Widget(tmpGroup, iClob(makeToggle_Widget("ident.temp"))); 2092 addChild_Widget(tmpGroup, iClob(makeToggle_Widget("ident.temp")));
2061 setId_Widget( 2093 setId_Widget(
2062 addChildFlags_Widget( 2094 addChildFlags_Widget(tmpGroup,
2063 tmpGroup, 2095 iClob(new_LabelWidget(uiTextCaution_ColorEscape warning_Icon
2064 iClob(new_LabelWidget(uiTextCaution_ColorEscape "\u26a0 ${dlg.newident.notsaved}", NULL)), 2096 " ${dlg.newident.notsaved}",
2065 hidden_WidgetFlag | frameless_WidgetFlag), 2097 NULL)),
2098 hidden_WidgetFlag | frameless_WidgetFlag),
2066 "ident.temp.note"); 2099 "ident.temp.note");
2067 addChild_Widget(values, iClob(tmpGroup)); 2100 addChild_Widget(values, iClob(tmpGroup));
2068 } 2101 }
2069 addChild_Widget(headings, iClob(makePadding_Widget(gap_UI))); 2102 addChildFlags_Widget(headings, iClob(makePadding_Widget(gap_UI)), collapse_WidgetFlag | hidden_WidgetFlag);
2070 addChild_Widget(values, iClob(makePadding_Widget(gap_UI))); 2103 addChildFlags_Widget(values, iClob(makePadding_Widget(gap_UI)), collapse_WidgetFlag | hidden_WidgetFlag);
2071 addDialogInputWithHeading_(headings, values, "${dlg.newident.email}", "ident.email", iClob(inputs[1] = newHint_InputWidget(0, "${hint.newident.optional}"))); 2104 addDialogInputWithHeadingAndFlags_(headings, values, "${dlg.newident.email}", "ident.email", iClob(inputs[1] = newHint_InputWidget(0, "${hint.newident.optional}")), collapse_WidgetFlag | hidden_WidgetFlag);
2072 addDialogInputWithHeading_(headings, values, "${dlg.newident.userid}", "ident.userid", iClob(inputs[2] = newHint_InputWidget(0, "${hint.newident.optional}"))); 2105 addDialogInputWithHeadingAndFlags_(headings, values, "${dlg.newident.userid}", "ident.userid", iClob(inputs[2] = newHint_InputWidget(0, "${hint.newident.optional}")), collapse_WidgetFlag | hidden_WidgetFlag);
2073 addDialogInputWithHeading_(headings, values, "${dlg.newident.domain}", "ident.domain", iClob(inputs[3] = newHint_InputWidget(0, "${hint.newident.optional}"))); 2106 addDialogInputWithHeadingAndFlags_(headings, values, "${dlg.newident.domain}", "ident.domain", iClob(inputs[3] = newHint_InputWidget(0, "${hint.newident.optional}")), collapse_WidgetFlag | hidden_WidgetFlag);
2074 addDialogInputWithHeading_(headings, values, "${dlg.newident.org}", "ident.org", iClob(inputs[4] = newHint_InputWidget(0, "${hint.newident.optional}"))); 2107 addDialogInputWithHeadingAndFlags_(headings, values, "${dlg.newident.org}", "ident.org", iClob(inputs[4] = newHint_InputWidget(0, "${hint.newident.optional}")), collapse_WidgetFlag | hidden_WidgetFlag);
2075 addDialogInputWithHeading_(headings, values, "${dlg.newident.country}", "ident.country", iClob(inputs[5] = newHint_InputWidget(0, "${hint.newident.optional}"))); 2108 addDialogInputWithHeadingAndFlags_(headings, values, "${dlg.newident.country}", "ident.country", iClob(inputs[5] = newHint_InputWidget(0, "${hint.newident.optional}")), collapse_WidgetFlag | hidden_WidgetFlag);
2076 arrange_Widget(dlg); 2109 arrange_Widget(dlg);
2077 for (size_t i = 0; i < iElemCount(inputs); ++i) { 2110 for (size_t i = 0; i < iElemCount(inputs); ++i) {
2078 as_Widget(inputs[i])->rect.size.x = 100 * gap_UI - headings->rect.size.x; 2111 as_Widget(inputs[i])->rect.size.x = 100 * gap_UI - headings->rect.size.x;
2079 } 2112 }
2080 addChild_Widget(dlg, 2113 addChild_Widget(dlg,
2081 iClob(makeDialogButtons_Widget( 2114 iClob(makeDialogButtons_Widget(
2082 (iMenuItem[]){ { "${cancel}", 0, 0, NULL }, 2115 (iMenuItem[]){ { "${dlg.newident.more}", 0, 0, "ident.showmore" },
2116 { "---", 0, 0, NULL },
2117 { "${cancel}", SDLK_ESCAPE, 0, "ident.cancel" },
2083 { uiTextAction_ColorEscape "${dlg.newident.create}", 2118 { uiTextAction_ColorEscape "${dlg.newident.create}",
2084 SDLK_RETURN, 2119 SDLK_RETURN,
2085 KMOD_PRIMARY, 2120 KMOD_PRIMARY,
2086 "ident.accept" } }, 2121 "ident.accept" } },
2087 2))); 2122 4)));
2088 addChild_Widget(get_Root()->widget, iClob(dlg)); 2123 addChild_Widget(get_Root()->widget, iClob(dlg));
2089 finalizeSheet_Mobile(dlg); 2124 finalizeSheet_Mobile(dlg);
2090 return dlg; 2125 return dlg;
diff --git a/src/ui/widget.c b/src/ui/widget.c
index d31f7577..4eac7ecf 100644
--- a/src/ui/widget.c
+++ b/src/ui/widget.c
@@ -59,6 +59,7 @@ void init_Widget(iWidget *d) {
59 d->minSize = zero_I2(); 59 d->minSize = zero_I2();
60 d->sizeRef = NULL; 60 d->sizeRef = NULL;
61 d->offsetRef = NULL; 61 d->offsetRef = NULL;
62 d->animOffsetRef = NULL;
62 d->bgColor = none_ColorId; 63 d->bgColor = none_ColorId;
63 d->frameColor = none_ColorId; 64 d->frameColor = none_ColorId;
64 init_Anim(&d->visualOffset, 0.0f); 65 init_Anim(&d->visualOffset, 0.0f);
@@ -101,9 +102,6 @@ static void aboutToBeDestroyed_Widget_(iWidget *d) {
101 setFocus_Widget(NULL); 102 setFocus_Widget(NULL);
102 return; 103 return;
103 } 104 }
104 if (flags_Widget(d) & keepOnTop_WidgetFlag) {
105 removeOne_PtrArray(onTop_Root(d->root), d);
106 }
107 remove_Periodic(periodic_App(), d); 105 remove_Periodic(periodic_App(), d);
108 if (isHover_Widget(d)) { 106 if (isHover_Widget(d)) {
109 get_Window()->hover = NULL; 107 get_Window()->hover = NULL;
@@ -771,6 +769,9 @@ static void applyVisualOffset_Widget_(const iWidget *d, iInt2 *pos) {
771 pos->y += off; 769 pos->y += off;
772 } 770 }
773 } 771 }
772 if (d->animOffsetRef) {
773 pos->y -= value_Anim(d->animOffsetRef);
774 }
774 if (d->flags & refChildrenOffset_WidgetFlag) { 775 if (d->flags & refChildrenOffset_WidgetFlag) {
775 iConstForEach(ObjectList, i, children_Widget(d->offsetRef)) { 776 iConstForEach(ObjectList, i, children_Widget(d->offsetRef)) {
776 const iWidget *child = i.object; 777 const iWidget *child = i.object;
@@ -843,6 +844,12 @@ iBool containsExpanded_Widget(const iWidget *d, iInt2 windowCoord, int expand) {
843 addY_I2(d->rect.size, 844 addY_I2(d->rect.size,
844 d->flags & drawBackgroundToBottom_WidgetFlag ? size_Root(d->root).y : 0) 845 d->flags & drawBackgroundToBottom_WidgetFlag ? size_Root(d->root).y : 0)
845 }; 846 };
847 /* Apply the animated offset. (Visual offsets don't affect interaction.) */
848 for (const iWidget *w = d; w; w = w->parent) {
849 if (w->animOffsetRef) {
850 windowCoord.y += value_Anim(w->animOffsetRef);
851 }
852 }
846 return contains_Rect(expand ? expanded_Rect(bounds, init1_I2(expand)) : bounds, 853 return contains_Rect(expand ? expanded_Rect(bounds, init1_I2(expand)) : bounds,
847 windowToInner_Widget(d, windowCoord)); 854 windowToInner_Widget(d, windowCoord));
848} 855}
@@ -857,6 +864,9 @@ iLocalDef iBool isMouseEvent_(const SDL_Event *ev) {
857} 864}
858 865
859static iBool filterEvent_Widget_(const iWidget *d, const SDL_Event *ev) { 866static iBool filterEvent_Widget_(const iWidget *d, const SDL_Event *ev) {
867 if (d->flags & destroyPending_WidgetFlag) {
868 return iFalse; /* no more events handled */
869 }
860 const iBool isKey = isKeyboardEvent_(ev); 870 const iBool isKey = isKeyboardEvent_(ev);
861 const iBool isMouse = isMouseEvent_(ev); 871 const iBool isMouse = isMouseEvent_(ev);
862 if ((d->flags & disabled_WidgetFlag) || (d->flags & hidden_WidgetFlag && 872 if ((d->flags & disabled_WidgetFlag) || (d->flags & hidden_WidgetFlag &&
@@ -1102,8 +1112,8 @@ void drawBackground_Widget(const iWidget *d) {
1102 drawSoftShadow_Paint(&p, bounds_Widget(d), 12 * gap_UI, black_ColorId, 30); 1112 drawSoftShadow_Paint(&p, bounds_Widget(d), 12 * gap_UI, black_ColorId, 30);
1103 } 1113 }
1104 const iBool isFaded = fadeBackground && 1114 const iBool isFaded = fadeBackground &&
1105 ~d->flags & noFadeBackground_WidgetFlag && 1115 ~d->flags & noFadeBackground_WidgetFlag;/* &&
1106 ~d->flags & destroyPending_WidgetFlag; 1116 ~d->flags & destroyPending_WidgetFlag;*/
1107 if (isFaded) { 1117 if (isFaded) {
1108 iPaint p; 1118 iPaint p;
1109 init_Paint(&p); 1119 init_Paint(&p);
diff --git a/src/ui/widget.h b/src/ui/widget.h
index 79d45f23..8de62b7a 100644
--- a/src/ui/widget.h
+++ b/src/ui/widget.h
@@ -139,6 +139,7 @@ struct Impl_Widget {
139 iInt2 minSize; 139 iInt2 minSize;
140 iWidget * sizeRef; 140 iWidget * sizeRef;
141 iWidget * offsetRef; 141 iWidget * offsetRef;
142 const iAnim *animOffsetRef;
142 int padding[4]; /* left, top, right, bottom */ 143 int padding[4]; /* left, top, right, bottom */
143 iAnim visualOffset; 144 iAnim visualOffset;
144 int bgColor; 145 int bgColor;
diff --git a/src/ui/window.c b/src/ui/window.c
index 87db2f3e..96a22fee 100644
--- a/src/ui/window.c
+++ b/src/ui/window.c
@@ -858,6 +858,19 @@ iBool processEvent_Window(iWindow *d, const SDL_Event *ev) {
858 const iInt2 pos = coord_Window(d, event.button.x, event.button.y); 858 const iInt2 pos = coord_Window(d, event.button.x, event.button.y);
859 event.button.x = pos.x; 859 event.button.x = pos.x;
860 event.button.y = pos.y; 860 event.button.y = pos.y;
861 if (event.type == SDL_MOUSEBUTTONDOWN) {
862 /* Button clicks will change keyroot. */
863 if (numRoots_Window(d) > 1) {
864 const iInt2 click = init_I2(event.button.x, event.button.y);
865 iForIndices(i, d->roots) {
866 iRoot *root = d->roots[i];
867 if (root != d->keyRoot && contains_Rect(rect_Root(root), click)) {
868 setKeyRoot_Window(d, root);
869 break;
870 }
871 }
872 }
873 }
861 } 874 }
862 const iWidget *oldHover = d->hover; 875 const iWidget *oldHover = d->hover;
863 iBool wasUsed = iFalse; 876 iBool wasUsed = iFalse;
@@ -889,7 +902,9 @@ iBool processEvent_Window(iWindow *d, const SDL_Event *ev) {
889 wasUsed = dispatchEvent_Window(d, &paste); 902 wasUsed = dispatchEvent_Window(d, &paste);
890 } 903 }
891 if (event.type == SDL_MOUSEBUTTONDOWN && event.button.button == SDL_BUTTON_RIGHT) { 904 if (event.type == SDL_MOUSEBUTTONDOWN && event.button.button == SDL_BUTTON_RIGHT) {
892 postContextClick_Window(d, &event.button); 905 if (postContextClick_Window(d, &event.button)) {
906 wasUsed = iTrue;
907 }
893 } 908 }
894 } 909 }
895 if (isMetricsChange_UserEvent(&event)) { 910 if (isMetricsChange_UserEvent(&event)) {