summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJaakko Keränen <jaakko.keranen@iki.fi>2022-02-20 12:53:12 +0200
committerJaakko Keränen <jaakko.keranen@iki.fi>2022-02-20 12:53:12 +0200
commit6d7b6122e68c8d53b6e7533a6e5d5f55e33831e1 (patch)
tree80bc2867510e3c9b7bc43578fc416165dae3876c
parent2d52f13afb15232cc9c834f0aa14284bf9072a31 (diff)
parent41f378c4b46cb5dd3599d44c81fa51d3183eefee (diff)
Merge branch 'work/v1.11' into dev
# Conflicts: # res/lang/es.bin # res/lang/ie.bin # res/lang/ru.bin # res/lang/sr.bin # res/lang/uk.bin
-rw-r--r--CMakeLists.txt2
-rw-r--r--Depends.cmake2
m---------lib/the_Foundation0
-rw-r--r--po/en.po76
-rw-r--r--res/about/help.gmi34
-rw-r--r--res/about/version.gmi3
-rw-r--r--res/lang/cs.binbin32252 -> 33216 bytes
-rw-r--r--res/lang/de.binbin31062 -> 32026 bytes
-rw-r--r--res/lang/en.binbin27131 -> 28095 bytes
-rw-r--r--res/lang/eo.binbin26321 -> 27285 bytes
-rw-r--r--res/lang/es_MX.binbin28201 -> 29165 bytes
-rw-r--r--res/lang/fi.binbin30772 -> 31736 bytes
-rw-r--r--res/lang/fr.binbin31859 -> 32823 bytes
-rw-r--r--res/lang/gl.binbin30135 -> 31099 bytes
-rw-r--r--res/lang/hu.binbin31884 -> 32848 bytes
-rw-r--r--res/lang/ia.binbin29914 -> 30878 bytes
-rw-r--r--res/lang/isv.binbin25852 -> 26816 bytes
-rw-r--r--res/lang/nl.binbin29241 -> 30205 bytes
-rw-r--r--res/lang/pl.binbin30487 -> 31451 bytes
-rw-r--r--res/lang/sk.binbin26188 -> 27152 bytes
-rw-r--r--res/lang/tok.binbin27977 -> 28941 bytes
-rw-r--r--res/lang/tr.binbin30078 -> 31042 bytes
-rw-r--r--res/lang/zh_Hans.binbin26096 -> 27060 bytes
-rw-r--r--res/lang/zh_Hant.binbin26494 -> 27458 bytes
-rw-r--r--src/app.c553
-rw-r--r--src/app.h13
-rw-r--r--src/defs.h12
-rw-r--r--src/fontpack.c120
-rw-r--r--src/fontpack.h4
-rw-r--r--src/gmdocument.c61
-rw-r--r--src/gmdocument.h4
-rw-r--r--src/gmutil.c87
-rw-r--r--src/gmutil.h9
-rw-r--r--src/macos.m51
-rw-r--r--src/periodic.c19
-rw-r--r--src/prefs.c12
-rw-r--r--src/prefs.h7
-rw-r--r--src/sitespec.c32
-rw-r--r--src/sitespec.h1
-rw-r--r--src/ui/banner.c3
-rw-r--r--src/ui/color.c4
-rw-r--r--src/ui/color.h2
-rw-r--r--src/ui/documentwidget.c135
-rw-r--r--src/ui/inputwidget.c5
-rw-r--r--src/ui/inputwidget.h1
-rw-r--r--src/ui/keys.c2
-rw-r--r--src/ui/labelwidget.c12
-rw-r--r--src/ui/linkinfo.c4
-rw-r--r--src/ui/lookupwidget.c2
-rw-r--r--src/ui/root.c16
-rw-r--r--src/ui/root.h1
-rw-r--r--src/ui/sidebarwidget.c187
-rw-r--r--src/ui/text.c60
-rw-r--r--src/ui/text.h2
-rw-r--r--src/ui/touch.c3
-rw-r--r--src/ui/util.c200
-rw-r--r--src/ui/util.h14
-rw-r--r--src/ui/widget.c3
-rw-r--r--src/ui/window.c75
-rw-r--r--src/ui/window.h3
60 files changed, 1366 insertions, 470 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index a1e4226c..11348fcc 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -18,7 +18,7 @@
18cmake_minimum_required (VERSION 3.9) 18cmake_minimum_required (VERSION 3.9)
19 19
20project (Lagrange 20project (Lagrange
21 VERSION 1.10.6 21 VERSION 1.11.0
22 DESCRIPTION "A Beautiful Gemini Client" 22 DESCRIPTION "A Beautiful Gemini Client"
23 LANGUAGES C 23 LANGUAGES C
24) 24)
diff --git a/Depends.cmake b/Depends.cmake
index b7a6cd25..348039b3 100644
--- a/Depends.cmake
+++ b/Depends.cmake
@@ -18,7 +18,7 @@ set (_dependsToBuild)
18 18
19if (NOT EXISTS ${CMAKE_SOURCE_DIR}/lib/the_Foundation/CMakeLists.txt) 19if (NOT EXISTS ${CMAKE_SOURCE_DIR}/lib/the_Foundation/CMakeLists.txt)
20 set (INSTALL_THE_FOUNDATION YES) 20 set (INSTALL_THE_FOUNDATION YES)
21 find_package (the_Foundation 1.1.0 REQUIRED) 21 find_package (the_Foundation 1.2.0 REQUIRED)
22else () 22else ()
23 if (EXISTS ${CMAKE_SOURCE_DIR}/lib/the_Foundation/.git) 23 if (EXISTS ${CMAKE_SOURCE_DIR}/lib/the_Foundation/.git)
24 # the_Foundation is checked out as a submodule, make sure it's up to date. 24 # the_Foundation is checked out as a submodule, make sure it's up to date.
diff --git a/lib/the_Foundation b/lib/the_Foundation
Subproject 453f05f6efb46824ff0dca0174036c7624473e4 Subproject 11e7a3bdbb4b40ea4417518dac9eba7ad55748d
diff --git a/po/en.po b/po/en.po
index e528a967..6647324e 100644
--- a/po/en.po
+++ b/po/en.po
@@ -148,6 +148,9 @@ msgstr "Identity"
148msgid "menu.title.help" 148msgid "menu.title.help"
149msgstr "Help" 149msgstr "Help"
150 150
151msgid "menu.newwindow"
152msgstr "New Window"
153
151msgid "menu.newtab" 154msgid "menu.newtab"
152msgstr "New Tab" 155msgstr "New Tab"
153 156
@@ -519,9 +522,6 @@ msgstr "%b. %d, %Y"
519msgid "feeds.today" 522msgid "feeds.today"
520msgstr "Today" 523msgstr "Today"
521 524
522msgid "feeds.entry.newtab"
523msgstr "Open Entry in New Tab"
524
525msgid "feeds.entry.markread" 525msgid "feeds.entry.markread"
526msgstr "Mark as Read" 526msgstr "Mark as Read"
527 527
@@ -558,6 +558,9 @@ msgstr "Open in New Tab"
558msgid "menu.opentab.background" 558msgid "menu.opentab.background"
559msgstr "Open in Background Tab" 559msgstr "Open in Background Tab"
560 560
561msgid "menu.openwindow"
562msgstr "Open in New Window"
563
561msgid "menu.openfile" 564msgid "menu.openfile"
562msgstr "Open File…" 565msgstr "Open File…"
563 566
@@ -760,6 +763,27 @@ msgstr "Trust"
760msgid "dlg.cert.fingerprint" 763msgid "dlg.cert.fingerprint"
761msgstr "Copy Fingerprint" 764msgstr "Copy Fingerprint"
762 765
766msgid "pageinfo.settings"
767msgstr "Settings"
768
769msgid "heading.sitespec"
770msgstr "Site-Specific Settings"
771
772msgid "sitespec.ansi"
773msgstr "ANSI escape warnings:"
774
775msgid "sitespec.palette"
776msgstr "Theme palette seed:"
777
778msgid "sitespec.accept"
779msgstr "Save Settings"
780
781msgid "keys.pageinfo"
782msgstr "Show page information"
783
784msgid "keys.sitespec"
785msgstr "Show site-specific settings"
786
763#, c-format 787#, c-format
764msgid "dlg.input.prompt" 788msgid "dlg.input.prompt"
765msgstr "Please enter input for %s:" 789msgstr "Please enter input for %s:"
@@ -849,6 +873,9 @@ msgstr "Open Link to the Side"
849msgid "link.side.newtab" 873msgid "link.side.newtab"
850msgstr "Open Link in New Tab to the Side" 874msgstr "Open Link in New Tab to the Side"
851 875
876msgid "link.newwindow"
877msgstr "Open Link in New Window"
878
852msgid "link.browser" 879msgid "link.browser"
853msgstr "Open Link in Default Browser" 880msgstr "Open Link in Default Browser"
854 881
@@ -1184,6 +1211,33 @@ msgstr "Russian"
1184msgid "lang.es" 1211msgid "lang.es"
1185msgstr "Spanish" 1212msgstr "Spanish"
1186 1213
1214msgid "heading.glyphfinder"
1215msgstr "Missing Glyphs"
1216
1217msgid "dlg.glyphfinder.missing"
1218msgstr "The following characters could not be shown:"
1219
1220msgid "dlg.glyphfinder.help"
1221msgstr "You can try searching the skyjake.fi Font Library for fonts that provide glyphs for these characters, or manually install new TrueType fonts."
1222
1223msgid "dlg.glyphfinder.help.empty"
1224msgstr "Please reload the page to check again for missing glyphs."
1225
1226msgid "dlg.glyphfinder.disable"
1227msgstr "Disable Warnings"
1228
1229msgid "dlg.glyphfinder.search"
1230msgstr "Search Font Library"
1231
1232msgid "heading.glyphfinder.results"
1233msgstr "Search Results"
1234
1235msgid "glyphfinder.results"
1236msgstr "The following fontpacks provide one or more of the missing glyphs:"
1237
1238msgid "glyphfinder.results.empty"
1239msgstr "Sorry, no matching fontpacks were found."
1240
1187msgid "heading.newident" 1241msgid "heading.newident"
1188msgstr "New Identity" 1242msgstr "New Identity"
1189 1243
@@ -1402,6 +1456,10 @@ msgstr "Collapse preformatted:"
1402msgid "prefs.bookmarks.addbottom" 1456msgid "prefs.bookmarks.addbottom"
1403msgstr "Add bookmarks to bottom:" 1457msgstr "Add bookmarks to bottom:"
1404 1458
1459# User preference that controls whether image data embedded in Data URLs gets automatically displayed when a page is loaded.
1460msgid "prefs.dataurl.openimages"
1461msgstr "Open images in Data URLs:"
1462
1405# User preference that controls whether index.gmi pages get automatically opened when browsing the contents of a directory inside a compressed archive. 1463# User preference that controls whether index.gmi pages get automatically opened when browsing the contents of a directory inside a compressed archive.
1406msgid "prefs.archive.openindex" 1464msgid "prefs.archive.openindex"
1407msgstr "Open archive indices:" 1465msgstr "Open archive indices:"
@@ -1643,6 +1701,9 @@ msgstr "Wrap plain text:"
1643msgid "prefs.decodeurls" 1701msgid "prefs.decodeurls"
1644msgstr "Decode URLs:" 1702msgstr "Decode URLs:"
1645 1703
1704msgid "prefs.urlsize"
1705msgstr "Maximum URL size:"
1706
1646msgid "prefs.cachesize" 1707msgid "prefs.cachesize"
1647msgstr "Cache size:" 1708msgstr "Cache size:"
1648 1709
@@ -2089,12 +2150,3 @@ msgstr "Permanently dismiss warning about terminal emulation on %s?"
2089 2150
2090msgid "dlg.dismiss.warning" 2151msgid "dlg.dismiss.warning"
2091msgstr "Dismiss Warning" 2152msgstr "Dismiss Warning"
2092
2093msgid "heading.fontpack.classic"
2094msgstr "Download Fontpack"
2095
2096msgid "dlg.fontpack.classic.msg"
2097msgstr "The fonts previously bundled with the app are now available as a separate download. Would you like to download the \"Classic set\" fontpack now?"
2098
2099msgid "dlg.fontpack.classic"
2100msgstr "Download Fontpack (25 MB)"
diff --git a/res/about/help.gmi b/res/about/help.gmi
index 93bc6a05..cd1a332e 100644
--- a/res/about/help.gmi
+++ b/res/about/help.gmi
@@ -106,10 +106,15 @@ The type and destination of a link are indicated by the link's icon and color:
106 106
107Link colors remain the same regardless of which color theme is being used for page content. (Color themes are discussed in the Customization section.) 107Link colors remain the same regardless of which color theme is being used for page content. (Color themes are discussed in the Customization section.)
108 108
109When you move the mouse cursor over a link, additional information will appear: the destination domain, with the URL scheme shown for non-Gemini links, and the date of the last visit to the URL. 109The "Show URL on hover" option can be enabled in Preferences to show additional information when you move the mouse cursor over a link: the destination domain, URL scheme for non-Gemini links, date of the last visit to the URL, and the identity that will be used when opening the link.
110 110
111If a link would normally use the default ➤ icon but there is an Emoji at the beginning of the link label, that Emoji is used as the link icon instead. In these cases, you can always assume that the link is a Gemini link whose destination is the same domain that you're currently on. 111If a link would normally use the default ➤ icon but there is an Emoji at the beginning of the link label, that Emoji is used as the link icon instead. In these cases, you can always assume that the link is a Gemini link whose destination is the same domain that you're currently on.
112 112
113The "Network" tab of Preferences has a few settings that affect the presentation of links and URLs in general:
114
115* "Decode URLs" causes percent-coding to be decoded for the user interface, so one can see international characters in URLs.
116* "Maximum URL size" sets a limit for how long URLs can be. While Gemini servers are required to enforce a limit of 1024 bytes for URLs, this setting affects all URLs regardless of scheme. Link lines with URLs longer than this will be presented as plain text.
117
113### 1.1.3 Page caching 118### 1.1.3 Page caching
114 119
115When navigating to a new page, the old page is cached in memory. If you navigate back, the cached copy of the page is restored. Think of it as rewinding time — you return to a past time as if nothing had happened. The same applies to forward navigation; cached pages are loaded if available. This allows back and forward navigation to happen instantly, without any network requests. 120When navigating to a new page, the old page is cached in memory. If you navigate back, the cached copy of the page is restored. Think of it as rewinding time — you return to a past time as if nothing had happened. The same applies to forward navigation; cached pages are loaded if available. This allows back and forward navigation to happen instantly, without any network requests.
@@ -355,6 +360,8 @@ You can find a number of settings in Preferences to customize the user interface
355 360
356One important characteristic of Gemini is that you remain in control of what gets loaded and when. The browser will not suddenly fetch a ton of images, autoplay videos, or make surreptitious connections to any tracking servers — each network request is purposeful and manually triggered. With this in mind, the "Load image on scroll" option is provided to assist keyboard-only browsing and to facilitate a focused reading experience. When enabled, if there is an image link visible on the page and you press Space or ↓, it will be loaded and shown inline _instead_ of the view scrolling down. This allows you to read and see all the content of the page while only tapping on a single key on the keyboard. 361One important characteristic of Gemini is that you remain in control of what gets loaded and when. The browser will not suddenly fetch a ton of images, autoplay videos, or make surreptitious connections to any tracking servers — each network request is purposeful and manually triggered. With this in mind, the "Load image on scroll" option is provided to assist keyboard-only browsing and to facilitate a focused reading experience. When enabled, if there is an image link visible on the page and you press Space or ↓, it will be loaded and shown inline _instead_ of the view scrolling down. This allows you to read and see all the content of the page while only tapping on a single key on the keyboard.
357 362
363The "Show images in Data URLs" option determines if image data embedded into links using data URLs is automatically shown after the page has finished loading. This only applies to a handful of common image media types: JPEG, PNG, GIF, and WebP. Note that the maximum size of an URL has a limit, so these embedded images have to fit under the configured limit to be recognized as valid links. Image from data URLs are displayed as links' inline content, just like with any other URL scheme.
364
358The "Open archive indices" option controls whether index.gmi pages are automatically opened while browsing the contents of a ZIP archive. The purpose is to simulate the behavior of a Gemini server where opening a directory will by default show its index page. Enabling this option makes navigating an archived copy of a capsule a more streamlined experience. 365The "Open archive indices" option controls whether index.gmi pages are automatically opened while browsing the contents of a ZIP archive. The purpose is to simulate the behavior of a Gemini server where opening a directory will by default show its index page. Enabling this option makes navigating an archived copy of a capsule a more streamlined experience.
359 366
360"Split view pinning" controls which tab links will be opened on when browsing in split view mode. The default mode is "Left Tab", which means that the page in the left tab is pinned (remains unchanged) when clicking on a link. For more information, see section 1.9. 367"Split view pinning" controls which tab links will be opened on when browsing in split view mode. The default mode is "Left Tab", which means that the page in the left tab is pinned (remains unchanged) when clicking on a link. For more information, see section 1.9.
@@ -388,6 +395,17 @@ Page content color themes are selected on the "Colors" tab of Preferences. The "
388* Sepia: Light sepia background with black text. Does not change depending on domain; use this for readability if you prefer a sepia reading experience. 395* Sepia: Light sepia background with black text. Does not change depending on domain; use this for readability if you prefer a sepia reading experience.
389* High Contrast: White background with black text. Does not change depending on domain; use this for readability if you prefer maximum contrast between text and the background. 396* High Contrast: White background with black text. Does not change depending on domain; use this for readability if you prefer maximum contrast between text and the background.
390 397
398### 2.3.1 Site-specific theme
399
400You can manually customize the color theme of a site. Open "Page Information" and click on "Settings" to open the Site-Specific Settings dialog. The default keyboard shortcut for this is ${SHIFT+}${CTRL+}Comma.
401
402The "Theme palette seed" is the input data given to the theme generator that determines a site's color palette. The theme generator is designed to algorithmically choose a palette whose colors go together. It is not possible to individually pick the colors of a theme.
403
404You can enter any text as the palette seed. By default, the seed is the site's hostname, or a user name found in the URL path (e.g., "gemini://example.com/~User/" → seed "User"). Examples of how to use this:
405
406* Enter a word or phrase, or just random characters, to find a theme that you like.
407* To copy the theme of another site, enter the hostname of that site as the seed.
408
391## 2.4 Fonts 409## 2.4 Fonts
392 410
393This version of Lagrange supports TrueType fonts. To use a new font, simply view a .ttf file in the app and a page footer action is available for performing the installation. For example, try drag-and-dropping a .ttf file on the window. Alternatively, you can manually copy the font to the "fonts" subdirectory of the user-specific configuration directory (see section 3.5). 411This version of Lagrange supports TrueType fonts. To use a new font, simply view a .ttf file in the app and a page footer action is available for performing the installation. For example, try drag-and-dropping a .ttf file on the window. Alternatively, you can manually copy the font to the "fonts" subdirectory of the user-specific configuration directory (see section 3.5).
@@ -493,7 +511,19 @@ Gemini allows relaying requests via a proxy server. On the "Network" tab, you ca
493 511
494When an HTTP proxy server is configured, HTTP/HTTPS links will no longer open in the system's default web browser but will be loaded via the proxy, expecting it to serve a 'text/gemini' version of the link contents. 512When an HTTP proxy server is configured, HTTP/HTTPS links will no longer open in the system's default web browser but will be loaded via the proxy, expecting it to serve a 'text/gemini' version of the link contents.
495 513
496## 2.7 Keybindings 514## 2.7 URL handling
515
516The "Network" tab of Preferences has a few options related to handling URLs.
517
518Enabling the "Decode URLs" option causes all percent-encoded URLs to be shown in decoded form in the UI. Enabling this option is useful when encountering URLs that contain characters outside the basic Latin (A-Z) alphabet. This does not affect what gets sent to a server when loading pages: Gemini requests are always required to be sent in encoded form.
519
520The "Maximum URL size" setting defines what is considered a valid link in Gemtext. Link lines with URLs longer than this are not considered to be links and are displayed like regular text lines. This only affects links that don't use the "gemini" scheme. The maximum size of a Gemini URL is 1024 bytes.
521
522Having a length limit is necessary due the special case of Data URLs:
523=> https://datatracker.ietf.org/doc/html/rfc2397 RFC 2397: The "data" URL scheme
524With Data URLs, one is able to embed arbitrary data into a Gemtext file using link lines. This makes it possible to circumvent the intentional limitations of Gemini and override the clients' ability to choose which linked resources should be loaded and when. Without a limit, a server could for example send an arbitrarily large image attachment as part of the page, with no regard to the user's or client's preferences or abilities to view images. Also note that like any non-Gemini scheme, "data" is not expected to be supported by Gemini clients.
525
526## 2.8 Keybindings
497 527
498The "Keys" tab lets you change which keys are bound to UI commands. Click on an item with the mouse and then press the new key combination that you want bound to it. Right-click on a binding to erase it or reset it to the default. 528The "Keys" tab lets you change which keys are bound to UI commands. Click on an item with the mouse and then press the new key combination that you want bound to it. Right-click on a binding to erase it or reset it to the default.
499 529
diff --git a/res/about/version.gmi b/res/about/version.gmi
index cf15ba65..8021d9df 100644
--- a/res/about/version.gmi
+++ b/res/about/version.gmi
@@ -6,6 +6,9 @@
6``` 6```
7# Release notes 7# Release notes
8 8
9## 1.11
10⚠️ Downgrading back to v1.10 causes all site-specific themes to be forgotten. Back up your sitespec.ini beforehand.
11
9## 1.10.6 12## 1.10.6
10* Added bindings for switching Feeds list to Unread/All mode. 13* Added bindings for switching Feeds list to Unread/All mode.
11* Fixed normalization of empty Gemini URL paths to `/` as per the November 2021 spec update. 14* Fixed normalization of empty Gemini URL paths to `/` as per the November 2021 spec update.
diff --git a/res/lang/cs.bin b/res/lang/cs.bin
index c10c9d89..0cf36a2b 100644
--- a/res/lang/cs.bin
+++ b/res/lang/cs.bin
Binary files differ
diff --git a/res/lang/de.bin b/res/lang/de.bin
index a7cbadec..ea6e1841 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 e1a3679e..372cef24 100644
--- a/res/lang/en.bin
+++ b/res/lang/en.bin
Binary files differ
diff --git a/res/lang/eo.bin b/res/lang/eo.bin
index 146cce67..8a9519b6 100644
--- a/res/lang/eo.bin
+++ b/res/lang/eo.bin
Binary files differ
diff --git a/res/lang/es_MX.bin b/res/lang/es_MX.bin
index 690f65e5..5c527322 100644
--- a/res/lang/es_MX.bin
+++ b/res/lang/es_MX.bin
Binary files differ
diff --git a/res/lang/fi.bin b/res/lang/fi.bin
index fe150f13..cdbda504 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 9a0ad5b4..3ab07f12 100644
--- a/res/lang/fr.bin
+++ b/res/lang/fr.bin
Binary files differ
diff --git a/res/lang/gl.bin b/res/lang/gl.bin
index 58518de1..5f5d467b 100644
--- a/res/lang/gl.bin
+++ b/res/lang/gl.bin
Binary files differ
diff --git a/res/lang/hu.bin b/res/lang/hu.bin
index 94e2dad0..17486efa 100644
--- a/res/lang/hu.bin
+++ b/res/lang/hu.bin
Binary files differ
diff --git a/res/lang/ia.bin b/res/lang/ia.bin
index 9d22be73..8cae5947 100644
--- a/res/lang/ia.bin
+++ b/res/lang/ia.bin
Binary files differ
diff --git a/res/lang/isv.bin b/res/lang/isv.bin
index 82242165..81d30963 100644
--- a/res/lang/isv.bin
+++ b/res/lang/isv.bin
Binary files differ
diff --git a/res/lang/nl.bin b/res/lang/nl.bin
index 86a9e929..e860c109 100644
--- a/res/lang/nl.bin
+++ b/res/lang/nl.bin
Binary files differ
diff --git a/res/lang/pl.bin b/res/lang/pl.bin
index 98fb053f..b759b1b7 100644
--- a/res/lang/pl.bin
+++ b/res/lang/pl.bin
Binary files differ
diff --git a/res/lang/sk.bin b/res/lang/sk.bin
index e06344fc..a0197c5b 100644
--- a/res/lang/sk.bin
+++ b/res/lang/sk.bin
Binary files differ
diff --git a/res/lang/tok.bin b/res/lang/tok.bin
index 6ec80d5a..977454a8 100644
--- a/res/lang/tok.bin
+++ b/res/lang/tok.bin
Binary files differ
diff --git a/res/lang/tr.bin b/res/lang/tr.bin
index 07351d2a..c131d0ea 100644
--- a/res/lang/tr.bin
+++ b/res/lang/tr.bin
Binary files differ
diff --git a/res/lang/zh_Hans.bin b/res/lang/zh_Hans.bin
index 400bb2b7..acaaaa77 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 ef468edf..cf674d55 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 b6c48062..543467ef 100644
--- a/src/app.c
+++ b/src/app.c
@@ -125,7 +125,8 @@ struct Impl_App {
125 iGmCerts * certs; 125 iGmCerts * certs;
126 iVisited * visited; 126 iVisited * visited;
127 iBookmarks * bookmarks; 127 iBookmarks * bookmarks;
128 iMainWindow *window; 128 iMainWindow *window; /* currently active MainWindow */
129 iPtrArray mainWindows;
129 iPtrArray popupWindows; 130 iPtrArray popupWindows;
130 iSortedArray tickers; /* per-frame callbacks, used for animations */ 131 iSortedArray tickers; /* per-frame callbacks, used for animations */
131 uint32_t lastTickerTime; 132 uint32_t lastTickerTime;
@@ -153,7 +154,8 @@ struct Impl_App {
153 /* Preferences: */ 154 /* Preferences: */
154 iBool commandEcho; /* --echo */ 155 iBool commandEcho; /* --echo */
155 iBool forceSoftwareRender; /* --sw */ 156 iBool forceSoftwareRender; /* --sw */
156 iRect initialWindowRect; 157 //iRect initialWindowRect;
158 iArray initialWindowRects; /* one per window */
157 iPrefs prefs; 159 iPrefs prefs;
158}; 160};
159 161
@@ -199,33 +201,43 @@ static iString *serializePrefs_App_(const iApp *d) {
199 appendFormat_String(str, "window.retain arg:%d\n", d->prefs.retainWindowSize); 201 appendFormat_String(str, "window.retain arg:%d\n", d->prefs.retainWindowSize);
200 if (d->prefs.retainWindowSize) { 202 if (d->prefs.retainWindowSize) {
201 int w, h, x, y; 203 int w, h, x, y;
202 x = d->window->place.normalRect.pos.x; 204 iConstForEach(PtrArray, i, &d->mainWindows) {
203 y = d->window->place.normalRect.pos.y; 205 const iMainWindow *win = i.ptr;
204 w = d->window->place.normalRect.size.x; 206 const size_t winIndex = index_PtrArrayConstIterator(&i);
205 h = d->window->place.normalRect.size.y; 207 x = win->place.normalRect.pos.x;
206 appendFormat_String(str, "window.setrect width:%d height:%d coord:%d %d\n", w, h, x, y); 208 y = win->place.normalRect.pos.y;
207 /* On macOS, maximization should be applied at creation time or the window will take 209 w = win->place.normalRect.size.x;
208 a moment to animate to its maximized size. */ 210 h = win->place.normalRect.size.y;
211 appendFormat_String(str,
212 "window.setrect index:%zu width:%d height:%d coord:%d %d\n",
213 winIndex,
214 w,
215 h,
216 x,
217 y);
218 /* On macOS, maximization should be applied at creation time or the window will take
219 a moment to animate to its maximized size. */
209#if defined (LAGRANGE_ENABLE_CUSTOM_FRAME) 220#if defined (LAGRANGE_ENABLE_CUSTOM_FRAME)
210 if (snap_MainWindow(d->window)) { 221 if (snap_MainWindow(win)) {
211 if (snap_MainWindow(d->window) == maximized_WindowSnap) { 222 if (snap_MainWindow(win) == maximized_WindowSnap) {
212 appendFormat_String(str, "~window.maximize\n"); 223 appendFormat_String(str, "~window.maximize index:%zu\n", winIndex);
213 } 224 }
214 else if (~SDL_GetWindowFlags(d->window->base.win) & SDL_WINDOW_MINIMIZED) { 225 else if (~SDL_GetWindowFlags(win->base.win) & SDL_WINDOW_MINIMIZED) {
215 /* Save the actual visible window position, too, because snapped windows may 226 /* Save the actual visible window position, too, because snapped windows may
216 still be resized/moved without affecting normalRect. */ 227 still be resized/moved without affecting normalRect. */
217 SDL_GetWindowPosition(d->window->base.win, &x, &y); 228 SDL_GetWindowPosition(win->base.win, &x, &y);
218 SDL_GetWindowSize(d->window->base.win, &w, &h); 229 SDL_GetWindowSize(win->base.win, &w, &h);
219 appendFormat_String( 230 appendFormat_String(
220 str, "~window.setrect snap:%d width:%d height:%d coord:%d %d\n", 231 str, "~window.setrect index:%zu snap:%d width:%d height:%d coord:%d %d\n",
221 snap_MainWindow(d->window), w, h, x, y); 232 winIndex, snap_MainWindow(d->window), w, h, x, y);
233 }
222 } 234 }
223 }
224#elif !defined (iPlatformApple) 235#elif !defined (iPlatformApple)
225 if (snap_MainWindow(d->window) == maximized_WindowSnap) { 236 if (snap_MainWindow(win) == maximized_WindowSnap) {
226 appendFormat_String(str, "~window.maximize\n"); 237 appendFormat_String(str, "~window.maximize index:%zu\n", winIndex);
227 } 238 }
228#endif 239#endif
240 }
229 } 241 }
230 appendFormat_String(str, "uilang id:%s\n", cstr_String(&d->prefs.strings[uiLanguage_PrefsString])); 242 appendFormat_String(str, "uilang id:%s\n", cstr_String(&d->prefs.strings[uiLanguage_PrefsString]));
231 appendFormat_String(str, "uiscale arg:%f\n", uiScale_Window(as_Window(d->window))); 243 appendFormat_String(str, "uiscale arg:%f\n", uiScale_Window(as_Window(d->window)));
@@ -244,6 +256,7 @@ static iString *serializePrefs_App_(const iApp *d) {
244 appendFormat_String(str, "imageloadscroll arg:%d\n", d->prefs.loadImageInsteadOfScrolling); 256 appendFormat_String(str, "imageloadscroll arg:%d\n", d->prefs.loadImageInsteadOfScrolling);
245 appendFormat_String(str, "cachesize.set arg:%d\n", d->prefs.maxCacheSize); 257 appendFormat_String(str, "cachesize.set arg:%d\n", d->prefs.maxCacheSize);
246 appendFormat_String(str, "memorysize.set arg:%d\n", d->prefs.maxMemorySize); 258 appendFormat_String(str, "memorysize.set arg:%d\n", d->prefs.maxMemorySize);
259 appendFormat_String(str, "urlsize.set arg:%d\n", d->prefs.maxUrlSize);
247 appendFormat_String(str, "decodeurls arg:%d\n", d->prefs.decodeUserVisibleURLs); 260 appendFormat_String(str, "decodeurls arg:%d\n", d->prefs.decodeUserVisibleURLs);
248 appendFormat_String(str, "linewidth.set arg:%d\n", d->prefs.lineWidth); 261 appendFormat_String(str, "linewidth.set arg:%d\n", d->prefs.lineWidth);
249 appendFormat_String(str, "linespacing.set arg:%f\n", d->prefs.lineSpacing); 262 appendFormat_String(str, "linespacing.set arg:%f\n", d->prefs.lineSpacing);
@@ -279,6 +292,7 @@ static iString *serializePrefs_App_(const iApp *d) {
279 { "prefs.collapsepreonload", &d->prefs.collapsePreOnLoad }, 292 { "prefs.collapsepreonload", &d->prefs.collapsePreOnLoad },
280 { "prefs.hoverlink", &d->prefs.hoverLink }, 293 { "prefs.hoverlink", &d->prefs.hoverLink },
281 { "prefs.bookmarks.addbottom", &d->prefs.addBookmarksToBottom }, 294 { "prefs.bookmarks.addbottom", &d->prefs.addBookmarksToBottom },
295 { "prefs.dataurl.openimages", &d->prefs.openDataUrlImagesOnLoad },
282 { "prefs.archive.openindex", &d->prefs.openArchiveIndexPages }, 296 { "prefs.archive.openindex", &d->prefs.openArchiveIndexPages },
283 { "prefs.font.warnmissing", &d->prefs.warnAboutMissingGlyphs }, 297 { "prefs.font.warnmissing", &d->prefs.warnAboutMissingGlyphs },
284 { "prefs.blink", &d->prefs.blinkingCursor }, 298 { "prefs.blink", &d->prefs.blinkingCursor },
@@ -401,9 +415,14 @@ static void loadPrefs_App_(iApp *d) {
401 d->prefs.customFrame = arg_Command(cmd); 415 d->prefs.customFrame = arg_Command(cmd);
402 } 416 }
403 else if (equal_Command(cmd, "window.setrect") && !argLabel_Command(cmd, "snap")) { 417 else if (equal_Command(cmd, "window.setrect") && !argLabel_Command(cmd, "snap")) {
404 const iInt2 pos = coord_Command(cmd); 418 const int index = argLabel_Command(cmd, "index");
405 d->initialWindowRect = init_Rect( 419 const iInt2 pos = coord_Command(cmd);
420 iRect winRect = init_Rect(
406 pos.x, pos.y, argLabel_Command(cmd, "width"), argLabel_Command(cmd, "height")); 421 pos.x, pos.y, argLabel_Command(cmd, "width"), argLabel_Command(cmd, "height"));
422 if (index >= 0 && index < 100) {
423 resize_Array(&d->initialWindowRects, index + 1);
424 set_Array(&d->initialWindowRects, index, &winRect);
425 }
407 } 426 }
408 else if (equal_Command(cmd, "fontpack.disable")) { 427 else if (equal_Command(cmd, "fontpack.disable")) {
409 insert_StringSet(d->prefs.disabledFontPacks, 428 insert_StringSet(d->prefs.disabledFontPacks,
@@ -439,6 +458,7 @@ static void loadPrefs_App_(iApp *d) {
439 } 458 }
440 iRelease(f); 459 iRelease(f);
441 /* Upgrade checks. */ 460 /* Upgrade checks. */
461#if 0 /* disabled in v1.11 (font library search) */
442 if (cmp_Version(&upgradedFromAppVersion, &(iVersion){ 1, 8, 0 }) < 0) { 462 if (cmp_Version(&upgradedFromAppVersion, &(iVersion){ 1, 8, 0 }) < 0) {
443#if !defined (iPlatformAppleMobile) && !defined (iPlatformAndroidMobile) 463#if !defined (iPlatformAppleMobile) && !defined (iPlatformAndroidMobile)
444 /* When upgrading to v1.8.0, the old hardcoded font library is gone and that means 464 /* When upgrading to v1.8.0, the old hardcoded font library is gone and that means
@@ -448,6 +468,7 @@ static void loadPrefs_App_(iApp *d) {
448 postCommand_App("~fontpack.suggest.classic"); 468 postCommand_App("~fontpack.suggest.classic");
449#endif 469#endif
450 } 470 }
471#endif
451#if !defined (LAGRANGE_ENABLE_CUSTOM_FRAME) 472#if !defined (LAGRANGE_ENABLE_CUSTOM_FRAME)
452 d->prefs.customFrame = iFalse; 473 d->prefs.customFrame = iFalse;
453#endif 474#endif
@@ -470,10 +491,36 @@ static const char *magicTabDocument_App_ = "tabd";
470static const char *magicSidebar_App_ = "side"; 491static const char *magicSidebar_App_ = "side";
471 492
472enum iDocumentStateFlag { 493enum iDocumentStateFlag {
473 current_DocumentStateFlag = iBit(1), 494 current_DocumentStateFlag = iBit(1),
474 rootIndex1_DocumentStateFlag = iBit(2) 495 rootIndex1_DocumentStateFlag = iBit(2),
496};
497
498enum iWindowStateFlag {
499 current_WindowStateFlag = iBit(9),
475}; 500};
476 501
502static iRect initialWindowRect_App_(const iApp *d, size_t windowIndex) {
503 if (windowIndex < size_Array(&d->initialWindowRects)) {
504 return constValue_Array(&d->initialWindowRects, windowIndex, iRect);
505 }
506 /* The default window rectangle. */
507 iRect rect = init_Rect(-1, -1, 900, 560);
508#if defined (iPlatformMsys)
509 /* Must scale by UI scaling factor. */
510 mulfv_I2(&rect.size, desktopDPI_Win32());
511#endif
512#if defined (iPlatformLinux) && !defined (iPlatformAndroid)
513 /* Scale by the primary (?) monitor DPI. */
514 if (isRunningUnderWindowSystem_App()) {
515 float vdpi;
516 SDL_GetDisplayDPI(0, NULL, NULL, &vdpi);
517 const float factor = vdpi / 96.0f;
518 mulfv_I2(&rect.size, iMax(factor, 1.0f));
519 }
520#endif
521 return rect;
522}
523
477static iBool loadState_App_(iApp *d) { 524static iBool loadState_App_(iApp *d) {
478 iUnused(d); 525 iUnused(d);
479 const char *oldPath = concatPath_CStr(dataDir_App_(), oldStateFileName_App_); 526 const char *oldPath = concatPath_CStr(dataDir_App_(), oldStateFileName_App_);
@@ -494,19 +541,49 @@ static iBool loadState_App_(iApp *d) {
494 } 541 }
495 setVersion_Stream(stream_File(f), version); 542 setVersion_Stream(stream_File(f), version);
496 /* Window state. */ 543 /* Window state. */
497 iDocumentWidget *doc = NULL; 544 iDeclareType(CurrentTabs);
498 iDocumentWidget *current[2] = { NULL, NULL }; 545 struct Impl_CurrentTabs {
499 iBool isFirstTab[2] = { iTrue, iTrue }; 546 iDocumentWidget *currentTab[2]; /* for each root */
547 };
548 int numWins = 0;
549 iMainWindow * win = NULL;
550 iMainWindow * currentWin = d->window;
551 iArray * currentTabs; /* two per window (per root per window) */
552 iBool isFirstTab[2];
553 currentTabs = collectNew_Array(sizeof(iCurrentTabs));
500 while (!atEnd_File(f)) { 554 while (!atEnd_File(f)) {
501 readData_File(f, 4, magic); 555 readData_File(f, 4, magic);
502 if (!memcmp(magic, magicWindow_App_, 4)) { 556 if (!memcmp(magic, magicWindow_App_, 4)) {
503 const int splitMode = read32_File(f); 557 numWins++;
504 const int keyRoot = read32_File(f); 558 const int splitMode = read32_File(f);
505 d->window->pendingSplitMode = splitMode; 559 const int winState = read32_File(f);
506 setSplitMode_MainWindow(d->window, splitMode | noEvents_WindowSplit); 560 const int keyRoot = (winState & 1);
507 d->window->base.keyRoot = d->window->base.roots[keyRoot]; 561 const iBool isCurrent = (winState & current_WindowStateFlag) != 0;
562// printf("[State] '%.4s' split:%d state:%x\n", magic, splitMode, winState);
563 if (numWins == 1) {
564 win = d->window;
565 }
566 else {
567 win = new_MainWindow(initialWindowRect_App_(d, numWins - 1));
568 addWindow_App(win);
569 }
570 pushBack_Array(currentTabs, &(iCurrentTabs){ { NULL, NULL } });
571 isFirstTab[0] = isFirstTab[1] = iTrue;
572 if (isCurrent) {
573 currentWin = win;
574 }
575 setCurrent_Window(win);
576 setCurrent_Root(NULL);
577 win->pendingSplitMode = splitMode;
578 setSplitMode_MainWindow(win, splitMode | noEvents_WindowSplit);
579 win->base.keyRoot = win->base.roots[keyRoot];
508 } 580 }
509 else if (!memcmp(magic, magicSidebar_App_, 4)) { 581 else if (!memcmp(magic, magicSidebar_App_, 4)) {
582 if (!win) {
583 printf("%s: missing window\n", cstr_String(path_File(f)));
584 setCurrent_Root(NULL);
585 return iFalse;
586 }
510 const uint16_t bits = readU16_File(f); 587 const uint16_t bits = readU16_File(f);
511 const uint8_t modes = readU8_File(f); 588 const uint8_t modes = readU8_File(f);
512 const float widths[2] = { 589 const float widths[2] = {
@@ -523,7 +600,7 @@ static iBool loadState_App_(iApp *d) {
523 } 600 }
524 const uint8_t rootIndex = bits & 0xff; 601 const uint8_t rootIndex = bits & 0xff;
525 const uint8_t flags = bits >> 8; 602 const uint8_t flags = bits >> 8;
526 iRoot *root = d->window->base.roots[rootIndex]; 603 iRoot *root = win->base.roots[rootIndex];
527 if (root) { 604 if (root) {
528 iSidebarWidget *sidebar = findChild_Widget(root->widget, "sidebar"); 605 iSidebarWidget *sidebar = findChild_Widget(root->widget, "sidebar");
529 iSidebarWidget *sidebar2 = findChild_Widget(root->widget, "sidebar2"); 606 iSidebarWidget *sidebar2 = findChild_Widget(root->widget, "sidebar2");
@@ -546,12 +623,18 @@ static iBool loadState_App_(iApp *d) {
546 } 623 }
547 } 624 }
548 else if (!memcmp(magic, magicTabDocument_App_, 4)) { 625 else if (!memcmp(magic, magicTabDocument_App_, 4)) {
626 if (!win) {
627 printf("%s: missing window\n", cstr_String(path_File(f)));
628 setCurrent_Root(NULL);
629 return iFalse;
630 }
549 const int8_t flags = read8_File(f); 631 const int8_t flags = read8_File(f);
550 int rootIndex = flags & rootIndex1_DocumentStateFlag ? 1 : 0; 632 int rootIndex = flags & rootIndex1_DocumentStateFlag ? 1 : 0;
551 if (rootIndex > numRoots_Window(as_Window(d->window)) - 1) { 633 if (rootIndex > numRoots_Window(as_Window(win)) - 1) {
552 rootIndex = 0; 634 rootIndex = 0;
553 } 635 }
554 setCurrent_Root(d->window->base.roots[rootIndex]); 636 setCurrent_Root(win->base.roots[rootIndex]);
637 iDocumentWidget *doc;
555 if (isFirstTab[rootIndex]) { 638 if (isFirstTab[rootIndex]) {
556 isFirstTab[rootIndex] = iFalse; 639 isFirstTab[rootIndex] = iFalse;
557 /* There is one pre-created tab in each root. */ 640 /* There is one pre-created tab in each root. */
@@ -561,7 +644,7 @@ static iBool loadState_App_(iApp *d) {
561 doc = newTab_App(NULL, iFalse /* no switching */); 644 doc = newTab_App(NULL, iFalse /* no switching */);
562 } 645 }
563 if (flags & current_DocumentStateFlag) { 646 if (flags & current_DocumentStateFlag) {
564 current[rootIndex] = doc; 647 value_Array(currentTabs, numWins - 1, iCurrentTabs).currentTab[rootIndex] = doc;
565 } 648 }
566 deserializeState_DocumentWidget(doc, stream_File(f)); 649 deserializeState_DocumentWidget(doc, stream_File(f));
567 doc = NULL; 650 doc = NULL;
@@ -572,12 +655,23 @@ static iBool loadState_App_(iApp *d) {
572 return iFalse; 655 return iFalse;
573 } 656 }
574 } 657 }
575 if (d->window->splitMode) { 658 iForEach(Array, i, currentTabs) {
576 /* Update root placement. */ 659 const iCurrentTabs *cur = i.value;
577 resize_MainWindow(d->window, -1, -1); 660 win = at_PtrArray(&d->mainWindows, index_ArrayIterator(&i));
661 for (size_t j = 0; j < 2; ++j) {
662 postCommandf_Root(win->base.roots[j], "tabs.switch page:%p", cur->currentTab[j]);
663 }
664 if (win->splitMode) {
665 /* Update root placement. */
666 resize_MainWindow(win, -1, -1);
667 }
668// postCommand_Root(win->base.roots[0], "window.unfreeze");
669 win->isDrawFrozen = iFalse;
670 SDL_ShowWindow(win->base.win);
578 } 671 }
579 iForIndices(i, current) { 672 if (numWindows_App() > 1) {
580 postCommandf_Root(NULL, "tabs.switch page:%p", current[i]); 673 SDL_RaiseWindow(currentWin->base.win);
674 setActiveWindow_App(currentWin);
581 } 675 }
582 setCurrent_Root(NULL); 676 setCurrent_Root(NULL);
583 return iTrue; 677 return iTrue;
@@ -588,7 +682,6 @@ static iBool loadState_App_(iApp *d) {
588static void saveState_App_(const iApp *d) { 682static void saveState_App_(const iApp *d) {
589 iUnused(d); 683 iUnused(d);
590 trimCache_App(); 684 trimCache_App();
591 iMainWindow *win = d->window;
592 /* UI state is saved in binary because it is quite complex (e.g., 685 /* UI state is saved in binary because it is quite complex (e.g.,
593 navigation history, cached content) and depends closely on the widget 686 navigation history, cached content) and depends closely on the widget
594 tree. The data is largely not reorderable and should not be modified 687 tree. The data is largely not reorderable and should not be modified
@@ -597,43 +690,48 @@ static void saveState_App_(const iApp *d) {
597 if (open_File(f, writeOnly_FileMode)) { 690 if (open_File(f, writeOnly_FileMode)) {
598 writeData_File(f, magicState_App_, 4); 691 writeData_File(f, magicState_App_, 4);
599 writeU32_File(f, latest_FileVersion); /* version */ 692 writeU32_File(f, latest_FileVersion); /* version */
600 /* Begin with window state. */ { 693 iConstForEach(PtrArray, winIter, &d->mainWindows) {
601 writeData_File(f, magicWindow_App_, 4); 694 const iMainWindow *win = winIter.ptr;
602 writeU32_File(f, win->splitMode); 695 setCurrent_Window(winIter.ptr);
603 writeU32_File(f, win->base.keyRoot == win->base.roots[0] ? 0 : 1); 696 /* Window state. */ {
604 } 697 writeData_File(f, magicWindow_App_, 4);
605 /* State of UI elements. */ { 698 writeU32_File(f, win->splitMode);
606 iForIndices(i, win->base.roots) { 699 writeU32_File(f, (win->base.keyRoot == win->base.roots[0] ? 0 : 1) |
607 const iRoot *root = win->base.roots[i]; 700 (win == d->window ? current_WindowStateFlag : 0));
608 if (root) { 701 }
609 writeData_File(f, magicSidebar_App_, 4); 702 /* State of UI elements. */ {
610 const iSidebarWidget *sidebar = findChild_Widget(root->widget, "sidebar"); 703 iForIndices(i, win->base.roots) {
611 const iSidebarWidget *sidebar2 = findChild_Widget(root->widget, "sidebar2"); 704 const iRoot *root = win->base.roots[i];
612 writeU16_File(f, i | 705 if (root) {
613 (isVisible_Widget(sidebar) ? 0x100 : 0) | 706 writeData_File(f, magicSidebar_App_, 4);
614 (isVisible_Widget(sidebar2) ? 0x200 : 0) | 707 const iSidebarWidget *sidebar = findChild_Widget(root->widget, "sidebar");
615 (feedsMode_SidebarWidget(sidebar) == unread_FeedsMode ? 0x400 : 0) | 708 const iSidebarWidget *sidebar2 = findChild_Widget(root->widget, "sidebar2");
616 (feedsMode_SidebarWidget(sidebar2) == unread_FeedsMode ? 0x800 : 0)); 709 writeU16_File(f, i |
617 writeU8_File(f, 710 (isVisible_Widget(sidebar) ? 0x100 : 0) |
618 mode_SidebarWidget(sidebar) | 711 (isVisible_Widget(sidebar2) ? 0x200 : 0) |
619 (mode_SidebarWidget(sidebar2) << 4)); 712 (feedsMode_SidebarWidget(sidebar) == unread_FeedsMode ? 0x400 : 0) |
620 writef_Stream(stream_File(f), width_SidebarWidget(sidebar)); 713 (feedsMode_SidebarWidget(sidebar2) == unread_FeedsMode ? 0x800 : 0));
621 writef_Stream(stream_File(f), width_SidebarWidget(sidebar2)); 714 writeU8_File(f,
622 serialize_IntSet(closedFolders_SidebarWidget(sidebar), stream_File(f)); 715 mode_SidebarWidget(sidebar) |
623 serialize_IntSet(closedFolders_SidebarWidget(sidebar2), stream_File(f)); 716 (mode_SidebarWidget(sidebar2) << 4));
717 writef_Stream(stream_File(f), width_SidebarWidget(sidebar));
718 writef_Stream(stream_File(f), width_SidebarWidget(sidebar2));
719 serialize_IntSet(closedFolders_SidebarWidget(sidebar), stream_File(f));
720 serialize_IntSet(closedFolders_SidebarWidget(sidebar2), stream_File(f));
721 }
624 } 722 }
625 } 723 }
626 } 724 iConstForEach(ObjectList, i, iClob(listDocuments_App(NULL))) {
627 iConstForEach(ObjectList, i, iClob(listDocuments_App(NULL))) { 725 iAssert(isInstance_Object(i.object, &Class_DocumentWidget));
628 iAssert(isInstance_Object(i.object, &Class_DocumentWidget)); 726 const iWidget *widget = constAs_Widget(i.object);
629 const iWidget *widget = constAs_Widget(i.object); 727 writeData_File(f, magicTabDocument_App_, 4);
630 writeData_File(f, magicTabDocument_App_, 4); 728 int8_t flags = (document_Root(widget->root) == i.object ? current_DocumentStateFlag : 0);
631 int8_t flags = (document_Root(widget->root) == i.object ? current_DocumentStateFlag : 0); 729 if (widget->root == win->base.roots[1]) {
632 if (widget->root == win->base.roots[1]) { 730 flags |= rootIndex1_DocumentStateFlag;
633 flags |= rootIndex1_DocumentStateFlag; 731 }
732 write8_File(f, flags);
733 serializeState_DocumentWidget(i.object, stream_File(f));
634 } 734 }
635 write8_File(f, flags);
636 serializeState_DocumentWidget(i.object, stream_File(f));
637 } 735 }
638 iRelease(f); 736 iRelease(f);
639 } 737 }
@@ -760,6 +858,7 @@ static void init_App_(iApp *d, int argc, char **argv) {
760 d->isDarkSystemTheme = iTrue; /* will be updated by system later on, if supported */ 858 d->isDarkSystemTheme = iTrue; /* will be updated by system later on, if supported */
761 d->isSuspended = iFalse; 859 d->isSuspended = iFalse;
762 d->tempFilesPendingDeletion = new_StringSet(); 860 d->tempFilesPendingDeletion = new_StringSet();
861 init_Array(&d->initialWindowRects, sizeof(iRect));
763 init_CommandLine(&d->args, argc, argv); 862 init_CommandLine(&d->args, argc, argv);
764 /* Where was the app started from? We ask SDL first because the command line alone 863 /* Where was the app started from? We ask SDL first because the command line alone
765 cannot be relied on (behavior differs depending on OS). */ { 864 cannot be relied on (behavior differs depending on OS). */ {
@@ -894,20 +993,6 @@ static void init_App_(iApp *d, int argc, char **argv) {
894 d->elapsedSinceLastTicker = 0; 993 d->elapsedSinceLastTicker = 0;
895 d->commandEcho = iClob(checkArgument_CommandLine(&d->args, "echo;E")) != NULL; 994 d->commandEcho = iClob(checkArgument_CommandLine(&d->args, "echo;E")) != NULL;
896 d->forceSoftwareRender = iClob(checkArgument_CommandLine(&d->args, "sw")) != NULL; 995 d->forceSoftwareRender = iClob(checkArgument_CommandLine(&d->args, "sw")) != NULL;
897 d->initialWindowRect = init_Rect(-1, -1, 900, 560);
898#if defined (iPlatformMsys)
899 /* Must scale by UI scaling factor. */
900 mulfv_I2(&d->initialWindowRect.size, desktopDPI_Win32());
901#endif
902#if defined (iPlatformLinux) && !defined (iPlatformAndroid)
903 /* Scale by the primary (?) monitor DPI. */
904 if (isRunningUnderWindowSystem_App()) {
905 float vdpi;
906 SDL_GetDisplayDPI(0, NULL, NULL, &vdpi);
907 const float factor = vdpi / 96.0f;
908 mulfv_I2(&d->initialWindowRect.size, iMax(factor, 1.0f));
909 }
910#endif
911 init_Prefs(&d->prefs); 996 init_Prefs(&d->prefs);
912 init_SiteSpec(dataDir_App_()); 997 init_SiteSpec(dataDir_App_());
913 setCStr_String(&d->prefs.strings[downloadDir_PrefsString], downloadDir_App_()); 998 setCStr_String(&d->prefs.strings[downloadDir_PrefsString], downloadDir_App_());
@@ -929,21 +1014,30 @@ static void init_App_(iApp *d, int argc, char **argv) {
929 init_Fonts(dataDir_App_()); 1014 init_Fonts(dataDir_App_());
930 loadPalette_Color(dataDir_App_()); 1015 loadPalette_Color(dataDir_App_());
931 setThemePalette_Color(d->prefs.theme); /* default UI colors */ 1016 setThemePalette_Color(d->prefs.theme); /* default UI colors */
932 loadPrefs_App_(d); 1017 /* Initial window rectangle of the first window. */ {
1018 iAssert(isEmpty_Array(&d->initialWindowRects));
1019 const iRect winRect = initialWindowRect_App_(d, 0); /* calculated */
1020 resize_Array(&d->initialWindowRects, 1);
1021 set_Array(&d->initialWindowRects, 0, &winRect);
1022 }
1023 loadPrefs_App_(d);
933 updateActive_Fonts(); 1024 updateActive_Fonts();
934 load_Keys(dataDir_App_()); 1025 load_Keys(dataDir_App_());
1026 iRect *winRect0 = at_Array(&d->initialWindowRects, 0);
935 /* See if the user wants to override the window size. */ { 1027 /* See if the user wants to override the window size. */ {
936 iCommandLineArg *arg = iClob(checkArgument_CommandLine(&d->args, windowWidth_CommandLineOption)); 1028 iCommandLineArg *arg = iClob(checkArgument_CommandLine(&d->args, windowWidth_CommandLineOption));
937 if (arg) { 1029 if (arg) {
938 d->initialWindowRect.size.x = toInt_String(value_CommandLineArg(arg, 0)); 1030 winRect0->size.x = toInt_String(value_CommandLineArg(arg, 0));
939 } 1031 }
940 arg = iClob(checkArgument_CommandLine(&d->args, windowHeight_CommandLineOption)); 1032 arg = iClob(checkArgument_CommandLine(&d->args, windowHeight_CommandLineOption));
941 if (arg) { 1033 if (arg) {
942 d->initialWindowRect.size.y = toInt_String(value_CommandLineArg(arg, 0)); 1034 winRect0->size.y = toInt_String(value_CommandLineArg(arg, 0));
943 } 1035 }
944 } 1036 }
1037 init_PtrArray(&d->mainWindows);
945 init_PtrArray(&d->popupWindows); 1038 init_PtrArray(&d->popupWindows);
946 d->window = new_MainWindow(d->initialWindowRect); 1039 d->window = new_MainWindow(*winRect0); /* first window is always created */
1040 addWindow_App(d->window);
947 load_Visited(d->visited, dataDir_App_()); 1041 load_Visited(d->visited, dataDir_App_());
948 load_Bookmarks(d->bookmarks, dataDir_App_()); 1042 load_Bookmarks(d->bookmarks, dataDir_App_());
949 load_MimeHooks(d->mimehooks, dataDir_App_()); 1043 load_MimeHooks(d->mimehooks, dataDir_App_());
@@ -968,8 +1062,8 @@ static void init_App_(iApp *d, int argc, char **argv) {
968 } 1062 }
969 postCommand_App("~navbar.actions.changed"); 1063 postCommand_App("~navbar.actions.changed");
970 postCommand_App("~toolbar.actions.changed"); 1064 postCommand_App("~toolbar.actions.changed");
971 postCommand_Root(NULL, "~window.unfreeze"); 1065 postCommand_App("~window.unfreeze");
972 postCommand_Root(NULL, "font.reset"); 1066 postCommand_App("font.reset");
973 d->autoReloadTimer = SDL_AddTimer(60 * 1000, postAutoReloadCommand_App_, NULL); 1067 d->autoReloadTimer = SDL_AddTimer(60 * 1000, postAutoReloadCommand_App_, NULL);
974 postCommand_Root(NULL, "document.autoreload"); 1068 postCommand_Root(NULL, "document.autoreload");
975#if defined (LAGRANGE_ENABLE_IDLE_SLEEP) 1069#if defined (LAGRANGE_ENABLE_IDLE_SLEEP)
@@ -1008,7 +1102,11 @@ static void deinit_App(iApp *d) {
1008 SDL_RemoveTimer(d->autoReloadTimer); 1102 SDL_RemoveTimer(d->autoReloadTimer);
1009 saveState_App_(d); 1103 saveState_App_(d);
1010 savePrefs_App_(d); 1104 savePrefs_App_(d);
1011 delete_MainWindow(d->window); 1105 iReverseForEach(PtrArray, j, &d->mainWindows) {
1106 delete_MainWindow(j.ptr);
1107 }
1108 iAssert(isEmpty_PtrArray(&d->mainWindows));
1109 deinit_PtrArray(&d->mainWindows);
1012 d->window = NULL; 1110 d->window = NULL;
1013 deinit_Feeds(); 1111 deinit_Feeds();
1014 save_Keys(dataDir_App_()); 1112 save_Keys(dataDir_App_());
@@ -1023,7 +1121,6 @@ static void deinit_App(iApp *d) {
1023 delete_GmCerts(d->certs); 1121 delete_GmCerts(d->certs);
1024 save_MimeHooks(d->mimehooks); 1122 save_MimeHooks(d->mimehooks);
1025 delete_MimeHooks(d->mimehooks); 1123 delete_MimeHooks(d->mimehooks);
1026 d->window = NULL;
1027 deinit_CommandLine(&d->args); 1124 deinit_CommandLine(&d->args);
1028 iRelease(d->launchCommands); 1125 iRelease(d->launchCommands);
1029 delete_String(d->execPath); 1126 delete_String(d->execPath);
@@ -1037,7 +1134,8 @@ static void deinit_App(iApp *d) {
1037 /* Delete all temporary files created while running. */ 1134 /* Delete all temporary files created while running. */
1038 iConstForEach(StringSet, tmp, d->tempFilesPendingDeletion) { 1135 iConstForEach(StringSet, tmp, d->tempFilesPendingDeletion) {
1039 remove(cstr_String(tmp.value)); 1136 remove(cstr_String(tmp.value));
1040} 1137 }
1138 deinit_Array(&d->initialWindowRects);
1041 iRelease(d->tempFilesPendingDeletion); 1139 iRelease(d->tempFilesPendingDeletion);
1042} 1140}
1043 1141
@@ -1282,10 +1380,23 @@ static iPtrArray *listWindows_App_(const iApp *d, iPtrArray *windows) {
1282 iReverseConstForEach(PtrArray, i, &d->popupWindows) { 1380 iReverseConstForEach(PtrArray, i, &d->popupWindows) {
1283 pushBack_PtrArray(windows, i.ptr); 1381 pushBack_PtrArray(windows, i.ptr);
1284 } 1382 }
1285 pushBack_PtrArray(windows, d->window); 1383 if (d->window) {
1384 pushBack_PtrArray(windows, d->window);
1385 }
1386 iConstForEach(PtrArray, j, &d->mainWindows) {
1387 if (j.ptr != d->window) {
1388 pushBack_PtrArray(windows, j.ptr);
1389 }
1390 }
1286 return windows; 1391 return windows;
1287} 1392}
1288 1393
1394iPtrArray *listWindows_App(void) {
1395 iPtrArray *wins = new_PtrArray();
1396 listWindows_App_(&app_, wins);
1397 return wins;
1398}
1399
1289void processEvents_App(enum iAppEventMode eventMode) { 1400void processEvents_App(enum iAppEventMode eventMode) {
1290 iApp *d = &app_; 1401 iApp *d = &app_;
1291 iRoot *oldCurrentRoot = current_Root(); /* restored afterwards */ 1402 iRoot *oldCurrentRoot = current_Root(); /* restored afterwards */
@@ -1365,6 +1476,10 @@ void processEvents_App(enum iAppEventMode eventMode) {
1365 dispatchCommands_Periodic(&d->periodic); 1476 dispatchCommands_Periodic(&d->periodic);
1366 continue; 1477 continue;
1367 } 1478 }
1479 if (ev.type == SDL_USEREVENT && ev.user.code == releaseObject_UserEventCode) {
1480 iRelease(ev.user.data1);
1481 continue;
1482 }
1368 if (ev.type == SDL_USEREVENT && ev.user.code == refresh_UserEventCode) { 1483 if (ev.type == SDL_USEREVENT && ev.user.code == refresh_UserEventCode) {
1369 gotRefresh = iTrue; 1484 gotRefresh = iTrue;
1370 continue; 1485 continue;
@@ -1470,6 +1585,7 @@ void processEvents_App(enum iAppEventMode eventMode) {
1470 window->lastHover = window->hover; 1585 window->lastHover = window->hover;
1471 wasUsed = processEvent_Window(window, &ev); 1586 wasUsed = processEvent_Window(window, &ev);
1472 if (ev.type == SDL_MOUSEMOTION || ev.type == SDL_MOUSEBUTTONDOWN) { 1587 if (ev.type == SDL_MOUSEMOTION || ev.type == SDL_MOUSEBUTTONDOWN) {
1588 /* Only offered to the frontmost window. */
1473 break; 1589 break;
1474 } 1590 }
1475 if (wasUsed) break; 1591 if (wasUsed) break;
@@ -1563,6 +1679,9 @@ static void runTickers_App_(iApp *d) {
1563 iConstForEach(Array, i, &pending->values) { 1679 iConstForEach(Array, i, &pending->values) {
1564 const iTicker *ticker = i.value; 1680 const iTicker *ticker = i.value;
1565 if (ticker->callback) { 1681 if (ticker->callback) {
1682 if (ticker->root) {
1683 setCurrent_Window(ticker->root->window);
1684 }
1566 setCurrent_Root(ticker->root); /* root might be NULL */ 1685 setCurrent_Root(ticker->root); /* root might be NULL */
1567 ticker->callback(ticker->context); 1686 ticker->callback(ticker->context);
1568 } 1687 }
@@ -1755,11 +1874,11 @@ void postCommand_Root(iRoot *d, const char *command) {
1755 } 1874 }
1756 SDL_Event ev = { .type = SDL_USEREVENT }; 1875 SDL_Event ev = { .type = SDL_USEREVENT };
1757 ev.user.code = command_UserEventCode; 1876 ev.user.code = command_UserEventCode;
1758 /*ev.user.windowID = id_Window(get_Window());*/
1759 ev.user.data1 = strdup(command); 1877 ev.user.data1 = strdup(command);
1760 ev.user.data2 = d; /* all events are root-specific */ 1878 ev.user.data2 = d; /* all events are root-specific */
1879 ev.user.windowID = d ? id_Window(d->window) : 0; /* root-specific means window-specific */
1761 SDL_PushEvent(&ev); 1880 SDL_PushEvent(&ev);
1762 iWindow *win = get_Window(); 1881 iWindow *win = d ? d->window : NULL;
1763#if defined (iPlatformAndroid) 1882#if defined (iPlatformAndroid)
1764 SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "%s[command] {%d} %s", 1883 SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "%s[command] {%d} %s",
1765 app_.isLoadingPrefs ? "[Prefs] " : "", 1884 app_.isLoadingPrefs ? "[Prefs] " : "",
@@ -1767,10 +1886,17 @@ void postCommand_Root(iRoot *d, const char *command) {
1767 command); 1886 command);
1768#else 1887#else
1769 if (app_.commandEcho) { 1888 if (app_.commandEcho) {
1770 printf("%s[command] {%d} %s\n", 1889 const int windowIndex =
1771 app_.isLoadingPrefs ? "[Prefs] " : "", 1890 win && type_Window(win) == main_WindowType ? windowIndex_App(as_MainWindow(win)) + 1 : 0;
1772 (d == NULL || win == NULL ? 0 : d == win->roots[0] ? 1 : 2), 1891 printf("%s%s[command] {%d:%d} %s\n",
1773 command); fflush(stdout); 1892 !app_.isFinishedLaunching ? "<Ln> " : "",
1893 app_.isLoadingPrefs ? "<Pr> " : "",
1894 windowIndex,
1895 (d == NULL || win == NULL ? 0
1896 : d == win->roots[0] ? 1
1897 : 2),
1898 command);
1899 fflush(stdout);
1774 } 1900 }
1775#endif 1901#endif
1776} 1902}
@@ -1835,6 +1961,41 @@ void removeTicker_App(iTickerFunc ticker, iAny *context) {
1835 remove_SortedArray(&d->tickers, &(iTicker){ context, NULL, ticker }); 1961 remove_SortedArray(&d->tickers, &(iTicker){ context, NULL, ticker });
1836} 1962}
1837 1963
1964void addWindow_App(iMainWindow *win) {
1965 iApp *d = &app_;
1966 pushBack_PtrArray(&d->mainWindows, win);
1967}
1968
1969void removeWindow_App(iMainWindow *win) {
1970 iApp *d = &app_;
1971 removeOne_PtrArray(&d->mainWindows, win);
1972}
1973
1974size_t numWindows_App(void) {
1975 return size_PtrArray(&app_.mainWindows);
1976}
1977
1978size_t windowIndex_App(const iMainWindow *win) {
1979 return indexOf_PtrArray(&app_.mainWindows, win);
1980}
1981
1982iMainWindow *newMainWindow_App(void) {
1983 iApp *d = &app_;
1984 iMainWindow *win = new_MainWindow(initialWindowRect_App_(d, size_PtrArray(&d->mainWindows)));
1985 addWindow_App(win);
1986 return win;
1987}
1988
1989const iPtrArray *mainWindows_App(void) {
1990 return &app_.mainWindows;
1991}
1992
1993void setActiveWindow_App(iMainWindow *win) {
1994 iApp *d = &app_;
1995 d->window = win;
1996 printf("Active window: %p\n", win); fflush(stdout);
1997}
1998
1838void addPopup_App(iWindow *popup) { 1999void addPopup_App(iWindow *popup) {
1839 iApp *d = &app_; 2000 iApp *d = &app_;
1840 pushBack_PtrArray(&d->popupWindows, popup); 2001 pushBack_PtrArray(&d->popupWindows, popup);
@@ -1970,6 +2131,8 @@ static iBool handlePrefsCommands_(iWidget *d, const char *cmd) {
1970 toInt_String(text_InputWidget(findChild_Widget(d, "prefs.cachesize")))); 2131 toInt_String(text_InputWidget(findChild_Widget(d, "prefs.cachesize"))));
1971 postCommandf_App("memorysize.set arg:%d", 2132 postCommandf_App("memorysize.set arg:%d",
1972 toInt_String(text_InputWidget(findChild_Widget(d, "prefs.memorysize")))); 2133 toInt_String(text_InputWidget(findChild_Widget(d, "prefs.memorysize"))));
2134 postCommandf_App("urlsize.set arg:%d",
2135 toInt_String(text_InputWidget(findChild_Widget(d, "prefs.urlsize"))));
1973 postCommandf_App("ca.file path:%s", 2136 postCommandf_App("ca.file path:%s",
1974 cstrText_InputWidget(findChild_Widget(d, "prefs.ca.file"))); 2137 cstrText_InputWidget(findChild_Widget(d, "prefs.ca.file")));
1975 postCommandf_App("ca.path path:%s", 2138 postCommandf_App("ca.path path:%s",
@@ -2111,6 +2274,22 @@ iDocumentWidget *newTab_App(const iDocumentWidget *duplicateOf, iBool switchToNe
2111 return doc; 2274 return doc;
2112} 2275}
2113 2276
2277void closeWindow_App(iMainWindow *win) {
2278 iApp *d = &app_;
2279 delete_MainWindow(win);
2280 if (d->window == win) {
2281 /* Activate another window. */
2282 iForEach(PtrArray, i, &d->mainWindows) {
2283 if (i.ptr != d->window) {
2284 SDL_RaiseWindow(i.ptr);
2285 setActiveWindow_App(i.ptr);
2286 setCurrent_Window(i.ptr);
2287 break;
2288 }
2289 }
2290 }
2291}
2292
2114static iBool handleIdentityCreationCommands_(iWidget *dlg, const char *cmd) { 2293static iBool handleIdentityCreationCommands_(iWidget *dlg, const char *cmd) {
2115 iApp *d = &app_; 2294 iApp *d = &app_;
2116 if (equal_Command(cmd, "ident.showmore")) { 2295 if (equal_Command(cmd, "ident.showmore")) {
@@ -2257,6 +2436,13 @@ void resetFonts_App(void) {
2257 } 2436 }
2258} 2437}
2259 2438
2439void availableFontsChanged_App(void) {
2440 iApp *d = &app_;
2441 iConstForEach(PtrArray, win, listWindows_App_(d, collectNew_PtrArray())) {
2442 resetMissing_Text(text_Window(win.ptr));
2443 }
2444}
2445
2260static void invalidateCachedDocuments_App_(void) { 2446static void invalidateCachedDocuments_App_(void) {
2261 iForEach(ObjectList, i, iClob(listDocuments_App(NULL))) { 2447 iForEach(ObjectList, i, iClob(listDocuments_App(NULL))) {
2262 invalidateCachedLayout_History(history_DocumentWidget(i.object)); 2448 invalidateCachedLayout_History(history_DocumentWidget(i.object));
@@ -2274,6 +2460,7 @@ iBool handleCommand_App(const char *cmd) {
2274 suffixPtr_Command(cmd, "where"))); 2460 suffixPtr_Command(cmd, "where")));
2275 return iTrue; 2461 return iTrue;
2276 } 2462 }
2463#if 0 /* disabled in v1.11 */
2277 else if (equal_Command(cmd, "fontpack.suggest.classic")) { 2464 else if (equal_Command(cmd, "fontpack.suggest.classic")) {
2278 /* TODO: Don't use this when system fonts are accessible. */ 2465 /* TODO: Don't use this when system fonts are accessible. */
2279 if (!isInstalled_Fonts("classic-set") && !isInstalled_Fonts("cjk")) { 2466 if (!isInstalled_Fonts("classic-set") && !isInstalled_Fonts("cjk")) {
@@ -2290,6 +2477,7 @@ iBool handleCommand_App(const char *cmd) {
2290 } 2477 }
2291 return iTrue; 2478 return iTrue;
2292 } 2479 }
2480#endif
2293 else if (equal_Command(cmd, "prefs.changed")) { 2481 else if (equal_Command(cmd, "prefs.changed")) {
2294 savePrefs_App_(d); 2482 savePrefs_App_(d);
2295 return iTrue; 2483 return iTrue;
@@ -2360,12 +2548,16 @@ iBool handleCommand_App(const char *cmd) {
2360 return iTrue; 2548 return iTrue;
2361 } 2549 }
2362 else if (equal_Command(cmd, "window.maximize")) { 2550 else if (equal_Command(cmd, "window.maximize")) {
2363 if (!argLabel_Command(cmd, "toggle")) { 2551 const size_t winIndex = argU32Label_Command(cmd, "index");
2364 setSnap_MainWindow(d->window, maximized_WindowSnap); 2552 if (winIndex < size_PtrArray(&d->mainWindows)) {
2365 } 2553 iMainWindow *win = at_PtrArray(&d->mainWindows, winIndex);
2366 else { 2554 if (!argLabel_Command(cmd, "toggle")) {
2367 setSnap_MainWindow(d->window, snap_MainWindow(d->window) == maximized_WindowSnap ? 0 : 2555 setSnap_MainWindow(win, maximized_WindowSnap);
2368 maximized_WindowSnap); 2556 }
2557 else {
2558 setSnap_MainWindow(
2559 win, snap_MainWindow(win) == maximized_WindowSnap ? 0 : maximized_WindowSnap);
2560 }
2369 } 2561 }
2370 return iTrue; 2562 return iTrue;
2371 } 2563 }
@@ -2383,24 +2575,49 @@ iBool handleCommand_App(const char *cmd) {
2383 reload_Fonts(); /* also does font cache reset, window invalidation */ 2575 reload_Fonts(); /* also does font cache reset, window invalidation */
2384 return iTrue; 2576 return iTrue;
2385 } 2577 }
2386#if 0 2578 else if (equal_Command(cmd, "font.find")) {
2387 else if (equal_Command(cmd, "font.user")) { 2579 searchOnlineLibraryForCharacters_Fonts(string_Command(cmd, "chars"));
2388 const char *path = suffixPtr_Command(cmd, "path"); 2580 return iTrue;
2389 if (cmp_String(&d->prefs.symbolFontPath, path)) { 2581 }
2390 if (!isFrozen) { 2582 else if (equal_Command(cmd, "font.found")) {
2391 setFreezeDraw_MainWindow(get_MainWindow(), iTrue); 2583 if (hasLabel_Command(cmd, "error")) {
2392 } 2584 makeSimpleMessage_Widget("${heading.glyphfinder}",
2393 setCStr_String(&d->prefs.symbolFontPath, path); 2585 format_CStr("%d %s",
2394 loadUserFonts_Text(); 2586 argLabel_Command(cmd, "error"),
2395 resetFonts_App(d); 2587 suffixPtr_Command(cmd, "msg")));
2396 if (!isFrozen) { 2588 return iTrue;
2397 postCommand_App("font.changed");
2398 postCommand_App("window.unfreeze");
2399 }
2400 } 2589 }
2590 iString *src = collectNew_String();
2591 setCStr_String(src, "# ${heading.glyphfinder.results}\n\n");
2592 iRangecc path = iNullRange;
2593 iBool isFirst = iTrue;
2594 while (nextSplit_Rangecc(range_Command(cmd, "packs"), ",", &path)) {
2595 if (isFirst) {
2596 appendCStr_String(src, "${glyphfinder.results}\n\n");
2597 }
2598 iRangecc fpath = path;
2599 iRangecc fsize = path;
2600 fpath.end = strchr(fpath.start, ';');
2601 fsize.start = fpath.end + 1;
2602 const uint32_t size = strtoul(fsize.start, NULL, 10);
2603 appendFormat_String(src, "=> gemini://skyjake.fi/fonts/%s %s (%.1f MB)\n",
2604 cstr_Rangecc(fpath),
2605 cstr_Rangecc(fpath),
2606 (double) size / 1.0e6);
2607 isFirst = iFalse;
2608 }
2609 if (isFirst) {
2610 appendFormat_String(src, "${glyphfinder.results.empty}\n");
2611 }
2612 appendCStr_String(src, "\n=> about:fonts ${menu.fonts}");
2613 iDocumentWidget *page = newTab_App(NULL, iTrue);
2614 translate_Lang(src);
2615 setUrlAndSource_DocumentWidget(page,
2616 collectNewCStr_String(""),
2617 collectNewCStr_String("text/gemini"),
2618 utf8_String(src));
2401 return iTrue; 2619 return iTrue;
2402 } 2620 }
2403#endif
2404 else if (equal_Command(cmd, "font.set")) { 2621 else if (equal_Command(cmd, "font.set")) {
2405 if (!isFrozen) { 2622 if (!isFrozen) {
2406 setFreezeDraw_MainWindow(get_MainWindow(), iTrue); 2623 setFreezeDraw_MainWindow(get_MainWindow(), iTrue);
@@ -2690,6 +2907,10 @@ iBool handleCommand_App(const char *cmd) {
2690 postRefresh_App(); 2907 postRefresh_App();
2691 return iTrue; 2908 return iTrue;
2692 } 2909 }
2910 else if (equal_Command(cmd, "prefs.dataurl.openimages.changed")) {
2911 d->prefs.openDataUrlImagesOnLoad = arg_Command(cmd) != 0;
2912 return iTrue;
2913 }
2693 else if (equal_Command(cmd, "prefs.archive.openindex.changed")) { 2914 else if (equal_Command(cmd, "prefs.archive.openindex.changed")) {
2694 d->prefs.openArchiveIndexPages = arg_Command(cmd) != 0; 2915 d->prefs.openArchiveIndexPages = arg_Command(cmd) != 0;
2695 return iTrue; 2916 return iTrue;
@@ -2735,6 +2956,13 @@ iBool handleCommand_App(const char *cmd) {
2735 } 2956 }
2736 return iTrue; 2957 return iTrue;
2737 } 2958 }
2959 else if (equal_Command(cmd, "urlsize.set")) {
2960 d->prefs.maxUrlSize = arg_Command(cmd);
2961 if (d->prefs.maxUrlSize < 1024) {
2962 d->prefs.maxUrlSize = 1024; /* Gemini protocol requirement */
2963 }
2964 return iTrue;
2965 }
2738 else if (equal_Command(cmd, "searchurl")) { 2966 else if (equal_Command(cmd, "searchurl")) {
2739 iString *url = &d->prefs.strings[searchUrl_PrefsString]; 2967 iString *url = &d->prefs.strings[searchUrl_PrefsString];
2740 setCStr_String(url, suffixPtr_Command(cmd, "address")); 2968 setCStr_String(url, suffixPtr_Command(cmd, "address"));
@@ -2817,7 +3045,18 @@ iBool handleCommand_App(const char *cmd) {
2817 return iTrue; /* invalid command */ 3045 return iTrue; /* invalid command */
2818 } 3046 }
2819 if (findWidget_App("prefs")) { 3047 if (findWidget_App("prefs")) {
2820 postCommand_App("prefs.dismiss"); 3048 postCommand_App("prefs.dismiss");
3049 }
3050 if (argLabel_Command(cmd, "newwindow")) {
3051 const iRangecc gotoheading = range_Command(cmd, "gotoheading");
3052 const iRangecc gotourlheading = range_Command(cmd, "gotourlheading");
3053 postCommandf_Root(get_Root(), "window.new%s%s%s%s url:%s",
3054 isEmpty_Range(&gotoheading) ? "" : " gotoheading:",
3055 isEmpty_Range(&gotoheading) ? "" : cstr_Rangecc(gotoheading),
3056 isEmpty_Range(&gotourlheading) ? "" : " gotourlheading:",
3057 isEmpty_Range(&gotourlheading) ? "" : cstr_Rangecc(gotourlheading),
3058 urlArg);
3059 return iTrue;
2821 } 3060 }
2822 iString *url = collectNewCStr_String(urlArg); 3061 iString *url = collectNewCStr_String(urlArg);
2823 const iBool noProxy = argLabel_Command(cmd, "noproxy") != 0; 3062 const iBool noProxy = argLabel_Command(cmd, "noproxy") != 0;
@@ -2961,6 +3200,20 @@ iBool handleCommand_App(const char *cmd) {
2961#endif 3200#endif
2962 return iFalse; 3201 return iFalse;
2963 } 3202 }
3203 else if (equal_Command(cmd, "window.new")) {
3204 iMainWindow *newWin = new_MainWindow(initialWindowRect_App_(d, numWindows_App()));
3205 addWindow_App(newWin); /* takes ownership */
3206 SDL_ShowWindow(newWin->base.win);
3207 setCurrent_Window(newWin);
3208 if (hasLabel_Command(cmd, "url")) {
3209 postCommandf_Root(newWin->base.roots[0], "~open %s", cmd + 11 /* all arguments passed on */);
3210 }
3211 else {
3212 postCommand_Root(newWin->base.roots[0], "~navigate.home");
3213 }
3214 postCommand_Root(newWin->base.roots[0], "~window.unfreeze");
3215 return iTrue;
3216 }
2964 else if (equal_Command(cmd, "tabs.new")) { 3217 else if (equal_Command(cmd, "tabs.new")) {
2965 const iBool isDuplicate = argLabel_Command(cmd, "duplicate") != 0; 3218 const iBool isDuplicate = argLabel_Command(cmd, "duplicate") != 0;
2966 newTab_App(isDuplicate ? document_App() : NULL, iTrue); 3219 newTab_App(isDuplicate ? document_App() : NULL, iTrue);
@@ -3021,6 +3274,9 @@ iBool handleCommand_App(const char *cmd) {
3021 } 3274 }
3022 } 3275 }
3023 } 3276 }
3277 else if (numWindows_App() > 1) {
3278 closeWindow_App(d->window);
3279 }
3024 else { 3280 else {
3025 postCommand_App("quit"); 3281 postCommand_App("quit");
3026 } 3282 }
@@ -3050,6 +3306,7 @@ iBool handleCommand_App(const char *cmd) {
3050 setToggle_Widget(findChild_Widget(dlg, "prefs.hidetoolbarscroll"), d->prefs.hideToolbarOnScroll); 3306 setToggle_Widget(findChild_Widget(dlg, "prefs.hidetoolbarscroll"), d->prefs.hideToolbarOnScroll);
3051 setToggle_Widget(findChild_Widget(dlg, "prefs.bookmarks.addbottom"), d->prefs.addBookmarksToBottom); 3307 setToggle_Widget(findChild_Widget(dlg, "prefs.bookmarks.addbottom"), d->prefs.addBookmarksToBottom);
3052 setToggle_Widget(findChild_Widget(dlg, "prefs.font.warnmissing"), d->prefs.warnAboutMissingGlyphs); 3308 setToggle_Widget(findChild_Widget(dlg, "prefs.font.warnmissing"), d->prefs.warnAboutMissingGlyphs);
3309 setToggle_Widget(findChild_Widget(dlg, "prefs.dataurl.openimages"), d->prefs.openDataUrlImagesOnLoad);
3053 setToggle_Widget(findChild_Widget(dlg, "prefs.archive.openindex"), d->prefs.openArchiveIndexPages); 3310 setToggle_Widget(findChild_Widget(dlg, "prefs.archive.openindex"), d->prefs.openArchiveIndexPages);
3054 setToggle_Widget(findChild_Widget(dlg, "prefs.ostheme"), d->prefs.useSystemTheme); 3311 setToggle_Widget(findChild_Widget(dlg, "prefs.ostheme"), d->prefs.useSystemTheme);
3055 setToggle_Widget(findChild_Widget(dlg, "prefs.customframe"), d->prefs.customFrame); 3312 setToggle_Widget(findChild_Widget(dlg, "prefs.customframe"), d->prefs.customFrame);
@@ -3122,6 +3379,8 @@ iBool handleCommand_App(const char *cmd) {
3122 collectNewFormat_String("%d", d->prefs.maxCacheSize)); 3379 collectNewFormat_String("%d", d->prefs.maxCacheSize));
3123 setText_InputWidget(findChild_Widget(dlg, "prefs.memorysize"), 3380 setText_InputWidget(findChild_Widget(dlg, "prefs.memorysize"),
3124 collectNewFormat_String("%d", d->prefs.maxMemorySize)); 3381 collectNewFormat_String("%d", d->prefs.maxMemorySize));
3382 setText_InputWidget(findChild_Widget(dlg, "prefs.urlsize"),
3383 collectNewFormat_String("%d", d->prefs.maxUrlSize));
3125 setToggle_Widget(findChild_Widget(dlg, "prefs.decodeurls"), d->prefs.decodeUserVisibleURLs); 3384 setToggle_Widget(findChild_Widget(dlg, "prefs.decodeurls"), d->prefs.decodeUserVisibleURLs);
3126 setText_InputWidget(findChild_Widget(dlg, "prefs.searchurl"), &d->prefs.strings[searchUrl_PrefsString]); 3385 setText_InputWidget(findChild_Widget(dlg, "prefs.searchurl"), &d->prefs.strings[searchUrl_PrefsString]);
3127 setText_InputWidget(findChild_Widget(dlg, "prefs.ca.file"), &d->prefs.strings[caFile_PrefsString]); 3386 setText_InputWidget(findChild_Widget(dlg, "prefs.ca.file"), &d->prefs.strings[caFile_PrefsString]);
@@ -3509,21 +3768,7 @@ void revealPath_App(const iString *path) {
3509} 3768}
3510 3769
3511iObjectList *listDocuments_App(const iRoot *rootOrNull) { 3770iObjectList *listDocuments_App(const iRoot *rootOrNull) {
3512 iWindow *win = get_Window(); 3771 return listDocuments_MainWindow(get_MainWindow(), rootOrNull);
3513 iObjectList *docs = new_ObjectList();
3514 iForIndices(i, win->roots) {
3515 iRoot *root = win->roots[i];
3516 if (!root) continue;
3517 if (!rootOrNull || root == rootOrNull) {
3518 const iWidget *tabs = findChild_Widget(root->widget, "doctabs");
3519 iForEach(ObjectList, i, children_Widget(findChild_Widget(tabs, "tabs.pages"))) {
3520 if (isInstance_Object(i.object, &Class_DocumentWidget)) {
3521 pushBack_ObjectList(docs, i.object);
3522 }
3523 }
3524 }
3525 }
3526 return docs;
3527} 3772}
3528 3773
3529iStringSet *listOpenURLs_App(void) { 3774iStringSet *listOpenURLs_App(void) {
@@ -3540,12 +3785,16 @@ iMainWindow *mainWindow_App(void) {
3540 return app_.window; 3785 return app_.window;
3541} 3786}
3542 3787
3543void closePopups_App(void) { 3788void closePopups_App(iBool doForce) {
3544 iApp *d = &app_; 3789 iApp *d = &app_;
3545 const uint32_t now = SDL_GetTicks(); 3790 const uint32_t now = SDL_GetTicks();
3546 iConstForEach(PtrArray, i, &d->popupWindows) { 3791 iForEach(PtrArray, i, &d->popupWindows) {
3547 const iWindow *win = i.ptr; 3792 iWindow *win = i.ptr;
3548 if (now - win->focusGainedAt > 200) { 3793// if (doForce) {
3794// collect_Garbage(win, (iDeleteFunc) delete_Window);
3795// }
3796// else
3797 if (now - win->focusGainedAt > 200) {
3549 postCommand_Root(((const iWindow *) i.ptr)->roots[0], "cancel"); 3798 postCommand_Root(((const iWindow *) i.ptr)->roots[0], "cancel");
3550 } 3799 }
3551 } 3800 }
diff --git a/src/app.h b/src/app.h
index 5968de0d..5f7b506f 100644
--- a/src/app.h
+++ b/src/app.h
@@ -67,6 +67,7 @@ enum iUserEventCode {
67 take, it could turn into a tap-and-hold for example. */ 67 take, it could turn into a tap-and-hold for example. */
68 widgetTapBegins_UserEventCode, 68 widgetTapBegins_UserEventCode,
69 widgetTouchEnds_UserEventCode, /* finger lifted, but momentum may continue */ 69 widgetTouchEnds_UserEventCode, /* finger lifted, but momentum may continue */
70 releaseObject_UserEventCode, /* object that needs releasing in the main thread */
70}; 71};
71 72
72const iString *execPath_App (void); 73const iString *execPath_App (void);
@@ -97,6 +98,7 @@ iPeriodic * periodic_App (void);
97iDocumentWidget * document_App (void); 98iDocumentWidget * document_App (void);
98iObjectList * listDocuments_App (const iRoot *rootOrNull); /* NULL for all roots */ 99iObjectList * listDocuments_App (const iRoot *rootOrNull); /* NULL for all roots */
99iStringSet * listOpenURLs_App (void); /* all tabs */ 100iStringSet * listOpenURLs_App (void); /* all tabs */
101iPtrArray * listWindows_App (void);
100iDocumentWidget * newTab_App (const iDocumentWidget *duplicateOf, iBool switchToNew); 102iDocumentWidget * newTab_App (const iDocumentWidget *duplicateOf, iBool switchToNew);
101void trimCache_App (void); 103void trimCache_App (void);
102void trimMemory_App (void); 104void trimMemory_App (void);
@@ -121,6 +123,14 @@ iAny * findWidget_App (const char *id);
121void addTicker_App (iTickerFunc ticker, iAny *context); 123void addTicker_App (iTickerFunc ticker, iAny *context);
122void addTickerRoot_App (iTickerFunc ticker, iRoot *root, iAny *context); 124void addTickerRoot_App (iTickerFunc ticker, iRoot *root, iAny *context);
123void removeTicker_App (iTickerFunc ticker, iAny *context); 125void removeTicker_App (iTickerFunc ticker, iAny *context);
126void addWindow_App (iMainWindow *win);
127void removeWindow_App (iMainWindow *win);
128void setActiveWindow_App (iMainWindow *win);
129void closeWindow_App (iMainWindow *win);
130size_t numWindows_App (void);
131size_t windowIndex_App (const iMainWindow *win);
132iMainWindow *newMainWindow_App (void);
133const iPtrArray *mainWindows_App(void);
124void addPopup_App (iWindow *popup); 134void addPopup_App (iWindow *popup);
125void removePopup_App (iWindow *popup); 135void removePopup_App (iWindow *popup);
126void postRefresh_App (void); 136void postRefresh_App (void);
@@ -142,6 +152,7 @@ iDocumentWidget * document_Command (const char *cmd);
142void openInDefaultBrowser_App(const iString *url); 152void openInDefaultBrowser_App(const iString *url);
143void revealPath_App (const iString *path); 153void revealPath_App (const iString *path);
144void resetFonts_App (void); 154void resetFonts_App (void);
155void availableFontsChanged_App(void);
145 156
146iMainWindow * mainWindow_App (void); 157iMainWindow * mainWindow_App (void);
147void closePopups_App (void); 158void closePopups_App (iBool doForce);
diff --git a/src/defs.h b/src/defs.h
index e2edd100..c3480bc2 100644
--- a/src/defs.h
+++ b/src/defs.h
@@ -38,9 +38,10 @@ enum iFileVersion {
38 serializedSidebarState_FileVersion = 3, 38 serializedSidebarState_FileVersion = 3,
39 addedRecentUrlFlags_FileVersion = 4, 39 addedRecentUrlFlags_FileVersion = 4,
40 bookmarkFolderState_FileVersion = 5, 40 bookmarkFolderState_FileVersion = 5,
41 multipleWindows_FileVersion = 6,
41 /* meta */ 42 /* meta */
42 idents_FileVersion = 1, /* version used by GmCerts/idents.lgr */ 43 latest_FileVersion = 6, /* used by state.lgr */
43 latest_FileVersion = 5, 44 idents_FileVersion = 1, /* used by GmCerts/idents.lgr */
44}; 45};
45 46
46enum iImageStyle { 47enum iImageStyle {
@@ -156,8 +157,9 @@ iLocalDef int acceptKeyMod_ReturnKeyBehavior(int behavior) {
156#define bookmark_Icon "\U0001f516" 157#define bookmark_Icon "\U0001f516"
157#define folder_Icon "\U0001f4c1" 158#define folder_Icon "\U0001f4c1"
158#define file_Icon "\U0001f5ce" 159#define file_Icon "\U0001f5ce"
159#define openTab_Icon "\u2750" 160#define openWindow_Icon "\u2b1a" //"\U0001F5d4"
160#define openTabBg_Icon "\u2b1a" 161#define openTab_Icon add_Icon
162#define openTabBg_Icon "\u2750" //"\u2b1a"
161#define openExt_Icon "\u27a0" 163#define openExt_Icon "\u27a0"
162#define add_Icon "\u2795" 164#define add_Icon "\u2795"
163#define page_Icon "\U00010117" 165#define page_Icon "\U00010117"
@@ -186,6 +188,8 @@ iLocalDef int acceptKeyMod_ReturnKeyBehavior(int behavior) {
186#define downAngle_Icon "\ufe40" 188#define downAngle_Icon "\ufe40"
187#define photo_Icon "\U0001f5bc" 189#define photo_Icon "\U0001f5bc"
188#define fontpack_Icon "\U0001f520" 190#define fontpack_Icon "\U0001f520"
191#define package_Icon "\U0001f4e6"
192#define paperclip_Icon "\U0001f4ce"
189 193
190#if defined (iPlatformApple) 194#if defined (iPlatformApple)
191# define shift_Icon "\u21e7" 195# define shift_Icon "\u21e7"
diff --git a/src/fontpack.c b/src/fontpack.c
index a440234e..96006226 100644
--- a/src/fontpack.c
+++ b/src/fontpack.c
@@ -23,6 +23,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
23#include "fontpack.h" 23#include "fontpack.h"
24#include "resources.h" 24#include "resources.h"
25#include "ui/window.h" 25#include "ui/window.h"
26#include "gmrequest.h"
26#include "app.h" 27#include "app.h"
27 28
28#include <the_Foundation/archive.h> 29#include <the_Foundation/archive.h>
@@ -748,7 +749,7 @@ const iPtrArray *listSpecsByPriority_Fonts(void) {
748 return &fonts_.specOrder; 749 return &fonts_.specOrder;
749} 750}
750 751
751iString *infoText_FontPack(const iFontPack *d) { 752iString *infoText_FontPack(const iFontPack *d, iBool isFull) {
752 const iFontPack *installed = pack_Fonts(cstr_String(&d->id)); 753 const iFontPack *installed = pack_Fonts(cstr_String(&d->id));
753 const iBool isInstalled = (installed != NULL); 754 const iBool isInstalled = (installed != NULL);
754 const int installedVersion = installed ? installed->version : 0; 755 const int installedVersion = installed ? installed->version : 0;
@@ -757,9 +758,17 @@ iString *infoText_FontPack(const iFontPack *d) {
757 size_t sizeInBytes = 0; 758 size_t sizeInBytes = 0;
758 iPtrSet *uniqueFiles = new_PtrSet(); 759 iPtrSet *uniqueFiles = new_PtrSet();
759 iStringList *names = new_StringList(); 760 iStringList *names = new_StringList();
761 size_t numNames = 0;
762 iBool isAbbreviated = iFalse;
760 iConstForEach(PtrArray, i, listSpecs_FontPack(d)) { 763 iConstForEach(PtrArray, i, listSpecs_FontPack(d)) {
761 const iFontSpec *spec = i.ptr; 764 const iFontSpec *spec = i.ptr;
762 pushBack_StringList(names, &spec->name); 765 numNames++;
766 if (isFull || size_StringList(names) < 20) {
767 pushBack_StringList(names, &spec->name);
768 }
769 else {
770 isAbbreviated = iTrue;
771 }
763 iForIndices(j, spec->styles) { 772 iForIndices(j, spec->styles) {
764 insert_PtrSet(uniqueFiles, spec->styles[j]->sourceData.i); 773 insert_PtrSet(uniqueFiles, spec->styles[j]->sourceData.i);
765 } 774 }
@@ -777,11 +786,12 @@ iString *infoText_FontPack(const iFontPack *d) {
777 if (!endsWith_String(str, "(")) { 786 if (!endsWith_String(str, "(")) {
778 appendCStr_String(str, ", "); 787 appendCStr_String(str, ", ");
779 } 788 }
780 appendCStr_String(str, formatCStrs_Lang("num.fonts.n", size_StringList(names))); 789 appendCStr_String(str, formatCStrs_Lang("num.fonts.n", numNames));
781 } 790 }
782 appendFormat_String(str, ")"); 791 appendFormat_String(str, ")");
783 } 792 }
784 appendFormat_String(str, " \u2014 %s\n", cstrCollect_String(joinCStr_StringList(names, ", "))); 793 appendFormat_String(str, " \u2014 %s%s\n", cstrCollect_String(joinCStr_StringList(names, ", ")),
794 isAbbreviated ? ", ..." : "");
785 if (isInstalled && installedVersion != d->version) { 795 if (isInstalled && installedVersion != d->version) {
786 appendCStr_String(str, format_Lang("${fontpack.meta.version}\n", d->version)); 796 appendCStr_String(str, format_Lang("${fontpack.meta.version}\n", d->version));
787 } 797 }
@@ -945,7 +955,7 @@ const iString *infoPage_Fonts(iRangecc query) {
945 appendFormat_String(str, "### %s\n", 955 appendFormat_String(str, "### %s\n",
946 isEmpty_String(packId) ? "fonts.ini" : 956 isEmpty_String(packId) ? "fonts.ini" :
947 cstr_String(packId)); 957 cstr_String(packId));
948 append_String(str, collect_String(infoText_FontPack(pack))); 958 append_String(str, collect_String(infoText_FontPack(pack, iFalse)));
949 appendFormat_String(str, "=> %s ${fontpack.meta.viewfile}\n", 959 appendFormat_String(str, "=> %s ${fontpack.meta.viewfile}\n",
950 cstrCollect_String(makeFileUrl_String(&spec->sourcePath))); 960 cstrCollect_String(makeFileUrl_String(&spec->sourcePath)));
951 if (pack->isStandalone) { 961 if (pack->isStandalone) {
@@ -1018,6 +1028,7 @@ void install_Fonts(const iString *packId, const iBlock *data) {
1018 iRelease(f); 1028 iRelease(f);
1019 /* Newly installed fontpacks may have a higher priority that overrides other fonts. */ 1029 /* Newly installed fontpacks may have a higher priority that overrides other fonts. */
1020 reload_Fonts(); 1030 reload_Fonts();
1031 availableFontsChanged_App();
1021} 1032}
1022 1033
1023void installFontFile_Fonts(const iString *fileName, const iBlock *data) { 1034void installFontFile_Fonts(const iString *fileName, const iBlock *data) {
@@ -1028,6 +1039,7 @@ void installFontFile_Fonts(const iString *fileName, const iBlock *data) {
1028 } 1039 }
1029 iRelease(f); 1040 iRelease(f);
1030 reload_Fonts(); 1041 reload_Fonts();
1042 availableFontsChanged_App();
1031} 1043}
1032 1044
1033void enablePack_Fonts(const iString *packId, iBool enable) { 1045void enablePack_Fonts(const iString *packId, iBool enable) {
@@ -1040,6 +1052,7 @@ void enablePack_Fonts(const iString *packId, iBool enable) {
1040 } 1052 }
1041 updateActive_Fonts(); 1053 updateActive_Fonts();
1042 resetFonts_App(); 1054 resetFonts_App();
1055 availableFontsChanged_App();
1043 invalidate_Window(get_MainWindow()); 1056 invalidate_Window(get_MainWindow());
1044} 1057}
1045 1058
@@ -1047,5 +1060,100 @@ void updateActive_Fonts(void) {
1047 sortSpecs_Fonts_(&fonts_); 1060 sortSpecs_Fonts_(&fonts_);
1048} 1061}
1049 1062
1050iDefineClass(FontFile) 1063static void findCharactersInCMap_(iGmRequest *d, iGmRequest *req) {
1064 /* Note: Called in background thread. */
1065 iUnused(req);
1066 const iString *missingChars = userData_Object(d);
1067 if (isSuccess_GmStatusCode(status_GmRequest(d))) {
1068 iStringList *matchingPacks = new_StringList();
1069 iChar needed[20];
1070 iChar minChar = UINT32_MAX, maxChar = 0;
1071 size_t numNeeded = 0;
1072 iConstForEach(String, ch, missingChars) {
1073 needed[numNeeded++] = ch.value;
1074 minChar = iMin(minChar, ch.value);
1075 maxChar = iMax(maxChar, ch.value);
1076 if (numNeeded == iElemCount(needed)) {
1077 /* Shouldn't be that many. */
1078 break;
1079 }
1080 }
1081 iBlock *data = decompressGzip_Block(body_GmRequest(d));
1082 iRangecc line = iNullRange;
1083 while (nextSplit_Rangecc(range_Block(data), "\n", &line)) {
1084 iRangecc fontpackPath = iNullRange;
1085 for (const char *pos = line.start; pos < line.end; pos++) {
1086 if (*pos == ':') {
1087 fontpackPath.start = line.start;
1088 fontpackPath.end = pos;
1089 line.start = pos + 1;
1090 trimStart_Rangecc(&line);
1091 break;
1092 }
1093 }
1094 if (fontpackPath.start) {
1095 /* Parse the character ranges and see if any match what we need. */
1096 const char *pos = line.start;
1097 while (pos < line.end) {
1098 char *endp;
1099 uint32_t first = strtoul(pos, &endp, 10);
1100 uint32_t last = first;
1101 if (*endp == '-') {
1102 last = strtoul(endp + 1, &endp, 10);
1103 }
1104 if (maxChar < first) {
1105 break; /* The rest are even higher. */
1106 }
1107 if (minChar <= last) {
1108 for (size_t i = 0; i < numNeeded; i++) {
1109 if (needed[i] >= first && needed[i] <= last) {
1110 /* Got it. */
1111 pushBackRange_StringList(matchingPacks, fontpackPath);
1112 break;
1113 }
1114 }
1115 }
1116 pos = endp + 1;
1117 }
1118 }
1119 }
1120 delete_Block(data);
1121 iString result;
1122 init_String(&result);
1123 format_String(&result, "font.found chars:%s packs:", cstr_String(missingChars));
1124 iConstForEach(StringList, s, matchingPacks) {
1125 if (s.pos != 0) {
1126 appendCStr_String(&result, ",");
1127 }
1128 append_String(&result, s.value);
1129 }
1130 postCommandString_Root(NULL, &result);
1131 deinit_String(&result);
1132 iRelease(matchingPacks);
1133 }
1134 else {
1135 /* Report error. */
1136 postCommandf_Root(NULL,
1137 "font.found chars:%s error:%d msg:\x1b[1m%s\x1b[0m\n%s",
1138 cstr_String(missingChars),
1139 status_GmRequest(d),
1140 cstr_String(meta_GmRequest(d)),
1141 cstr_String(url_GmRequest(d)));
1142 }
1143// fflush(stdout);
1144 delete_String(userData_Object(d));
1145 /* We can't delete ourselves; threads must be joined from another thread. */
1146 SDL_PushEvent((SDL_Event *) &(SDL_UserEvent){
1147 .type = SDL_USEREVENT, .code = releaseObject_UserEventCode, .data1 = d });
1148}
1051 1149
1150void searchOnlineLibraryForCharacters_Fonts(const iString *chars) {
1151 /* Fetch the character map from skyjake.fi. */
1152 iGmRequest *req = new_GmRequest(certs_App());
1153 setUrl_GmRequest(req, collectNewCStr_String("gemini://skyjake.fi/fonts/cmap.txt.gz"));
1154 setUserData_Object(req, copy_String(chars));
1155 iConnect(GmRequest, req, finished, req, findCharactersInCMap_);
1156 submit_GmRequest(req);
1157}
1158
1159iDefineClass(FontFile)
diff --git a/src/fontpack.h b/src/fontpack.h
index aa6f2b9f..f6d4d483 100644
--- a/src/fontpack.h
+++ b/src/fontpack.h
@@ -160,7 +160,7 @@ const iString * loadPath_FontPack (const iFontPack *); /* may return N
160iBool isDisabled_FontPack (const iFontPack *); 160iBool isDisabled_FontPack (const iFontPack *);
161iBool isReadOnly_FontPack (const iFontPack *); 161iBool isReadOnly_FontPack (const iFontPack *);
162const iPtrArray * listSpecs_FontPack (const iFontPack *); 162const iPtrArray * listSpecs_FontPack (const iFontPack *);
163iString * infoText_FontPack (const iFontPack *); 163iString * infoText_FontPack (const iFontPack *, iBool isFull);
164const iArray * actions_FontPack (const iFontPack *, iBool showInstalled); 164const iArray * actions_FontPack (const iFontPack *, iBool showInstalled);
165 165
166const iString * idFromUrl_FontPack (const iString *url); 166const iString * idFromUrl_FontPack (const iString *url);
@@ -186,3 +186,5 @@ void reload_Fonts (void);
186iLocalDef iBool isInstalled_Fonts(const char *packId) { 186iLocalDef iBool isInstalled_Fonts(const char *packId) {
187 return pack_Fonts(packId) != NULL; 187 return pack_Fonts(packId) != NULL;
188} 188}
189
190void searchOnlineLibraryForCharacters_Fonts (const iString *chars);
diff --git a/src/gmdocument.c b/src/gmdocument.c
index 5ff8c72f..a85e4b71 100644
--- a/src/gmdocument.c
+++ b/src/gmdocument.c
@@ -333,13 +333,14 @@ static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *li
333 link->urlRange = capturedRange_RegExpMatch(&m, 1); 333 link->urlRange = capturedRange_RegExpMatch(&m, 1);
334 setRange_String(&link->url, link->urlRange); 334 setRange_String(&link->url, link->urlRange);
335 set_String(&link->url, canonicalUrl_String(absoluteUrl_String(&d->url, &link->url))); 335 set_String(&link->url, canonicalUrl_String(absoluteUrl_String(&d->url, &link->url)));
336 if (startsWithCase_String(&link->url, "about:command")) { 336 /* If invalid, disregard the link. */
337 /* This is a special internal page that allows submitting UI events. */ 337 if ((d->format == gemini_SourceFormat && size_String(&link->url) > prefs_App()->maxUrlSize) ||
338 if (!d->enableCommandLinks) { 338 (startsWithCase_String(&link->url, "about:command")
339 delete_GmLink(link); 339 /* this is a special internal page that allows submitting UI events */
340 *linkId = 0; 340 && !d->enableCommandLinks)) {
341 return line; 341 delete_GmLink(link);
342 } 342 *linkId = 0;
343 return line;
343 } 344 }
344 /* Check the URL. */ { 345 /* Check the URL. */ {
345 iUrl parts; 346 iUrl parts;
@@ -370,6 +371,13 @@ static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *li
370 } 371 }
371 else if (equalCase_Rangecc(parts.scheme, "data")) { 372 else if (equalCase_Rangecc(parts.scheme, "data")) {
372 setScheme_GmLink_(link, data_GmLinkScheme); 373 setScheme_GmLink_(link, data_GmLinkScheme);
374 if (startsWith_Rangecc(parts.path, "image/png") ||
375 startsWith_Rangecc(parts.path, "image/jpg") ||
376 startsWith_Rangecc(parts.path, "image/jpeg") ||
377 startsWith_Rangecc(parts.path, "image/webp") ||
378 startsWith_Rangecc(parts.path, "image/gif")) {
379 link->flags |= imageFileExtension_GmLinkFlag;
380 }
373 } 381 }
374 else if (equalCase_Rangecc(parts.scheme, "about")) { 382 else if (equalCase_Rangecc(parts.scheme, "about")) {
375 setScheme_GmLink_(link, about_GmLinkScheme); 383 setScheme_GmLink_(link, about_GmLinkScheme);
@@ -382,7 +390,7 @@ static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *li
382 iString *path = newRange_String(parts.path); 390 iString *path = newRange_String(parts.path);
383 if (endsWithCase_String(path, ".gif") || endsWithCase_String(path, ".jpg") || 391 if (endsWithCase_String(path, ".gif") || endsWithCase_String(path, ".jpg") ||
384 endsWithCase_String(path, ".jpeg") || endsWithCase_String(path, ".png") || 392 endsWithCase_String(path, ".jpeg") || endsWithCase_String(path, ".png") ||
385 endsWithCase_String(path, ".tga") || endsWithCase_String(path, ".psd") || 393 endsWithCase_String(path, ".tga") || endsWithCase_String(path, ".psd") ||
386#if defined (LAGRANGE_ENABLE_WEBP) 394#if defined (LAGRANGE_ENABLE_WEBP)
387 endsWithCase_String(path, ".webp") || 395 endsWithCase_String(path, ".webp") ||
388#endif 396#endif
@@ -483,8 +491,7 @@ static iBool isNormalized_GmDocument_(const iGmDocument *d) {
483} 491}
484 492
485static enum iGmDocumentTheme currentTheme_(void) { 493static enum iGmDocumentTheme currentTheme_(void) {
486 return (isDark_ColorTheme(colorTheme_App()) ? prefs_App()->docThemeDark 494 return docTheme_Prefs(prefs_App());
487 : prefs_App()->docThemeLight);
488} 495}
489 496
490static void alignDecoration_GmRun_(iGmRun *run, iBool isCentered) { 497static void alignDecoration_GmRun_(iGmRun *run, iBool isCentered) {
@@ -904,6 +911,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
904 : scheme == titan_GmLinkScheme ? uploadArrow 911 : scheme == titan_GmLinkScheme ? uploadArrow
905 : scheme == finger_GmLinkScheme ? pointingFinger 912 : scheme == finger_GmLinkScheme ? pointingFinger
906 : scheme == mailto_GmLinkScheme ? envelope 913 : scheme == mailto_GmLinkScheme ? envelope
914 : scheme == data_GmLinkScheme ? paperclip_Icon
907 : link->flags & remote_GmLinkFlag ? globe 915 : link->flags & remote_GmLinkFlag ? globe
908 : link->flags & imageFileExtension_GmLinkFlag ? image 916 : link->flags & imageFileExtension_GmLinkFlag ? image
909 : link->flags & fontpackFileExtension_GmLinkFlag ? fontpack_Icon 917 : link->flags & fontpackFileExtension_GmLinkFlag ? fontpack_Icon
@@ -1283,7 +1291,7 @@ static void updateIconBasedOnUrl_GmDocument_(iGmDocument *d) {
1283 } 1291 }
1284} 1292}
1285 1293
1286void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) { 1294void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *paletteSeed, const iBlock *iconSeed) {
1287 const iPrefs * prefs = prefs_App(); 1295 const iPrefs * prefs = prefs_App();
1288 enum iGmDocumentTheme theme = currentTheme_(); 1296 enum iGmDocumentTheme theme = currentTheme_();
1289 static const iChar siteIcons[] = { 1297 static const iChar siteIcons[] = {
@@ -1294,6 +1302,16 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) {
1294 0x1f306, 0x1f308, 0x1f30a, 0x1f319, 0x1f31f, 0x1f320, 0x1f340, 0x1f4cd, 0x1f4e1, 0x1f531, 1302 0x1f306, 0x1f308, 0x1f30a, 0x1f319, 0x1f31f, 0x1f320, 0x1f340, 0x1f4cd, 0x1f4e1, 0x1f531,
1295 0x1f533, 0x1f657, 0x1f659, 0x1f665, 0x1f668, 0x1f66b, 0x1f78b, 0x1f796, 0x1f79c, 1303 0x1f533, 0x1f657, 0x1f659, 0x1f665, 0x1f668, 0x1f66b, 0x1f78b, 0x1f796, 0x1f79c,
1296 }; 1304 };
1305 if (!iconSeed) {
1306 iconSeed = paletteSeed;
1307 }
1308 if (iconSeed && !isEmpty_Block(iconSeed)) {
1309 const uint32_t seedHash = themeHash_(iconSeed);
1310 d->siteIcon = siteIcons[(seedHash >> 7) % iElemCount(siteIcons)];
1311 }
1312 else {
1313 d->siteIcon = 0;
1314 }
1297 /* Default colors. These are used on "about:" pages and local files, for example. */ { 1315 /* Default colors. These are used on "about:" pages and local files, for example. */ {
1298 /* Link colors are generally the same in all themes. */ 1316 /* Link colors are generally the same in all themes. */
1299 set_Color(tmBadLink_ColorId, get_Color(red_ColorId)); 1317 set_Color(tmBadLink_ColorId, get_Color(red_ColorId));
@@ -1495,13 +1513,11 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) {
1495 } 1513 }
1496 } 1514 }
1497 } 1515 }
1498 if (seed && !isEmpty_Block(seed)) { 1516 if (paletteSeed && !isEmpty_Block(paletteSeed)) {
1499 d->themeSeed = themeHash_(seed); 1517 d->themeSeed = themeHash_(paletteSeed);
1500 d->siteIcon = siteIcons[(d->themeSeed >> 7) % iElemCount(siteIcons)];
1501 } 1518 }
1502 else { 1519 else {
1503 d->themeSeed = 0; 1520 d->themeSeed = 0;
1504 d->siteIcon = 0;
1505 } 1521 }
1506 /* Set up colors. */ 1522 /* Set up colors. */
1507 if (d->themeSeed) { 1523 if (d->themeSeed) {
@@ -1552,7 +1568,7 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) {
1552 1568
1553 if (theme == colorfulDark_GmDocumentTheme) { 1569 if (theme == colorfulDark_GmDocumentTheme) {
1554 iHSLColor base = { hues[primIndex], 1570 iHSLColor base = { hues[primIndex],
1555 0.8f * (d->themeSeed >> 24) / 255.0f, 1571 0.8f * (d->themeSeed >> 24) / 255.0f + minSat_HSLColor,
1556 0.06f + 0.09f * ((d->themeSeed >> 5) & 0x7) / 7.0f, 1572 0.06f + 0.09f * ((d->themeSeed >> 5) & 0x7) / 7.0f,
1557 1.0f }; 1573 1.0f };
1558 iHSLColor altBase = { altHue, base.sat, base.lum, 1 }; 1574 iHSLColor altBase = { altHue, base.sat, base.lum, 1 };
@@ -1562,13 +1578,13 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) {
1562 setHsl_Color(tmBannerBackground_ColorId, addSatLum_HSLColor(base, 0.1f, 0.04f * (isBannerLighter ? 1 : -1))); 1578 setHsl_Color(tmBannerBackground_ColorId, addSatLum_HSLColor(base, 0.1f, 0.04f * (isBannerLighter ? 1 : -1)));
1563 setHsl_Color(tmBannerTitle_ColorId, setLum_HSLColor(addSatLum_HSLColor(base, 0.1f, 0), 0.55f)); 1579 setHsl_Color(tmBannerTitle_ColorId, setLum_HSLColor(addSatLum_HSLColor(base, 0.1f, 0), 0.55f));
1564 setHsl_Color(tmBannerIcon_ColorId, setLum_HSLColor(addSatLum_HSLColor(base, 0.35f, 0), 0.65f)); 1580 setHsl_Color(tmBannerIcon_ColorId, setLum_HSLColor(addSatLum_HSLColor(base, 0.35f, 0), 0.65f));
1565 1581
1566// printf("primHue: %zu alts: %d %d isDarkBgSat: %d\n", 1582// printf("primHue: %zu alts: %d %d isDarkBgSat: %d\n",
1567// primIndex, 1583// primIndex,
1568// altHues[primIndex].index[altIndex[0]], 1584// altHues[primIndex].index[altIndex[0]],
1569// altHues[primIndex].index[altIndex[1]], 1585// altHues[primIndex].index[altIndex[1]],
1570// isDarkBgSat); 1586// isDarkBgSat);
1571 1587
1572 const float titleLum = 0.2f * ((d->themeSeed >> 17) & 0x7) / 7.0f; 1588 const float titleLum = 0.2f * ((d->themeSeed >> 17) & 0x7) / 7.0f;
1573 setHsl_Color(tmHeading1_ColorId, setLum_HSLColor(altBase, titleLum + 0.80f)); 1589 setHsl_Color(tmHeading1_ColorId, setLum_HSLColor(altBase, titleLum + 0.80f));
1574 setHsl_Color(tmHeading2_ColorId, setLum_HSLColor(altBase, titleLum + 0.70f)); 1590 setHsl_Color(tmHeading2_ColorId, setLum_HSLColor(altBase, titleLum + 0.70f));
@@ -1730,8 +1746,8 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) {
1730 /* Derived colors. */ 1746 /* Derived colors. */
1731 setDerivedThemeColors_(theme); 1747 setDerivedThemeColors_(theme);
1732 /* Special exceptions. */ 1748 /* Special exceptions. */
1733 if (seed) { 1749 if (iconSeed) {
1734 if (equal_CStr(cstr_Block(seed), "gemini.circumlunar.space")) { 1750 if (equal_CStr(cstr_Block(iconSeed), "gemini.circumlunar.space")) {
1735 d->siteIcon = 0x264a; /* gemini symbol */ 1751 d->siteIcon = 0x264a; /* gemini symbol */
1736 } 1752 }
1737 updateIconBasedOnUrl_GmDocument_(d); 1753 updateIconBasedOnUrl_GmDocument_(d);
@@ -1752,7 +1768,8 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) {
1752void makePaletteGlobal_GmDocument(const iGmDocument *d) { 1768void makePaletteGlobal_GmDocument(const iGmDocument *d) {
1753 if (!d->isPaletteValid) { 1769 if (!d->isPaletteValid) {
1754 /* Recompute the palette since it's needed now. */ 1770 /* Recompute the palette since it's needed now. */
1755 setThemeSeed_GmDocument((iGmDocument *) d, urlThemeSeed_String(&d->url)); 1771 setThemeSeed_GmDocument(
1772 (iGmDocument *) d, urlPaletteSeed_String(&d->url), urlThemeSeed_String(&d->url));
1756 } 1773 }
1757 iAssert(d->isPaletteValid); 1774 iAssert(d->isPaletteValid);
1758 memcpy(get_Root()->tmPalette, d->palette, sizeof(d->palette)); 1775 memcpy(get_Root()->tmPalette, d->palette, sizeof(d->palette));
@@ -1929,7 +1946,7 @@ static void normalize_GmDocument(iGmDocument *d) {
1929void setUrl_GmDocument(iGmDocument *d, const iString *url) { 1946void setUrl_GmDocument(iGmDocument *d, const iString *url) {
1930 url = canonicalUrl_String(url); 1947 url = canonicalUrl_String(url);
1931 set_String(&d->url, url); 1948 set_String(&d->url, url);
1932 setThemeSeed_GmDocument(d, urlThemeSeed_String(url)); 1949 setThemeSeed_GmDocument(d, urlPaletteSeed_String(url), urlThemeSeed_String(url));
1933 iUrl parts; 1950 iUrl parts;
1934 init_Url(&parts, url); 1951 init_Url(&parts, url);
1935 setRange_String(&d->localHost, parts.host); 1952 setRange_String(&d->localHost, parts.host);
diff --git a/src/gmdocument.h b/src/gmdocument.h
index 0969794c..6c25dd6f 100644
--- a/src/gmdocument.h
+++ b/src/gmdocument.h
@@ -181,7 +181,9 @@ enum iGmDocumentUpdate {
181 final_GmDocumentUpdate, /* process all lines, including the last one if not terminated */ 181 final_GmDocumentUpdate, /* process all lines, including the last one if not terminated */
182}; 182};
183 183
184void setThemeSeed_GmDocument (iGmDocument *, const iBlock *seed); 184void setThemeSeed_GmDocument (iGmDocument *,
185 const iBlock *paletteSeed,
186 const iBlock *iconSeed); /* seeds may be NULL; NULL iconSeed will use paletteSeed instead */
185void setFormat_GmDocument (iGmDocument *, enum iSourceFormat format); 187void setFormat_GmDocument (iGmDocument *, enum iSourceFormat format);
186void setWidth_GmDocument (iGmDocument *, int width, int canvasWidth); 188void setWidth_GmDocument (iGmDocument *, int width, int canvasWidth);
187iBool updateWidth_GmDocument (iGmDocument *, int width, int canvasWidth); 189iBool updateWidth_GmDocument (iGmDocument *, int width, int canvasWidth);
diff --git a/src/gmutil.c b/src/gmutil.c
index ecfe2128..b32722ac 100644
--- a/src/gmutil.c
+++ b/src/gmutil.c
@@ -22,6 +22,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22 22
23#include "gmutil.h" 23#include "gmutil.h"
24#include "fontpack.h" 24#include "fontpack.h"
25#include "lang.h"
26#include "sitespec.h"
27#include "ui/color.h"
25 28
26#include <the_Foundation/file.h> 29#include <the_Foundation/file.h>
27#include <the_Foundation/fileinfo.h> 30#include <the_Foundation/fileinfo.h>
@@ -277,6 +280,19 @@ const iBlock *urlThemeSeed_String(const iString *url) {
277 return collect_Block(newRange_Block(user)); 280 return collect_Block(newRange_Block(user));
278} 281}
279 282
283const iBlock *urlPaletteSeed_String(const iString *url) {
284 if (equalCase_Rangecc(urlScheme_String(url), "file")) {
285 return urlThemeSeed_String(url);
286 }
287 /* Check for a site-specific setting. */
288 const iString *seed =
289 valueString_SiteSpec(collectNewRange_String(urlRoot_String(url)), paletteSeed_SiteSpecKey);
290 if (!isEmpty_String(seed)) {
291 return utf8_String(seed);
292 }
293 return urlThemeSeed_String(url);
294}
295
280static iBool isAbsolutePath_(iRangecc path) { 296static iBool isAbsolutePath_(iRangecc path) {
281 return isAbsolute_Path(collect_String(urlDecode_String(collect_String(newRange_String(path))))); 297 return isAbsolute_Path(collect_String(urlDecode_String(collect_String(newRange_String(path)))));
282} 298}
@@ -735,13 +751,36 @@ const iString *canonicalUrl_String(const iString *d) {
735 return canon ? collect_String(canon) : d; 751 return canon ? collect_String(canon) : d;
736} 752}
737 753
754const iString *prettyDataUrl_String(const iString *d, int contentColor) {
755 iUrl url;
756 init_Url(&url, d);
757 if (!equalCase_Rangecc(url.scheme, "data")) {
758 return d;
759 }
760 iString *pretty = new_String();
761 const char *comma = strchr(url.path.start, ',');
762 if (!comma) {
763 comma = iMin(constEnd_String(d), constBegin_String(d) + 256);
764 }
765 appendRange_String(pretty, (iRangecc){ constBegin_String(d), comma });
766 if (size_Range(&url.path)) {
767 if (contentColor != none_ColorId) {
768 appendCStr_String(pretty, escape_Color(contentColor));
769 }
770 appendCStr_String(pretty, " (");
771 appendCStr_String(pretty, formatCStrs_Lang("num.bytes.n", size_Range(&url.path)));
772 appendCStr_String(pretty, ")");
773 }
774 return collect_String(pretty);
775}
776
738iRangecc mediaTypeWithoutParameters_Rangecc(iRangecc mime) { 777iRangecc mediaTypeWithoutParameters_Rangecc(iRangecc mime) {
739 iRangecc part = iNullRange; 778 iRangecc part = iNullRange;
740 nextSplit_Rangecc(mime, ";", &part); 779 nextSplit_Rangecc(mime, ";", &part);
741 return part; 780 return part;
742} 781}
743 782
744const iString *feedEntryOpenCommand_String(const iString *url, int newTab) { 783const iString *feedEntryOpenCommand_String(const iString *url, int newTab, int newWindow) {
745 if (!isEmpty_String(url)) { 784 if (!isEmpty_String(url)) {
746 iString *cmd = collectNew_String(); 785 iString *cmd = collectNew_String();
747 const size_t fragPos = indexOf_String(url, '#'); 786 const size_t fragPos = indexOf_String(url, '#');
@@ -749,15 +788,20 @@ const iString *feedEntryOpenCommand_String(const iString *url, int newTab) {
749 iString *head = newRange_String( 788 iString *head = newRange_String(
750 (iRangecc){ constBegin_String(url) + fragPos + 1, constEnd_String(url) }); 789 (iRangecc){ constBegin_String(url) + fragPos + 1, constEnd_String(url) });
751 format_String(cmd, 790 format_String(cmd,
752 "open fromsidebar:1 newtab:%d gotourlheading:%s url:%s", 791 "open fromsidebar:1 newtab:%d newwindow:%d gotourlheading:%s url:%s",
753 newTab, 792 newTab,
793 newWindow,
754 cstr_String(head), 794 cstr_String(head),
755 cstr_Rangecc((iRangecc){ constBegin_String(url), 795 cstr_Rangecc((iRangecc){ constBegin_String(url),
756 constBegin_String(url) + fragPos })); 796 constBegin_String(url) + fragPos }));
757 delete_String(head); 797 delete_String(head);
758 } 798 }
759 else { 799 else {
760 format_String(cmd, "open fromsidebar:1 newtab:%d url:%s", newTab, cstr_String(url)); 800 format_String(cmd,
801 "open fromsidebar:1 newtab:%d newwindow:%d url:%s",
802 newTab,
803 newWindow,
804 cstr_String(url));
761 } 805 }
762 return cmd; 806 return cmd;
763 } 807 }
@@ -901,40 +945,3 @@ const iGmError *get_GmError(enum iGmStatusCode code) {
901 return &errors_[0].err; /* unknown */ 945 return &errors_[0].err; /* unknown */
902} 946}
903 947
904int replaceRegExp_String(iString *d, const iRegExp *regexp, const char *replacement,
905 void (*matchHandler)(void *, const iRegExpMatch *),
906 void *context) {
907 iRegExpMatch m;
908 iString result;
909 int numMatches = 0;
910 const char *pos = constBegin_String(d);
911 init_RegExpMatch(&m);
912 init_String(&result);
913 while (matchString_RegExp(regexp, d, &m)) {
914 appendRange_String(&result, (iRangecc){ pos, begin_RegExpMatch(&m) });
915 /* Replace any capture group back-references. */
916 for (const char *ch = replacement; *ch; ch++) {
917 if (*ch == '\\') {
918 ch++;
919 if (*ch == '\\') {
920 appendCStr_String(&result, "\\");
921 }
922 else if (*ch >= '0' && *ch <= '9') {
923 appendRange_String(&result, capturedRange_RegExpMatch(&m, *ch - '0'));
924 }
925 }
926 else {
927 appendData_Block(&result.chars, ch, 1);
928 }
929 }
930 if (matchHandler) {
931 matchHandler(context, &m);
932 }
933 pos = end_RegExpMatch(&m);
934 numMatches++;
935 }
936 appendRange_String(&result, (iRangecc){ pos, constEnd_String(d) });
937 set_String(d, &result);
938 deinit_String(&result);
939 return numMatches;
940}
diff --git a/src/gmutil.h b/src/gmutil.h
index 1594afc4..e4284cfd 100644
--- a/src/gmutil.h
+++ b/src/gmutil.h
@@ -120,6 +120,7 @@ uint16_t urlPort_String (const iString *);
120iRangecc urlUser_String (const iString *); 120iRangecc urlUser_String (const iString *);
121iRangecc urlRoot_String (const iString *); 121iRangecc urlRoot_String (const iString *);
122const iBlock * urlThemeSeed_String (const iString *); 122const iBlock * urlThemeSeed_String (const iString *);
123const iBlock * urlPaletteSeed_String (const iString *);
123 124
124const iString * absoluteUrl_String (const iString *, const iString *urlMaybeRelative); 125const iString * absoluteUrl_String (const iString *, const iString *urlMaybeRelative);
125iBool isLikelyUrl_String (const iString *); 126iBool isLikelyUrl_String (const iString *);
@@ -141,6 +142,7 @@ void urlEncodeSpaces_String (iString *);
141const iString * withSpacesEncoded_String(const iString *); 142const iString * withSpacesEncoded_String(const iString *);
142const iString * withScheme_String (const iString *, const char *scheme); /* replace URI scheme */ 143const iString * withScheme_String (const iString *, const char *scheme); /* replace URI scheme */
143const iString * canonicalUrl_String (const iString *); 144const iString * canonicalUrl_String (const iString *);
145const iString * prettyDataUrl_String (const iString *, int contentColor);
144 146
145const char * mediaType_Path (const iString *path); 147const char * mediaType_Path (const iString *path);
146const char * mediaTypeFromFileExtension_String (const iString *); 148const char * mediaTypeFromFileExtension_String (const iString *);
@@ -149,9 +151,4 @@ iRangecc mediaTypeWithoutParameters_Rangecc (iRangecc mime);
149const iString * findContainerArchive_Path (const iString *path); 151const iString * findContainerArchive_Path (const iString *path);
150 152
151 153
152const iString * feedEntryOpenCommand_String (const iString *url, int newTab); /* checks fragment */ 154const iString * feedEntryOpenCommand_String (const iString *url, int newTab, int newWindow); /* checks fragment */
153
154/* TODO: Consider adding this to the_Foundation. */
155int replaceRegExp_String (iString *, const iRegExp *regexp, const char *replacement,
156 void (*matchHandler)(void *, const iRegExpMatch *),
157 void *context);
diff --git a/src/macos.m b/src/macos.m
index 7b248c3b..5f376874 100644
--- a/src/macos.m
+++ b/src/macos.m
@@ -83,7 +83,9 @@ static void ignoreImmediateKeyDownEvents_(void) {
83 However, we shouldn't double-activate menu items when a shortcut key is used in our 83 However, we shouldn't double-activate menu items when a shortcut key is used in our
84 widgets. Quite a kludge: take advantage of Window's focus-acquisition threshold to 84 widgets. Quite a kludge: take advantage of Window's focus-acquisition threshold to
85 ignore the immediately following key down events. */ 85 ignore the immediately following key down events. */
86 get_Window()->focusGainedAt = SDL_GetTicks(); 86 iForEach(PtrArray, w, collect_PtrArray(listWindows_App())) {
87 as_Window(w.ptr)->focusGainedAt = SDL_GetTicks();
88 }
87} 89}
88 90
89/*----------------------------------------------------------------------------------------------*/ 91/*----------------------------------------------------------------------------------------------*/
@@ -435,9 +437,17 @@ static iBool processScrollWheelEvent_(NSEvent *event) {
435 const iBool isPerPixel = (event.hasPreciseScrollingDeltas != 0); 437 const iBool isPerPixel = (event.hasPreciseScrollingDeltas != 0);
436 const iBool isInertia = (event.momentumPhase & (NSEventPhaseBegan | NSEventPhaseChanged)) != 0; 438 const iBool isInertia = (event.momentumPhase & (NSEventPhaseBegan | NSEventPhaseChanged)) != 0;
437 const iBool isEnded = event.scrollingDeltaX == 0.0f && event.scrollingDeltaY == 0.0f && !isInertia; 439 const iBool isEnded = event.scrollingDeltaX == 0.0f && event.scrollingDeltaY == 0.0f && !isInertia;
438 const iWindow *win = &get_MainWindow()->base; 440 const iWindow *win = NULL; //&get_MainWindow()->base;
439 if (event.window != nsWindow_(win->win)) { 441 /* If this event belongs to one of the MainWindows, handle it and mark it for that window.
440 /* Not the main window. */ 442 If it's for an auxiliary window, let the system handle it. */
443 iConstForEach(PtrArray, i, mainWindows_App()) {
444 if (event.window == nsWindow_(as_Window(i.ptr)->win)) {
445 win = i.ptr;
446 break;
447 }
448 }
449 if (!win) { //event.window != nsWindow_(win->win)) {
450 /* Not a main window. */
441 return iFalse; 451 return iFalse;
442 } 452 }
443 if (isPerPixel) { 453 if (isPerPixel) {
@@ -478,16 +488,18 @@ static iBool processScrollWheelEvent_(NSEvent *event) {
478 else { 488 else {
479 SDL_MouseWheelEvent e = { .type = SDL_MOUSEWHEEL }; 489 SDL_MouseWheelEvent e = { .type = SDL_MOUSEWHEEL };
480 e.timestamp = SDL_GetTicks(); 490 e.timestamp = SDL_GetTicks();
491 e.windowID = id_Window(win);
481 e.which = 1; /* Distinction between trackpad and regular mouse. */ 492 e.which = 1; /* Distinction between trackpad and regular mouse. */
482 /* Disregard any wheel acceleration. */ 493 /* Disregard any wheel acceleration. */
483 e.x = event.scrollingDeltaX > 0 ? 1 : event.scrollingDeltaX < 0 ? -1 : 0; 494 e.x = event.scrollingDeltaX > 0 ? 1 : event.scrollingDeltaX < 0 ? -1 : 0;
484 e.y = event.scrollingDeltaY > 0 ? 1 : event.scrollingDeltaY < 0 ? -1 : 0; 495 e.y = event.scrollingDeltaY > 0 ? 1 : event.scrollingDeltaY < 0 ? -1 : 0;
485 SDL_PushEvent((SDL_Event *) &e); 496 SDL_PushEvent((SDL_Event *) &e);
486 return iTrue; 497 return iTrue;
487 } 498 }
488 /* Post corresponding MOUSEWHEEL events. */ 499 /* Post corresponding MOUSEWHEEL events. */
489 SDL_MouseWheelEvent e = { .type = SDL_MOUSEWHEEL }; 500 SDL_MouseWheelEvent e = { .type = SDL_MOUSEWHEEL };
490 e.timestamp = SDL_GetTicks(); 501 e.timestamp = SDL_GetTicks();
502 e.windowID = id_Window(win);
491 e.which = isPerPixel ? 0 : 1; /* Distinction between trackpad and regular mouse. */ 503 e.which = isPerPixel ? 0 : 1; /* Distinction between trackpad and regular mouse. */
492 setPerPixel_MouseWheelEvent(&e, isPerPixel); 504 setPerPixel_MouseWheelEvent(&e, isPerPixel);
493 if (isPerPixel) { 505 if (isPerPixel) {
@@ -517,28 +529,6 @@ static iBool processScrollWheelEvent_(NSEvent *event) {
517 // printf("#### [%d] dx:%d dy:%d phase:%ld inertia:%d end:%d\n", preventTapGlitch_, e.x, e.y, (long) event.momentumPhase, 529 // printf("#### [%d] dx:%d dy:%d phase:%ld inertia:%d end:%d\n", preventTapGlitch_, e.x, e.y, (long) event.momentumPhase,
518 // isInertia, isEnded); fflush(stdout); 530 // isInertia, isEnded); fflush(stdout);
519 SDL_PushEvent((SDL_Event *) &e); 531 SDL_PushEvent((SDL_Event *) &e);
520#if 0
521 /* On macOS, we handle both trackpad and mouse events. We expect SDL to identify
522 which device is sending the event. */
523 if (ev.wheel.which == 0) {
524 /* Trackpad with precise scrolling w/inertia (points). */
525 setPerPixel_MouseWheelEvent(&ev.wheel, iTrue);
526 ev.wheel.x *= -d->window->base.pixelRatio;
527 ev.wheel.y *= d->window->base.pixelRatio;
528 /* Only scroll on one axis at a time. */
529 if (iAbs(ev.wheel.x) > iAbs(ev.wheel.y)) {
530 ev.wheel.y = 0;
531 }
532 else {
533 ev.wheel.x = 0;
534 }
535 }
536 else {
537 /* Disregard wheel acceleration applied by the OS. */
538 ev.wheel.x = -ev.wheel.x;
539 ev.wheel.y = iSign(ev.wheel.y);
540 }
541#endif
542 return iTrue; 532 return iTrue;
543} 533}
544 534
@@ -565,13 +555,6 @@ void setupApplication_MacOS(void) {
565 windowCloseItem.action = @selector(closeTab); 555 windowCloseItem.action = @selector(closeTab);
566 [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskScrollWheel 556 [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskScrollWheel
567 handler:^NSEvent*(NSEvent *event){ 557 handler:^NSEvent*(NSEvent *event){
568// printf("event type: %lu\n", (unsigned long) event.type);
569// fflush(stdout);
570// if (event.type == NSEventTypeGesture) {
571// trackSwipe_(event);
572// printf("GESTURE phase:%lu\n", (unsigned long) event.phase);
573//fflush(stdout);
574// }
575 if (event.type == NSEventTypeScrollWheel && 558 if (event.type == NSEventTypeScrollWheel &&
576 processScrollWheelEvent_(event)) { 559 processScrollWheelEvent_(event)) {
577 return nil; /* was eaten */ 560 return nil; /* was eaten */
diff --git a/src/periodic.c b/src/periodic.c
index b4f51ed3..0558ed50 100644
--- a/src/periodic.c
+++ b/src/periodic.c
@@ -107,14 +107,17 @@ iBool dispatchCommands_Periodic(iPeriodic *d) {
107 iConstForEach(Array, i, &d->commands.values) { 107 iConstForEach(Array, i, &d->commands.values) {
108 const iPeriodicCommand *pc = i.value; 108 const iPeriodicCommand *pc = i.value;
109 iAssert(isInstance_Object(pc->context, &Class_Widget)); 109 iAssert(isInstance_Object(pc->context, &Class_Widget));
110 const SDL_UserEvent ev = { 110 iRoot *root = constAs_Widget(pc->context)->root;
111 .type = SDL_USEREVENT, 111 if (root) {
112 .code = command_UserEventCode, 112 const SDL_UserEvent ev = {
113 .data1 = (void *) cstr_String(&pc->command), 113 .type = SDL_USEREVENT,
114 .data2 = findRoot_Window(get_Window(), pc->context) 114 .code = command_UserEventCode,
115 }; 115 .data1 = (void *) cstr_String(&pc->command),
116 if (ev.data2) { 116 .data2 = root,
117 setCurrent_Root(ev.data2); 117 .windowID = id_Window(root->window),
118 };
119 setCurrent_Window(root->window);
120 setCurrent_Root(root);
118 dispatchEvent_Widget(pc->context, (const SDL_Event *) &ev); 121 dispatchEvent_Widget(pc->context, (const SDL_Event *) &ev);
119 wasPosted = iTrue; 122 wasPosted = iTrue;
120 } 123 }
diff --git a/src/prefs.c b/src/prefs.c
index 13a1dab7..08355a6a 100644
--- a/src/prefs.c
+++ b/src/prefs.c
@@ -66,13 +66,15 @@ void init_Prefs(iPrefs *d) {
66 d->smoothScrollSpeed[keyboard_ScrollType] = 13; 66 d->smoothScrollSpeed[keyboard_ScrollType] = 13;
67 d->smoothScrollSpeed[mouse_ScrollType] = 13; 67 d->smoothScrollSpeed[mouse_ScrollType] = 13;
68 d->loadImageInsteadOfScrolling = iFalse; 68 d->loadImageInsteadOfScrolling = iFalse;
69 d->collapsePreOnLoad = iFalse; 69 d->openDataUrlImagesOnLoad = iFalse;
70 d->openArchiveIndexPages = iTrue; 70 d->collapsePreOnLoad = iFalse;
71 d->addBookmarksToBottom = iTrue; 71 d->openArchiveIndexPages = iTrue;
72 d->warnAboutMissingGlyphs = iTrue; 72 d->addBookmarksToBottom = iTrue;
73 d->decodeUserVisibleURLs = iTrue; 73 d->warnAboutMissingGlyphs = iTrue;
74 d->decodeUserVisibleURLs = iTrue;
74 d->maxCacheSize = 10; 75 d->maxCacheSize = 10;
75 d->maxMemorySize = 200; 76 d->maxMemorySize = 200;
77 d->maxUrlSize = 8192;
76 setCStr_String(&d->strings[uiFont_PrefsString], "default"); 78 setCStr_String(&d->strings[uiFont_PrefsString], "default");
77 setCStr_String(&d->strings[headingFont_PrefsString], "default"); 79 setCStr_String(&d->strings[headingFont_PrefsString], "default");
78 setCStr_String(&d->strings[bodyFont_PrefsString], "default"); 80 setCStr_String(&d->strings[bodyFont_PrefsString], "default");
diff --git a/src/prefs.h b/src/prefs.h
index ea864f51..d988399e 100644
--- a/src/prefs.h
+++ b/src/prefs.h
@@ -75,6 +75,7 @@ enum iPrefsBool {
75 hoverLink_PrefsBool, 75 hoverLink_PrefsBool,
76 smoothScrolling_PrefsBool, 76 smoothScrolling_PrefsBool,
77 loadImageInsteadOfScrolling_PrefsBool, 77 loadImageInsteadOfScrolling_PrefsBool,
78 openDataUrlImagesOnLoad_PrefsBool,
78 collapsePreOnLoad_PrefsBool, 79 collapsePreOnLoad_PrefsBool,
79 openArchiveIndexPages_PrefsBool, 80 openArchiveIndexPages_PrefsBool,
80 81
@@ -128,6 +129,7 @@ struct Impl_Prefs {
128 iBool hoverLink; 129 iBool hoverLink;
129 iBool smoothScrolling; 130 iBool smoothScrolling;
130 iBool loadImageInsteadOfScrolling; 131 iBool loadImageInsteadOfScrolling;
132 iBool openDataUrlImagesOnLoad;
131 iBool collapsePreOnLoad; 133 iBool collapsePreOnLoad;
132 iBool openArchiveIndexPages; 134 iBool openArchiveIndexPages;
133 135
@@ -172,6 +174,7 @@ struct Impl_Prefs {
172 /* Network */ 174 /* Network */
173 int maxCacheSize; /* MB */ 175 int maxCacheSize; /* MB */
174 int maxMemorySize; /* MB */ 176 int maxMemorySize; /* MB */
177 int maxUrlSize; /* bytes; longer ones will be disregarded */
175 /* Style */ 178 /* Style */
176 iStringSet * disabledFontPacks; 179 iStringSet * disabledFontPacks;
177 int gemtextAnsiEscapes; 180 int gemtextAnsiEscapes;
@@ -190,3 +193,7 @@ iLocalDef float scrollSpeedFactor_Prefs(const iPrefs *d, enum iScrollType type)
190 iAssert(type >= 0 && type < max_ScrollType); 193 iAssert(type >= 0 && type < max_ScrollType);
191 return 10.0f / iMax(1, d->smoothScrollSpeed[type]) * (type == mouse_ScrollType ? 0.5f : 1.0f); 194 return 10.0f / iMax(1, d->smoothScrollSpeed[type]) * (type == mouse_ScrollType ? 0.5f : 1.0f);
192} 195}
196
197iLocalDef enum iGmDocumentTheme docTheme_Prefs(const iPrefs *d) {
198 return isDark_ColorTheme(d->theme) ? d->docThemeDark : d->docThemeLight;
199}
diff --git a/src/sitespec.c b/src/sitespec.c
index fe80ad13..5133abe5 100644
--- a/src/sitespec.c
+++ b/src/sitespec.c
@@ -37,7 +37,8 @@ struct Impl_SiteParams {
37 iString titanIdentity; /* fingerprint */ 37 iString titanIdentity; /* fingerprint */
38 int dismissWarnings; 38 int dismissWarnings;
39 iStringArray usedIdentities; /* fingerprints; latest ones at the end */ 39 iStringArray usedIdentities; /* fingerprints; latest ones at the end */
40 /* TODO: theme seed, style settings */ 40 iString paletteSeed;
41 /* TODO: style settings */
41}; 42};
42 43
43void init_SiteParams(iSiteParams *d) { 44void init_SiteParams(iSiteParams *d) {
@@ -45,9 +46,11 @@ void init_SiteParams(iSiteParams *d) {
45 init_String(&d->titanIdentity); 46 init_String(&d->titanIdentity);
46 d->dismissWarnings = 0; 47 d->dismissWarnings = 0;
47 init_StringArray(&d->usedIdentities); 48 init_StringArray(&d->usedIdentities);
49 init_String(&d->paletteSeed);
48} 50}
49 51
50void deinit_SiteParams(iSiteParams *d) { 52void deinit_SiteParams(iSiteParams *d) {
53 deinit_String(&d->paletteSeed);
51 deinit_StringArray(&d->usedIdentities); 54 deinit_StringArray(&d->usedIdentities);
52 deinit_String(&d->titanIdentity); 55 deinit_String(&d->titanIdentity);
53} 56}
@@ -149,6 +152,9 @@ static void handleIniKeyValue_SiteSpec_(void *context, const iString *table, con
149 pushBack_StringArray(&d->loadParams->usedIdentities, collectNewRange_String(seg)); 152 pushBack_StringArray(&d->loadParams->usedIdentities, collectNewRange_String(seg));
150 } 153 }
151 } 154 }
155 else if (!cmp_String(key, "paletteSeed") && value->type == string_TomlType) {
156 set_String(&d->loadParams->paletteSeed, value->value.string);
157 }
152} 158}
153 159
154static iBool load_SiteSpec_(iSiteSpec *d) { 160static iBool load_SiteSpec_(iSiteSpec *d) {
@@ -173,7 +179,7 @@ static void save_SiteSpec_(iSiteSpec *d) {
173 iBeginCollect(); 179 iBeginCollect();
174 const iBlock * key = &i.value->keyBlock; 180 const iBlock * key = &i.value->keyBlock;
175 const iSiteParams *params = i.value->object; 181 const iSiteParams *params = i.value->object;
176 format_String(buf, "[%s]\n", cstr_Block(key)); 182 clear_String(buf);
177 if (params->titanPort) { 183 if (params->titanPort) {
178 appendFormat_String(buf, "titanPort = %u\n", params->titanPort); 184 appendFormat_String(buf, "titanPort = %u\n", params->titanPort);
179 } 185 }
@@ -190,8 +196,18 @@ static void save_SiteSpec_(iSiteSpec *d) {
190 "usedIdentities = \"%s\"\n", 196 "usedIdentities = \"%s\"\n",
191 cstrCollect_String(joinCStr_StringArray(&params->usedIdentities, " "))); 197 cstrCollect_String(joinCStr_StringArray(&params->usedIdentities, " ")));
192 } 198 }
193 appendCStr_String(buf, "\n"); 199 if (!isEmpty_String(&params->paletteSeed)) {
194 write_File(f, utf8_String(buf)); 200 appendCStr_String(buf, "paletteSeed = \"");
201 append_String(buf, collect_String(quote_String(&params->paletteSeed, iFalse)));
202 appendCStr_String(buf, "\"\n");
203 }
204 if (!isEmpty_String(buf)) {
205 writeData_File(f, "[", 1);
206 writeData_File(f, constData_Block(key), size_Block(key));
207 writeData_File(f, "]\n", 2);
208 appendCStr_String(buf, "\n");
209 write_File(f, utf8_String(buf));
210 }
195 iEndCollect(); 211 iEndCollect();
196 } 212 }
197 delete_String(buf); 213 delete_String(buf);
@@ -257,6 +273,12 @@ void setValueString_SiteSpec(const iString *site, enum iSiteSpecKey key, const i
257 set_String(&params->titanIdentity, value); 273 set_String(&params->titanIdentity, value);
258 } 274 }
259 break; 275 break;
276 case paletteSeed_SiteSpecKey:
277 if (!equal_String(&params->paletteSeed, value)) {
278 needSave = iTrue;
279 set_String(&params->paletteSeed, value);
280 }
281 break;
260 default: 282 default:
261 break; 283 break;
262 } 284 }
@@ -328,6 +350,8 @@ const iString *valueString_SiteSpec(const iString *site, enum iSiteSpecKey key)
328 switch (key) { 350 switch (key) {
329 case titanIdentity_SiteSpecKey: 351 case titanIdentity_SiteSpecKey:
330 return &params->titanIdentity; 352 return &params->titanIdentity;
353 case paletteSeed_SiteSpecKey:
354 return &params->paletteSeed;
331 default: 355 default:
332 return collectNew_String(); 356 return collectNew_String();
333 } 357 }
diff --git a/src/sitespec.h b/src/sitespec.h
index 11c40e3c..372e021e 100644
--- a/src/sitespec.h
+++ b/src/sitespec.h
@@ -31,6 +31,7 @@ enum iSiteSpecKey {
31 titanIdentity_SiteSpecKey, /* String */ 31 titanIdentity_SiteSpecKey, /* String */
32 dismissWarnings_SiteSpecKey, /* int */ 32 dismissWarnings_SiteSpecKey, /* int */
33 usedIdentities_SiteSpecKey, /* StringArray */ 33 usedIdentities_SiteSpecKey, /* StringArray */
34 paletteSeed_SiteSpecKey, /* String */
34}; 35};
35 36
36void init_SiteSpec (const char *saveDir); 37void init_SiteSpec (const char *saveDir);
diff --git a/src/ui/banner.c b/src/ui/banner.c
index 11ae1574..79d70039 100644
--- a/src/ui/banner.c
+++ b/src/ui/banner.c
@@ -327,7 +327,8 @@ iBool processEvent_Banner(iBanner *d, const SDL_Event *ev) {
327 else { 327 else {
328 switch (item->code) { 328 switch (item->code) {
329 case missingGlyphs_GmStatusCode: 329 case missingGlyphs_GmStatusCode:
330 postCommandf_App("open newtab:1 url:about:fonts"); 330 //postCommandf_App("open newtab:1 url:about:fonts");
331 makeGlyphFinder_Widget();
331 break; 332 break;
332 case ansiEscapes_GmStatusCode: 333 case ansiEscapes_GmStatusCode:
333 makeQuestion_Widget( 334 makeQuestion_Widget(
diff --git a/src/ui/color.c b/src/ui/color.c
index 824342ae..9cba322d 100644
--- a/src/ui/color.c
+++ b/src/ui/color.c
@@ -522,8 +522,8 @@ iHSLColor setLum_HSLColor(iHSLColor d, float lum) {
522} 522}
523 523
524iHSLColor addSatLum_HSLColor(iHSLColor d, float sat, float lum) { 524iHSLColor addSatLum_HSLColor(iHSLColor d, float sat, float lum) {
525 d.sat = iClamp(d.sat + sat, 0, 1); 525 d.sat = iClamp(d.sat + sat, minSat_HSLColor, 1);
526 d.lum = iClamp(d.lum + lum, 0, 1); 526 d.lum = iClamp(d.lum + lum, minSat_HSLColor, 1);
527 return d; 527 return d;
528} 528}
529 529
diff --git a/src/ui/color.h b/src/ui/color.h
index 24f9e713..f46976d7 100644
--- a/src/ui/color.h
+++ b/src/ui/color.h
@@ -231,6 +231,8 @@ struct Impl_HSLColor {
231 float hue, sat, lum, a; 231 float hue, sat, lum, a;
232}; 232};
233 233
234#define minSat_HSLColor 0.013f /* Conversion to 8-bit RGB may result in saturation dropping to zero. */
235
234iHSLColor hsl_Color (iColor); 236iHSLColor hsl_Color (iColor);
235iColor rgb_HSLColor (iHSLColor); 237iColor rgb_HSLColor (iHSLColor);
236float luma_Color (iColor); 238float luma_Color (iColor);
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index a52e99af..fdc0dd75 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -358,6 +358,7 @@ static void updateSideIconBuf_DocumentWidget_ (const iDocumentWidget *d);
358static void prerender_DocumentWidget_ (iAny *); 358static void prerender_DocumentWidget_ (iAny *);
359static void scrollBegan_DocumentWidget_ (iAnyObject *, int, uint32_t); 359static void scrollBegan_DocumentWidget_ (iAnyObject *, int, uint32_t);
360static void refreshWhileScrolling_DocumentWidget_ (iAny *); 360static void refreshWhileScrolling_DocumentWidget_ (iAny *);
361static iBool requestMedia_DocumentWidget_ (iDocumentWidget *d, iGmLinkId linkId, iBool enableFilters);
361 362
362/* TODO: The following methods are called from DocumentView, which goes the wrong way. */ 363/* TODO: The following methods are called from DocumentView, which goes the wrong way. */
363 364
@@ -1824,7 +1825,7 @@ static void draw_DocumentView_(const iDocumentView *d) {
1824 } 1825 }
1825 if (d->drawBufs->flags & updateSideBuf_DrawBufsFlag) { 1826 if (d->drawBufs->flags & updateSideBuf_DrawBufsFlag) {
1826 updateSideIconBuf_DocumentView_(d); 1827 updateSideIconBuf_DocumentView_(d);
1827 } 1828 }
1828 const iRect docBounds = documentBounds_DocumentView_(d); 1829 const iRect docBounds = documentBounds_DocumentView_(d);
1829 const iRangei vis = visibleRange_DocumentView_(d); 1830 const iRangei vis = visibleRange_DocumentView_(d);
1830 iDrawContext ctx = { 1831 iDrawContext ctx = {
@@ -2410,6 +2411,20 @@ static const char *zipPageHeading_(const iRangecc mime) {
2410 2411
2411static void postProcessRequestContent_DocumentWidget_(iDocumentWidget *d, iBool isCached) { 2412static void postProcessRequestContent_DocumentWidget_(iDocumentWidget *d, iBool isCached) {
2412 iWidget *w = as_Widget(d); 2413 iWidget *w = as_Widget(d);
2414 /* Embedded images in data links can be shown immediately as they are already fetched
2415 data that is part of the document. */
2416 if (prefs_App()->openDataUrlImagesOnLoad) {
2417 iGmDocument *doc = d->view.doc;
2418 for (size_t linkId = 1; ; linkId++) {
2419 const int linkFlags = linkFlags_GmDocument(doc, linkId);
2420 const iString *linkUrl = linkUrl_GmDocument(doc, linkId);
2421 if (!linkUrl) break;
2422 if (scheme_GmLinkFlag(linkFlags) == data_GmLinkScheme &&
2423 (linkFlags & imageFileExtension_GmLinkFlag)) {
2424 requestMedia_DocumentWidget_(d, linkId, 0);
2425 }
2426 }
2427 }
2413 /* Gempub page behavior and footer actions. */ { 2428 /* Gempub page behavior and footer actions. */ {
2414 /* TODO: move this to gempub.c */ 2429 /* TODO: move this to gempub.c */
2415 delete_Gempub(d->sourceGempub); 2430 delete_Gempub(d->sourceGempub);
@@ -2681,7 +2696,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d,
2681 if (loadArchive_FontPack(fp, zip)) { 2696 if (loadArchive_FontPack(fp, zip)) {
2682 appendFormat_String(&str, "# " fontpack_Icon "%s\n%s", 2697 appendFormat_String(&str, "# " fontpack_Icon "%s\n%s",
2683 cstr_String(id_FontPack(fp).id), 2698 cstr_String(id_FontPack(fp).id),
2684 cstrCollect_String(infoText_FontPack(fp))); 2699 cstrCollect_String(infoText_FontPack(fp, iTrue)));
2685 } 2700 }
2686 appendCStr_String(&str, "\n"); 2701 appendCStr_String(&str, "\n");
2687 appendCStr_String(&str, cstr_Lang("fontpack.help")); 2702 appendCStr_String(&str, cstr_Lang("fontpack.help"));
@@ -3258,9 +3273,11 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) {
3258 setTextColor_LabelWidget(menu, uiTextAction_ColorId); 3273 setTextColor_LabelWidget(menu, uiTextAction_ColorId);
3259 } 3274 }
3260 } 3275 }
3261 setValidator_InputWidget(findChild_Widget(dlg, "input"), inputQueryValidator_, d); 3276 iInputWidget *input = findChild_Widget(dlg, "input");
3262 setSensitiveContent_InputWidget(findChild_Widget(dlg, "input"), 3277 setValidator_InputWidget(input, inputQueryValidator_, d);
3263 statusCode == sensitiveInput_GmStatusCode); 3278 setBackupFileName_InputWidget(input, "inputbackup.txt");
3279 setSelectAllOnFocus_InputWidget(input, iTrue);
3280 setSensitiveContent_InputWidget(input, statusCode == sensitiveInput_GmStatusCode);
3264 if (document_App() != d) { 3281 if (document_App() != d) {
3265 postCommandf_App("tabs.switch page:%p", d); 3282 postCommandf_App("tabs.switch page:%p", d);
3266 } 3283 }
@@ -3921,12 +3938,12 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
3921 const char *unchecked = red_ColorEscape "\u2610"; 3938 const char *unchecked = red_ColorEscape "\u2610";
3922 const char *checked = green_ColorEscape "\u2611"; 3939 const char *checked = green_ColorEscape "\u2611";
3923 const iBool haveFingerprint = (d->certFlags & haveFingerprint_GmCertFlag) != 0; 3940 const iBool haveFingerprint = (d->certFlags & haveFingerprint_GmCertFlag) != 0;
3924 const int requiredForTrust = (available_GmCertFlag | haveFingerprint_GmCertFlag | 3941 const int requiredForTrust =
3925 timeVerified_GmCertFlag); 3942 (available_GmCertFlag | haveFingerprint_GmCertFlag | timeVerified_GmCertFlag);
3926 const iBool canTrust = ~d->certFlags & trusted_GmCertFlag && 3943 const iBool canTrust = ~d->certFlags & trusted_GmCertFlag &&
3927 ((d->certFlags & requiredForTrust) == requiredForTrust); 3944 ((d->certFlags & requiredForTrust) == requiredForTrust);
3928 const iRecentUrl *recent = constMostRecentUrl_History(d->mod.history); 3945 const iRecentUrl *recent = constMostRecentUrl_History(d->mod.history);
3929 const iString *meta = &d->sourceMime; 3946 const iString *meta = &d->sourceMime;
3930 if (recent && recent->cachedResponse) { 3947 if (recent && recent->cachedResponse) {
3931 meta = &recent->cachedResponse->meta; 3948 meta = &recent->cachedResponse->meta;
3932 } 3949 }
@@ -3991,6 +4008,10 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
3991 if (haveFingerprint) { 4008 if (haveFingerprint) {
3992 pushBack_Array(items, &(iMenuItem){ "${dlg.cert.fingerprint}", 0, 0, "server.copycert" }); 4009 pushBack_Array(items, &(iMenuItem){ "${dlg.cert.fingerprint}", 0, 0, "server.copycert" });
3993 } 4010 }
4011 const iRangecc root = urlRoot_String(d->mod.url);
4012 if (!isEmpty_Range(&root)) {
4013 pushBack_Array(items, &(iMenuItem){ "${pageinfo.settings}", 0, 0, "document.sitespec" });
4014 }
3994 if (!isEmpty_Array(items)) { 4015 if (!isEmpty_Array(items)) {
3995 pushBack_Array(items, &(iMenuItem){ "---", 0, 0, 0 }); 4016 pushBack_Array(items, &(iMenuItem){ "---", 0, 0, 0 });
3996 } 4017 }
@@ -4014,6 +4035,12 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
4014 addAction_Widget(dlg, SDLK_SPACE, 0, "message.ok"); 4035 addAction_Widget(dlg, SDLK_SPACE, 0, "message.ok");
4015 return iTrue; 4036 return iTrue;
4016 } 4037 }
4038 else if (equal_Command(cmd, "document.sitespec") && d == document_App()) {
4039 if (!findWidget_App("sitespec.palette")) {
4040 makeSiteSpecificSettings_Widget(d->mod.url);
4041 }
4042 return iTrue;
4043 }
4017 else if (equal_Command(cmd, "server.unexpire") && document_App() == d) { 4044 else if (equal_Command(cmd, "server.unexpire") && document_App() == d) {
4018 const iRangecc host = urlHost_String(d->mod.url); 4045 const iRangecc host = urlHost_String(d->mod.url);
4019 const uint16_t port = urlPort_String(d->mod.url); 4046 const uint16_t port = urlPort_String(d->mod.url);
@@ -4922,7 +4949,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
4922 for (size_t i = 0; i < 64; ++i) { 4949 for (size_t i = 0; i < 64; ++i) {
4923 setByte_Block(seed, i, iRandom(0, 256)); 4950 setByte_Block(seed, i, iRandom(0, 256));
4924 } 4951 }
4925 setThemeSeed_GmDocument(view->doc, seed); 4952 setThemeSeed_GmDocument(view->doc, seed, NULL);
4926 delete_Block(seed); 4953 delete_Block(seed);
4927 invalidate_DocumentWidget_(d); 4954 invalidate_DocumentWidget_(d);
4928 refresh_Widget(w); 4955 refresh_Widget(w);
@@ -5044,10 +5071,9 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
5044 iArray items; 5071 iArray items;
5045 init_Array(&items, sizeof(iMenuItem)); 5072 init_Array(&items, sizeof(iMenuItem));
5046 if (d->contextLink) { 5073 if (d->contextLink) {
5047 /* Context menu for a link. */ 5074 /* Construct the link context menu, depending on what kind of link was clicked. */
5048 interactingWithLink_DocumentWidget_(d, d->contextLink->linkId); /* perhaps will be triggered */ 5075 interactingWithLink_DocumentWidget_(d, d->contextLink->linkId); /* perhaps will be triggered */
5049 const iString *linkUrl = linkUrl_GmDocument(view->doc, d->contextLink->linkId); 5076 const iString *linkUrl = linkUrl_GmDocument(view->doc, d->contextLink->linkId);
5050// const int linkFlags = linkFlags_GmDocument(d->doc, d->contextLink->linkId);
5051 const iRangecc scheme = urlScheme_String(linkUrl); 5077 const iRangecc scheme = urlScheme_String(linkUrl);
5052 const iBool isGemini = equalCase_Rangecc(scheme, "gemini"); 5078 const iBool isGemini = equalCase_Rangecc(scheme, "gemini");
5053 iBool isNative = iFalse; 5079 iBool isNative = iFalse;
@@ -5059,41 +5085,55 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
5059 format_CStr("```%s", cstr_String(infoText)), 5085 format_CStr("```%s", cstr_String(infoText)),
5060 0, 0, NULL }); 5086 0, 0, NULL });
5061 } 5087 }
5062 if (willUseProxy_App(scheme) || isGemini || 5088 if (isGemini ||
5089 willUseProxy_App(scheme) ||
5090 equalCase_Rangecc(scheme, "data") ||
5063 equalCase_Rangecc(scheme, "file") || 5091 equalCase_Rangecc(scheme, "file") ||
5064 equalCase_Rangecc(scheme, "finger") || 5092 equalCase_Rangecc(scheme, "finger") ||
5065 equalCase_Rangecc(scheme, "gopher")) { 5093 equalCase_Rangecc(scheme, "gopher")) {
5066 isNative = iTrue; 5094 isNative = iTrue;
5067 /* Regular links that we can open. */ 5095 /* Regular links that we can open. */
5068 pushBackN_Array( 5096 pushBackN_Array(&items,
5069 &items, 5097 (iMenuItem[]){
5070 (iMenuItem[]){ { openTab_Icon " ${link.newtab}", 5098 { openTab_Icon " ${link.newtab}",
5071 0, 5099 0,
5072 0, 5100 0,
5073 format_CStr("!open newtab:1 origin:%s url:%s", 5101 format_CStr("!open newtab:1 origin:%s url:%s",
5074 cstr_String(id_Widget(w)), 5102 cstr_String(id_Widget(w)),
5075 cstr_String(linkUrl)) }, 5103 cstr_String(linkUrl)) },
5076 { openTabBg_Icon " ${link.newtab.background}", 5104 { openTabBg_Icon " ${link.newtab.background}",
5077 0, 5105 0,
5078 0, 5106 0,
5079 format_CStr("!open newtab:2 origin:%s url:%s", 5107 format_CStr("!open newtab:2 origin:%s url:%s",
5080 cstr_String(id_Widget(w)), 5108 cstr_String(id_Widget(w)),
5081 cstr_String(linkUrl)) }, 5109 cstr_String(linkUrl)) },
5082 { "${link.side}", 5110 { openWindow_Icon " ${link.newwindow}",
5083 0, 5111 0,
5084 0, 5112 0,
5085 format_CStr("!open newtab:4 origin:%s url:%s", 5113 format_CStr("!open newwindow:1 origin:%s url:%s",
5086 cstr_String(id_Widget(w)), 5114 cstr_String(id_Widget(w)),
5087 cstr_String(linkUrl)) }, 5115 cstr_String(linkUrl)) },
5088 { "${link.side.newtab}", 5116 { "${link.side}",
5089 0, 5117 0,
5090 0, 5118 0,
5091 format_CStr("!open newtab:5 origin:%s url:%s", 5119 format_CStr("!open newtab:4 origin:%s url:%s",
5092 cstr_String(id_Widget(w)), 5120 cstr_String(id_Widget(w)),
5093 cstr_String(linkUrl)) } }, 5121 cstr_String(linkUrl)) },
5094 4); 5122 { "${link.side.newtab}",
5123 0,
5124 0,
5125 format_CStr("!open newtab:5 origin:%s url:%s",
5126 cstr_String(id_Widget(w)),
5127 cstr_String(linkUrl)) },
5128 },
5129 5);
5095 if (deviceType_App() == phone_AppDeviceType) { 5130 if (deviceType_App() == phone_AppDeviceType) {
5096 removeN_Array(&items, size_Array(&items) - 2, iInvalidSize); 5131 /* Phones don't do windows or splits. */
5132 removeN_Array(&items, size_Array(&items) - 3, iInvalidSize);
5133 }
5134 else if (deviceType_App() == tablet_AppDeviceType) {
5135 /* Tablets only do splits. */
5136 removeN_Array(&items, size_Array(&items) - 3, 1);
5097 } 5137 }
5098 if (equalCase_Rangecc(scheme, "file")) { 5138 if (equalCase_Rangecc(scheme, "file")) {
5099 pushBack_Array(&items, &(iMenuItem){ "---" }); 5139 pushBack_Array(&items, &(iMenuItem){ "---" });
@@ -5248,6 +5288,11 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
5248 "document.upload", 5288 "document.upload",
5249 !equalCase_Rangecc(urlScheme_String(d->mod.url), "gemini") && 5289 !equalCase_Rangecc(urlScheme_String(d->mod.url), "gemini") &&
5250 !equalCase_Rangecc(urlScheme_String(d->mod.url), "titan")); 5290 !equalCase_Rangecc(urlScheme_String(d->mod.url), "titan"));
5291 setMenuItemDisabled_Widget(
5292 d->menu,
5293 "document.upload copy:1",
5294 !equalCase_Rangecc(urlScheme_String(d->mod.url), "gemini") &&
5295 !equalCase_Rangecc(urlScheme_String(d->mod.url), "titan"));
5251 } 5296 }
5252 processContextMenuEvent_Widget(d->menu, ev, {}); 5297 processContextMenuEvent_Widget(d->menu, ev, {});
5253 } 5298 }
@@ -5545,12 +5590,12 @@ static void prerender_DocumentWidget_(iAny *context) {
5545 } 5590 }
5546 const iDocumentWidget *d = context; 5591 const iDocumentWidget *d = context;
5547 iDrawContext ctx = { 5592 iDrawContext ctx = {
5548 .view = &d->view, 5593 .view = &d->view,
5549 .docBounds = documentBounds_DocumentView_(&d->view), 5594 .docBounds = documentBounds_DocumentView_(&d->view),
5550 .vis = visibleRange_DocumentView_(&d->view), 5595 .vis = visibleRange_DocumentView_(&d->view),
5551 .showLinkNumbers = (d->flags & showLinkNumbers_DocumentWidgetFlag) != 0 5596 .showLinkNumbers = (d->flags & showLinkNumbers_DocumentWidgetFlag) != 0
5552 }; 5597 };
5553 // printf("%u prerendering\n", SDL_GetTicks()); 5598 // printf("%u prerendering\n", SDL_GetTicks());
5554 if (d->view.visBuf->buffers[0].texture) { 5599 if (d->view.visBuf->buffers[0].texture) {
5555 makePaletteGlobal_GmDocument(d->view.doc); 5600 makePaletteGlobal_GmDocument(d->view.doc);
5556 if (render_DocumentView_(&d->view, &ctx, iTrue /* just fill up progressively */)) { 5601 if (render_DocumentView_(&d->view, &ctx, iTrue /* just fill up progressively */)) {
diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c
index 6a8d428a..1b68ff57 100644
--- a/src/ui/inputwidget.c
+++ b/src/ui/inputwidget.c
@@ -1201,6 +1201,11 @@ void selectAll_InputWidget(iInputWidget *d) {
1201#endif 1201#endif
1202} 1202}
1203 1203
1204void deselect_InputWidget(iInputWidget *d) {
1205 iZap(d->mark);
1206 refresh_Widget(as_Widget(d));
1207}
1208
1204void validate_InputWidget(iInputWidget *d) { 1209void validate_InputWidget(iInputWidget *d) {
1205 if (d->validator) { 1210 if (d->validator) {
1206 d->validator(d, d->validatorContext); /* this may change the contents */ 1211 d->validator(d, d->validatorContext); /* this may change the contents */
diff --git a/src/ui/inputwidget.h b/src/ui/inputwidget.h
index 000fa4b7..832f7853 100644
--- a/src/ui/inputwidget.h
+++ b/src/ui/inputwidget.h
@@ -59,6 +59,7 @@ void setBackupFileName_InputWidget (iInputWidget *, const char *fileName);
59void begin_InputWidget (iInputWidget *); 59void begin_InputWidget (iInputWidget *);
60void end_InputWidget (iInputWidget *, iBool accept); 60void end_InputWidget (iInputWidget *, iBool accept);
61void selectAll_InputWidget (iInputWidget *); 61void selectAll_InputWidget (iInputWidget *);
62void deselect_InputWidget (iInputWidget *);
62void validate_InputWidget (iInputWidget *); 63void validate_InputWidget (iInputWidget *);
63 64
64void setSelectAllOnFocus_InputWidget (iInputWidget *, iBool selectAllOnFocus); 65void setSelectAllOnFocus_InputWidget (iInputWidget *, iBool selectAllOnFocus);
diff --git a/src/ui/keys.c b/src/ui/keys.c
index 26a286bc..88efa98b 100644
--- a/src/ui/keys.c
+++ b/src/ui/keys.c
@@ -243,6 +243,8 @@ static const struct { int id; iMenuItem bind; int flags; } defaultBindings_[] =
243 { 110,{ "${menu.save.downloads}", SDLK_s, KMOD_PRIMARY, "document.save" }, 0 }, 243 { 110,{ "${menu.save.downloads}", SDLK_s, KMOD_PRIMARY, "document.save" }, 0 },
244 { 120,{ "${keys.upload}", SDLK_u, KMOD_PRIMARY, "document.upload" }, 0 }, 244 { 120,{ "${keys.upload}", SDLK_u, KMOD_PRIMARY, "document.upload" }, 0 },
245 { 121,{ "${keys.upload.edit}", SDLK_e, KMOD_PRIMARY, "document.upload copy:1" }, 0 }, 245 { 121,{ "${keys.upload.edit}", SDLK_e, KMOD_PRIMARY, "document.upload copy:1" }, 0 },
246 { 125,{ "${keys.pageinfo}", SDLK_i, KMOD_PRIMARY, "document.info" }, 0 },
247 { 126,{ "${keys.sitespec}", ',', KMOD_PRIMARY | KMOD_SHIFT, "document.sitespec" }, 0 },
246 { 130,{ "${keys.input.precedingline}", SDLK_v, KMOD_PRIMARY | KMOD_SHIFT, "input.precedingline" }, 0 }, 248 { 130,{ "${keys.input.precedingline}", SDLK_v, KMOD_PRIMARY | KMOD_SHIFT, "input.precedingline" }, 0 },
247 /* The following cannot currently be changed (built-in duplicates). */ 249 /* The following cannot currently be changed (built-in duplicates). */
248#if defined (iPlatformApple) 250#if defined (iPlatformApple)
diff --git a/src/ui/labelwidget.c b/src/ui/labelwidget.c
index 3454014a..75cbbf3a 100644
--- a/src/ui/labelwidget.c
+++ b/src/ui/labelwidget.c
@@ -231,7 +231,17 @@ static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int
231 *bg = uiBackgroundUnfocusedSelection_ColorId; 231 *bg = uiBackgroundUnfocusedSelection_ColorId;
232 } 232 }
233 else { 233 else {
234 *bg = uiBackgroundSelected_ColorId; 234 const enum iGmDocumentTheme docTheme = docTheme_Prefs(prefs_App());
235 if ((docTheme == colorfulLight_GmDocumentTheme || docTheme == sepia_GmDocumentTheme) &&
236 !cmp_String(&d->widget.parent->id, "tabs.buttons")) {
237 *bg = (docTheme == sepia_GmDocumentTheme &&
238 colorTheme_App() == pureWhite_ColorTheme
239 ? tmBackground_ColorId
240 : tmBannerBackground_ColorId);
241 }
242 else {
243 *bg = uiBackgroundSelected_ColorId;
244 }
235 } 245 }
236 if (!isKeyRoot) { 246 if (!isKeyRoot) {
237 *bg = isDark_ColorTheme(colorTheme_App()) ? uiBackgroundUnfocusedSelection_ColorId 247 *bg = isDark_ColorTheme(colorTheme_App()) ? uiBackgroundUnfocusedSelection_ColorId
diff --git a/src/ui/linkinfo.c b/src/ui/linkinfo.c
index 36ab00c8..15aea16e 100644
--- a/src/ui/linkinfo.c
+++ b/src/ui/linkinfo.c
@@ -91,6 +91,10 @@ void infoText_LinkInfo(const iGmDocument *doc, iGmLinkId linkId, iString *text_o
91 appendCStr_String(text_out, "\x1b[0m"); 91 appendCStr_String(text_out, "\x1b[0m");
92 appendRange_String(text_out, (iRangecc){ parts.path.start, constEnd_String(url) }); 92 appendRange_String(text_out, (iRangecc){ parts.path.start, constEnd_String(url) });
93 } 93 }
94 else if (scheme == data_GmLinkScheme) {
95 appendCStr_String(text_out, paperclip_Icon " ");
96 append_String(text_out, prettyDataUrl_String(url, none_ColorId));
97 }
94 else if (scheme != gemini_GmLinkScheme) { 98 else if (scheme != gemini_GmLinkScheme) {
95 const size_t maxDispLen = 300; 99 const size_t maxDispLen = 300;
96 appendCStr_String(text_out, scheme == file_GmLinkScheme ? "" : globe_Icon " "); 100 appendCStr_String(text_out, scheme == file_GmLinkScheme ? "" : globe_Icon " ");
diff --git a/src/ui/lookupwidget.c b/src/ui/lookupwidget.c
index f14170ad..dc3264a2 100644
--- a/src/ui/lookupwidget.c
+++ b/src/ui/lookupwidget.c
@@ -568,7 +568,7 @@ static void presentResults_LookupWidget_(iLookupWidget *d) {
568 cstr_String(&res->label), 568 cstr_String(&res->label),
569 uiText_ColorEscape, 569 uiText_ColorEscape,
570 cstr_String(&res->meta)); 570 cstr_String(&res->meta));
571 const iString *cmd = feedEntryOpenCommand_String(&res->url, 0); 571 const iString *cmd = feedEntryOpenCommand_String(&res->url, 0, 0);
572 if (cmd) { 572 if (cmd) {
573 set_String(&item->command, cmd); 573 set_String(&item->command, cmd);
574 } 574 }
diff --git a/src/ui/root.c b/src/ui/root.c
index 6e187313..9dee50ae 100644
--- a/src/ui/root.c
+++ b/src/ui/root.c
@@ -56,7 +56,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
56#if defined (iPlatformPcDesktop) 56#if defined (iPlatformPcDesktop)
57/* TODO: Submenus wouldn't hurt here. */ 57/* TODO: Submenus wouldn't hurt here. */
58static const iMenuItem navMenuItems_[] = { 58static const iMenuItem navMenuItems_[] = {
59 { add_Icon " ${menu.newtab}", 't', KMOD_PRIMARY, "tabs.new" }, 59 { openWindow_Icon " ${menu.newwindow}", SDLK_n, KMOD_PRIMARY, "window.new" },
60 { add_Icon " ${menu.newtab}", SDLK_t, KMOD_PRIMARY, "tabs.new" },
60 { "${menu.openlocation}", SDLK_l, KMOD_PRIMARY, "navigate.focus" }, 61 { "${menu.openlocation}", SDLK_l, KMOD_PRIMARY, "navigate.focus" },
61 { "---" }, 62 { "---" },
62 { download_Icon " " saveToDownloads_Label, SDLK_s, KMOD_PRIMARY, "document.save" }, 63 { download_Icon " " saveToDownloads_Label, SDLK_s, KMOD_PRIMARY, "document.save" },
@@ -468,6 +469,10 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) {
468 return iFalse; 469 return iFalse;
469 } 470 }
470 else if (equal_Command(cmd, "window.setrect")) { 471 else if (equal_Command(cmd, "window.setrect")) {
472 if (hasLabel_Command(cmd, "index") &&
473 argU32Label_Command(cmd, "index") != windowIndex_Root(root->root)) {
474 return iFalse;
475 }
471 const int snap = argLabel_Command(cmd, "snap"); 476 const int snap = argLabel_Command(cmd, "snap");
472 if (snap) { 477 if (snap) {
473 iMainWindow *window = get_MainWindow(); 478 iMainWindow *window = get_MainWindow();
@@ -1059,6 +1064,8 @@ static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) {
1059 updateNavBarIdentity_(navBar); 1064 updateNavBarIdentity_(navBar);
1060 } 1065 }
1061 setFocus_Widget(NULL); 1066 setFocus_Widget(NULL);
1067 makePaletteGlobal_GmDocument(document_DocumentWidget(doc));
1068 refresh_Widget(findWidget_Root("doctabs"));
1062 } 1069 }
1063 else if (equal_Command(cmd, "mouse.clicked") && arg_Command(cmd)) { 1070 else if (equal_Command(cmd, "mouse.clicked") && arg_Command(cmd)) {
1064 iWidget *widget = pointer_Command(cmd); 1071 iWidget *widget = pointer_Command(cmd);
@@ -1774,6 +1781,13 @@ void showToolbar_Root(iRoot *d, iBool show) {
1774 } 1781 }
1775} 1782}
1776 1783
1784size_t windowIndex_Root(const iRoot *d) {
1785 if (type_Window(d->window) == main_WindowType) {
1786 return windowIndex_App(as_MainWindow(d->window));
1787 }
1788 return iInvalidPos;
1789}
1790
1777iInt2 size_Root(const iRoot *d) { 1791iInt2 size_Root(const iRoot *d) {
1778 return d && d->widget ? d->widget->rect.size : zero_I2(); 1792 return d && d->widget ? d->widget->rect.size : zero_I2();
1779} 1793}
diff --git a/src/ui/root.h b/src/ui/root.h
index a81ebdf7..3b053c9e 100644
--- a/src/ui/root.h
+++ b/src/ui/root.h
@@ -47,6 +47,7 @@ void showOrHideNewTabButton_Root (iRoot *);
47 47
48void notifyVisualOffsetChange_Root (iRoot *); 48void notifyVisualOffsetChange_Root (iRoot *);
49 49
50size_t windowIndex_Root (const iRoot *);
50iInt2 size_Root (const iRoot *); 51iInt2 size_Root (const iRoot *);
51iRect rect_Root (const iRoot *); 52iRect rect_Root (const iRoot *);
52iRect safeRect_Root (const iRoot *); 53iRect safeRect_Root (const iRoot *);
diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c
index da377ac2..8a96961a 100644
--- a/src/ui/sidebarwidget.c
+++ b/src/ui/sidebarwidget.c
@@ -412,19 +412,25 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct
412 setOutline_LabelWidget(child_Widget(d->actions, 1), d->feedsMode != all_FeedsMode); 412 setOutline_LabelWidget(child_Widget(d->actions, 1), d->feedsMode != all_FeedsMode);
413 setOutline_LabelWidget(child_Widget(d->actions, 2), d->feedsMode != unread_FeedsMode); 413 setOutline_LabelWidget(child_Widget(d->actions, 2), d->feedsMode != unread_FeedsMode);
414 } 414 }
415 d->menu = makeMenu_Widget( 415 const iMenuItem menuItems[] = {
416 as_Widget(d), 416 { openTab_Icon " ${menu.opentab}", 0, 0, "feed.entry.open newtab:1" },
417 (iMenuItem[]){ { openTab_Icon " ${feeds.entry.newtab}", 0, 0, "feed.entry.opentab" }, 417 { openTabBg_Icon " ${menu.opentab.background}", 0, 0, "feed.entry.open newtab:2" },
418 { circle_Icon " ${feeds.entry.markread}", 0, 0, "feed.entry.toggleread" }, 418#if defined (iPlatformDesktop)
419 { bookmark_Icon " ${feeds.entry.bookmark}", 0, 0, "feed.entry.bookmark" }, 419 { openWindow_Icon " ${menu.openwindow}", 0, 0, "feed.entry.open newwindow:1" },
420 { "---", 0, 0, NULL }, 420#endif
421 { page_Icon " ${feeds.entry.openfeed}", 0, 0, "feed.entry.openfeed" }, 421 { "---", 0, 0, NULL },
422 { edit_Icon " ${feeds.edit}", 0, 0, "feed.entry.edit" }, 422 { circle_Icon " ${feeds.entry.markread}", 0, 0, "feed.entry.toggleread" },
423 { whiteStar_Icon " " uiTextCaution_ColorEscape "${feeds.unsubscribe}", 0, 0, "feed.entry.unsubscribe" }, 423 { bookmark_Icon " ${feeds.entry.bookmark}", 0, 0, "feed.entry.bookmark" },
424 { "---", 0, 0, NULL }, 424 { "${menu.copyurl}", 0, 0, "feed.entry.copy" },
425 { check_Icon " ${feeds.markallread}", SDLK_a, KMOD_SHIFT, "feeds.markallread" }, 425 { "---", 0, 0, NULL },
426 { reload_Icon " ${feeds.refresh}", SDLK_r, KMOD_PRIMARY | KMOD_SHIFT, "feeds.refresh" } }, 426 { page_Icon " ${feeds.entry.openfeed}", 0, 0, "feed.entry.openfeed" },
427 10); 427 { edit_Icon " ${feeds.edit}", 0, 0, "feed.entry.edit" },
428 { whiteStar_Icon " " uiTextCaution_ColorEscape "${feeds.unsubscribe}", 0, 0, "feed.entry.unsubscribe" },
429 { "---", 0, 0, NULL },
430 { check_Icon " ${feeds.markallread}", SDLK_a, KMOD_SHIFT, "feeds.markallread" },
431 { reload_Icon " ${feeds.refresh}", SDLK_r, KMOD_PRIMARY | KMOD_SHIFT, "feeds.refresh" }
432 };
433 d->menu = makeMenu_Widget(as_Widget(d), menuItems, iElemCount(menuItems));
428 d->modeMenu = makeMenu_Widget( 434 d->modeMenu = makeMenu_Widget(
429 as_Widget(d), 435 as_Widget(d),
430 (iMenuItem[]){ 436 (iMenuItem[]){
@@ -487,26 +493,29 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct
487 addItem_ListWidget(d->list, item); 493 addItem_ListWidget(d->list, item);
488 iRelease(item); 494 iRelease(item);
489 } 495 }
490 d->menu = makeMenu_Widget( 496 const iMenuItem menuItems[] = {
491 as_Widget(d), 497 { openTab_Icon " ${menu.opentab}", 0, 0, "bookmark.open newtab:1" },
492 (iMenuItem[]){ { openTab_Icon " ${menu.opentab}", 0, 0, "bookmark.open newtab:1" }, 498 { openTabBg_Icon " ${menu.opentab.background}", 0, 0, "bookmark.open newtab:2" },
493 { openTabBg_Icon " ${menu.opentab.background}", 0, 0, "bookmark.open newtab:2" }, 499#if defined (iPlatformDesktop)
494 { "---", 0, 0, NULL }, 500 { openWindow_Icon " ${menu.openwindow}", 0, 0, "bookmark.open newwindow:1" },
495 { edit_Icon " ${menu.edit}", 0, 0, "bookmark.edit" }, 501#endif
496 { copy_Icon " ${menu.dup}", 0, 0, "bookmark.dup" }, 502 { "---", 0, 0, NULL },
497 { "${menu.copyurl}", 0, 0, "bookmark.copy" }, 503 { edit_Icon " ${menu.edit}", 0, 0, "bookmark.edit" },
498 { "---", 0, 0, NULL }, 504 { copy_Icon " ${menu.dup}", 0, 0, "bookmark.dup" },
499 { "", 0, 0, "bookmark.tag tag:subscribed" }, 505 { "${menu.copyurl}", 0, 0, "bookmark.copy" },
500 { "", 0, 0, "bookmark.tag tag:homepage" }, 506 { "---", 0, 0, NULL },
501 { "", 0, 0, "bookmark.tag tag:remotesource" }, 507 { "", 0, 0, "bookmark.tag tag:subscribed" },
502 { "---", 0, 0, NULL }, 508 { "", 0, 0, "bookmark.tag tag:homepage" },
503 { delete_Icon " " uiTextCaution_ColorEscape "${bookmark.delete}", 0, 0, "bookmark.delete" }, 509 { "", 0, 0, "bookmark.tag tag:remotesource" },
504 { "---", 0, 0, NULL }, 510 { "---", 0, 0, NULL },
505 { add_Icon " ${menu.newfolder}", 0, 0, "bookmark.addfolder" }, 511 { delete_Icon " " uiTextCaution_ColorEscape "${bookmark.delete}", 0, 0, "bookmark.delete" },
506 { upDownArrow_Icon " ${menu.sort.alpha}", 0, 0, "bookmark.sortfolder" }, 512 { "---", 0, 0, NULL },
507 { "---", 0, 0, NULL }, 513 { folder_Icon " ${menu.newfolder}", 0, 0, "bookmark.addfolder" },
508 { reload_Icon " ${bookmarks.reload}", 0, 0, "bookmarks.reload.remote" } }, 514 { upDownArrow_Icon " ${menu.sort.alpha}", 0, 0, "bookmark.sortfolder" },
509 17); 515 { "---", 0, 0, NULL },
516 { reload_Icon " ${bookmarks.reload}", 0, 0, "bookmarks.reload.remote" }
517 };
518 d->menu = makeMenu_Widget(as_Widget(d), menuItems, iElemCount(menuItems));
510 d->modeMenu = makeMenu_Widget( 519 d->modeMenu = makeMenu_Widget(
511 as_Widget(d), 520 as_Widget(d),
512 (iMenuItem[]){ { bookmark_Icon " ${menu.page.bookmark}", SDLK_d, KMOD_PRIMARY, "bookmark.add" }, 521 (iMenuItem[]){ { bookmark_Icon " ${menu.page.bookmark}", SDLK_d, KMOD_PRIMARY, "bookmark.add" },
@@ -520,7 +529,7 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct
520 addActionButton_SidebarWidget_(d, "${sidebar.action.bookmarks.newfolder}", 529 addActionButton_SidebarWidget_(d, "${sidebar.action.bookmarks.newfolder}",
521 "bookmarks.addfolder", !d->isEditing ? hidden_WidgetFlag : 0); 530 "bookmarks.addfolder", !d->isEditing ? hidden_WidgetFlag : 0);
522 addChildFlags_Widget(d->actions, iClob(new_Widget()), expand_WidgetFlag); 531 addChildFlags_Widget(d->actions, iClob(new_Widget()), expand_WidgetFlag);
523 iLabelWidget *btn = addActionButton_SidebarWidget_(d, 532 addActionButton_SidebarWidget_(d,
524 d->isEditing ? "${sidebar.close}" : "${sidebar.action.bookmarks.edit}", 533 d->isEditing ? "${sidebar.close}" : "${sidebar.action.bookmarks.edit}",
525 "sidebar.bookmarks.edit", 0); 534 "sidebar.bookmarks.edit", 0);
526 } 535 }
@@ -568,16 +577,21 @@ static void updateItemsWithFlags_SidebarWidget_(iSidebarWidget *d, iBool keepAct
568 addItem_ListWidget(d->list, item); 577 addItem_ListWidget(d->list, item);
569 iRelease(item); 578 iRelease(item);
570 } 579 }
571 d->menu = makeMenu_Widget( 580 const iMenuItem menuItems[] = {
572 as_Widget(d), 581 { openTab_Icon " ${menu.opentab}", 0, 0, "history.open newtab:1" },
573 (iMenuItem[]){ 582 { openTabBg_Icon " ${menu.opentab.background}", 0, 0, "history.open newtab:2" },
574 { "${menu.copyurl}", 0, 0, "history.copy" }, 583#if defined (iPlatformDesktop)
575 { bookmark_Icon " ${sidebar.entry.bookmark}", 0, 0, "history.addbookmark" }, 584 { openWindow_Icon " ${menu.openwindow}", 0, 0, "history.open newwindow:1" },
576 { "---", 0, 0, NULL }, 585#endif
577 { close_Icon " ${menu.forgeturl}", 0, 0, "history.delete" }, 586 { "---" },
578 { "---", 0, 0, NULL }, 587 { bookmark_Icon " ${sidebar.entry.bookmark}", 0, 0, "history.addbookmark" },
579 { delete_Icon " " uiTextCaution_ColorEscape "${history.clear}", 0, 0, "history.clear confirm:1" }, 588 { "${menu.copyurl}", 0, 0, "history.copy" },
580 }, 6); 589 { "---", 0, 0, NULL },
590 { close_Icon " ${menu.forgeturl}", 0, 0, "history.delete" },
591 { "---", 0, 0, NULL },
592 { delete_Icon " " uiTextCaution_ColorEscape "${history.clear}", 0, 0, "history.clear confirm:1" },
593 };
594 d->menu = makeMenu_Widget(as_Widget(d), menuItems, iElemCount(menuItems));
581 d->modeMenu = makeMenu_Widget( 595 d->modeMenu = makeMenu_Widget(
582 as_Widget(d), 596 as_Widget(d),
583 (iMenuItem[]){ 597 (iMenuItem[]){
@@ -981,7 +995,7 @@ static void itemClicked_SidebarWidget_(iSidebarWidget *d, iSidebarItem *item, si
981 } 995 }
982 case feeds_SidebarMode: { 996 case feeds_SidebarMode: {
983 postCommandString_Root(get_Root(), 997 postCommandString_Root(get_Root(),
984 feedEntryOpenCommand_String(&item->url, openTabMode_Sym(modState_Keys()))); 998 feedEntryOpenCommand_String(&item->url, openTabMode_Sym(modState_Keys()), 0));
985 break; 999 break;
986 } 1000 }
987 case bookmarks_SidebarMode: 1001 case bookmarks_SidebarMode:
@@ -1641,11 +1655,20 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1641 else if (startsWith_CStr(cmd, "feed.entry.") && d->mode == feeds_SidebarMode) { 1655 else if (startsWith_CStr(cmd, "feed.entry.") && d->mode == feeds_SidebarMode) {
1642 const iSidebarItem *item = d->contextItem; 1656 const iSidebarItem *item = d->contextItem;
1643 if (item) { 1657 if (item) {
1644 if (isCommand_Widget(w, ev, "feed.entry.opentab")) { 1658 if (isCommand_Widget(w, ev, "feed.entry.open")) {
1645 postCommandString_Root(get_Root(), feedEntryOpenCommand_String(&item->url, 1)); 1659 const char *cmd = command_UserEvent(ev);
1660 postCommandString_Root(
1661 get_Root(),
1662 feedEntryOpenCommand_String(&item->url,
1663 argLabel_Command(cmd, "newtab"),
1664 argLabel_Command(cmd, "newwindow")));
1646 return iTrue; 1665 return iTrue;
1647 } 1666 }
1648 if (isCommand_Widget(w, ev, "feed.entry.toggleread")) { 1667 else if (isCommand_Widget(w, ev, "feed.entry.copy")) {
1668 SDL_SetClipboardText(cstr_String(&item->url));
1669 return iTrue;
1670 }
1671 else if (isCommand_Widget(w, ev, "feed.entry.toggleread")) {
1649 iVisited *vis = visited_App(); 1672 iVisited *vis = visited_App();
1650 const iString *url = urlFragmentStripped_String(&item->url); 1673 const iString *url = urlFragmentStripped_String(&item->url);
1651 if (containsUrl_Visited(vis, url)) { 1674 if (containsUrl_Visited(vis, url)) {
@@ -1657,7 +1680,7 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1657 postCommand_App("visited.changed"); 1680 postCommand_App("visited.changed");
1658 return iTrue; 1681 return iTrue;
1659 } 1682 }
1660 if (isCommand_Widget(w, ev, "feed.entry.bookmark")) { 1683 else if (isCommand_Widget(w, ev, "feed.entry.bookmark")) {
1661 makeBookmarkCreation_Widget(&item->url, &item->label, item->icon); 1684 makeBookmarkCreation_Widget(&item->url, &item->label, item->icon);
1662 if (deviceType_App() == desktop_AppDeviceType) { 1685 if (deviceType_App() == desktop_AppDeviceType) {
1663 postCommand_App("focus.set id:bmed.title"); 1686 postCommand_App("focus.set id:bmed.title");
@@ -1706,6 +1729,18 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1706 } 1729 }
1707 return iTrue; 1730 return iTrue;
1708 } 1731 }
1732 else if (isCommand_Widget(w, ev, "history.open")) {
1733 const iSidebarItem *item = d->contextItem;
1734 if (item && !isEmpty_String(&item->url)) {
1735 const char *cmd = command_UserEvent(ev);
1736 postCommand_Widget(d,
1737 "!open newtab:%d newwindow:%d url:%s",
1738 argLabel_Command(cmd, "newtab"),
1739 argLabel_Command(cmd, "newwindow"),
1740 cstr_String(&item->url));
1741 }
1742 return iTrue;
1743 }
1709 else if (isCommand_Widget(w, ev, "history.copy")) { 1744 else if (isCommand_Widget(w, ev, "history.copy")) {
1710 const iSidebarItem *item = d->contextItem; 1745 const iSidebarItem *item = d->contextItem;
1711 if (item && !isEmpty_String(&item->url)) { 1746 if (item && !isEmpty_String(&item->url)) {
@@ -2156,28 +2191,38 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,
2156 : uiTextDim_ColorId; 2191 : uiTextDim_ColorId;
2157 iUrl parts; 2192 iUrl parts;
2158 init_Url(&parts, &d->label); 2193 init_Url(&parts, &d->label);
2159 const iBool isAbout = equalCase_Rangecc(parts.scheme, "about"); 2194 const iBool isAbout = equalCase_Rangecc(parts.scheme, "about");
2160 const iBool isGemini = equalCase_Rangecc(parts.scheme, "gemini"); 2195 const iBool isGemini = equalCase_Rangecc(parts.scheme, "gemini");
2161 draw_Text(font, 2196 const iBool isData = equalCase_Rangecc(parts.scheme, "data");
2162 add_I2(topLeft_Rect(itemRect), 2197 const int queryColor = isPressing ? uiTextPressed_ColorId
2163 init_I2(3 * gap_UI, (itemHeight - lineHeight_Text(font)) / 2)), 2198 : isHover ? uiText_ColorId
2164 fg, 2199 : uiAnnotation_ColorId;
2165 "%s%s%s%s%s%s%s%s", 2200 const iInt2 textPos =
2166 isGemini ? "" : cstr_Rangecc(parts.scheme), 2201 add_I2(topLeft_Rect(itemRect),
2167 isGemini ? "" 2202 init_I2(3 * gap_UI, (itemHeight - lineHeight_Text(font)) / 2));
2168 : isAbout ? ":" 2203 if (isData) {
2169 : "://", 2204 drawRange_Text(
2170 escape_Color(isHover ? (isPressing ? uiTextPressed_ColorId 2205 font, textPos, fg, range_String(prettyDataUrl_String(&d->label, queryColor)));
2171 : uiTextFramelessHover_ColorId) 2206 }
2172 : uiTextStrong_ColorId), 2207 else {
2173 cstr_Rangecc(parts.host), 2208 draw_Text(
2174 escape_Color(fg), 2209 font,
2175 cstr_Rangecc(parts.path), 2210 textPos,
2176 !isEmpty_Range(&parts.query) ? escape_Color(isPressing ? uiTextPressed_ColorId 2211 fg,
2177 : isHover ? uiText_ColorId 2212 "%s%s%s%s%s%s%s%s",
2178 : uiAnnotation_ColorId) 2213 isGemini ? "" : cstr_Rangecc(parts.scheme),
2179 : "", 2214 isGemini ? ""
2180 !isEmpty_Range(&parts.query) ? cstr_Rangecc(parts.query) : ""); 2215 : isAbout ? ":"
2216 : "://",
2217 escape_Color(isHover ? (isPressing ? uiTextPressed_ColorId
2218 : uiTextFramelessHover_ColorId)
2219 : uiTextStrong_ColorId),
2220 cstr_Rangecc(parts.host),
2221 escape_Color(fg),
2222 cstr_Rangecc(parts.path),
2223 !isEmpty_Range(&parts.query) ? escape_Color(queryColor) : "",
2224 !isEmpty_Range(&parts.query) ? cstr_Rangecc(parts.query) : "");
2225 }
2181 } 2226 }
2182 iEndCollect(); 2227 iEndCollect();
2183 } 2228 }
diff --git a/src/ui/text.c b/src/ui/text.c
index c19aed2f..83e87d0c 100644
--- a/src/ui/text.c
+++ b/src/ui/text.c
@@ -258,8 +258,6 @@ static int cmp_PrioMapItem_(const void *a, const void *b) {
258} 258}
259 259
260struct Impl_Text { 260struct Impl_Text {
261// enum iTextFont contentFont;
262// enum iTextFont headingFont;
263 float contentFontSize; 261 float contentFontSize;
264 iArray fonts; /* fonts currently selected for use (incl. all styles/sizes) */ 262 iArray fonts; /* fonts currently selected for use (incl. all styles/sizes) */
265 int overrideFontId; /* always checked for glyphs first, regardless of which font is used */ 263 int overrideFontId; /* always checked for glyphs first, regardless of which font is used */
@@ -276,7 +274,8 @@ struct Impl_Text {
276 int ansiFlags; 274 int ansiFlags;
277 int baseFontId; /* base attributes (for restoring via escapes) */ 275 int baseFontId; /* base attributes (for restoring via escapes) */
278 int baseFgColorId; 276 int baseFgColorId;
279 iBool missingGlyphs; /* true if a glyph couldn't be found */ 277 iBool missingGlyphs; /* true if a glyph couldn't be found */
278 iChar missingChars[20]; /* rotating buffer of the latest missing characters */
280}; 279};
281 280
282iDefineTypeConstructionArgs(Text, (SDL_Renderer *render), render) 281iDefineTypeConstructionArgs(Text, (SDL_Renderer *render), render)
@@ -296,6 +295,7 @@ static void setupFontVariants_Text_(iText *d, const iFontSpec *spec, int baseId)
296 /* This is the highest priority override font. */ 295 /* This is the highest priority override font. */
297 d->overrideFontId = baseId; 296 d->overrideFontId = baseId;
298 } 297 }
298 iAssert(activeText_ == d);
299 pushBack_Array(&d->fontPriorityOrder, &(iPrioMapItem){ spec->priority, baseId }); 299 pushBack_Array(&d->fontPriorityOrder, &(iPrioMapItem){ spec->priority, baseId });
300 for (enum iFontStyle style = 0; style < max_FontStyle; style++) { 300 for (enum iFontStyle style = 0; style < max_FontStyle; style++) {
301 for (enum iFontSize sizeId = 0; sizeId < max_FontSize; sizeId++) { 301 for (enum iFontSize sizeId = 0; sizeId < max_FontSize; sizeId++) {
@@ -357,6 +357,8 @@ static void initFonts_Text_(iText *d) {
357 printf("[Text] %zu font variants ready\n", size_Array(&d->fonts)); 357 printf("[Text] %zu font variants ready\n", size_Array(&d->fonts));
358#endif 358#endif
359 gap_Text = iRound(gap_UI * d->contentFontSize); 359 gap_Text = iRound(gap_UI * d->contentFontSize);
360// d->missingGlyphs = iFalse;
361// iZap(d->missingChars);
360} 362}
361 363
362static void deinitFonts_Text_(iText *d) { 364static void deinitFonts_Text_(iText *d) {
@@ -424,6 +426,7 @@ void init_Text(iText *d, SDL_Renderer *render) {
424 d->baseFontId = -1; 426 d->baseFontId = -1;
425 d->baseFgColorId = -1; 427 d->baseFgColorId = -1;
426 d->missingGlyphs = iFalse; 428 d->missingGlyphs = iFalse;
429 iZap(d->missingChars);
427 d->render = render; 430 d->render = render;
428 /* A grayscale palette for rasterized glyphs. */ { 431 /* A grayscale palette for rasterized glyphs. */ {
429 SDL_Color colors[256]; 432 SDL_Color colors[256];
@@ -497,10 +500,13 @@ static void resetCache_Text_(iText *d) {
497} 500}
498 501
499void resetFonts_Text(iText *d) { 502void resetFonts_Text(iText *d) {
503 iText *oldActive = activeText_;
504 setCurrent_Text(d); /* some routines rely on the global `activeText_` pointer */
500 deinitFonts_Text_(d); 505 deinitFonts_Text_(d);
501 deinitCache_Text_(d); 506 deinitCache_Text_(d);
502 initCache_Text_(d); 507 initCache_Text_(d);
503 initFonts_Text_(d); 508 initFonts_Text_(d);
509 setCurrent_Text(oldActive);
504} 510}
505 511
506static SDL_Palette *glyphPalette_(void) { 512static SDL_Palette *glyphPalette_(void) {
@@ -610,8 +616,23 @@ iLocalDef iFont *characterFont_Font_(iFont *d, iChar ch, uint32_t *glyphIndex) {
610 } 616 }
611 } 617 }
612 if (!*glyphIndex) { 618 if (!*glyphIndex) {
613 activeText_->missingGlyphs = iTrue; 619 fprintf(stderr, "failed to find %08x (%lc)\n", ch, (int) ch); fflush(stderr);
614 fprintf(stderr, "failed to find %08x (%lc)\n", ch, (int)ch); fflush(stderr); 620 iText *tx = activeText_;
621 tx->missingGlyphs = iTrue;
622 /* Remember a few of the latest missing characters. */
623 iBool gotIt = iFalse;
624 for (size_t i = 0; i < iElemCount(tx->missingChars); i++) {
625 if (tx->missingChars[i] == ch) {
626 gotIt = iTrue;
627 break;
628 }
629 }
630 if (!gotIt) {
631 memmove(tx->missingChars + 1,
632 tx->missingChars,
633 sizeof(tx->missingChars) - sizeof(tx->missingChars[0]));
634 tx->missingChars[0] = ch;
635 }
615 } 636 }
616 return d; 637 return d;
617} 638}
@@ -1459,14 +1480,14 @@ static void evenMonospaceAdvances_GlyphBuffer_(iGlyphBuffer *d, iFont *baseFont)
1459} 1480}
1460 1481
1461static iRect run_Font_(iFont *d, const iRunArgs *args) { 1482static iRect run_Font_(iFont *d, const iRunArgs *args) {
1462 const int mode = args->mode; 1483 const int mode = args->mode;
1463 const iInt2 orig = args->pos; 1484 const iInt2 orig = args->pos;
1464 iRect bounds = { orig, init_I2(0, d->height) }; 1485 iRect bounds = { orig, init_I2(0, d->height) };
1465 float xCursor = 0.0f; 1486 float xCursor = 0.0f;
1466 float yCursor = 0.0f; 1487 float yCursor = 0.0f;
1467 float xCursorMax = 0.0f; 1488 float xCursorMax = 0.0f;
1468 const iBool isMonospaced = isMonospaced_Font(d); 1489 const iBool isMonospaced = isMonospaced_Font(d);
1469 iWrapText *wrap = args->wrap; 1490 iWrapText *wrap = args->wrap;
1470 iAssert(args->text.end >= args->text.start); 1491 iAssert(args->text.end >= args->text.start);
1471 /* Split the text into a number of attributed runs that specify exactly which 1492 /* Split the text into a number of attributed runs that specify exactly which
1472 font is used and other attributes such as color. (HarfBuzz shaping is done 1493 font is used and other attributes such as color. (HarfBuzz shaping is done
@@ -2250,6 +2271,19 @@ iBool checkMissing_Text(void) {
2250 return missing; 2271 return missing;
2251} 2272}
2252 2273
2274iChar missing_Text(size_t index) {
2275 const iText *d = activeText_;
2276 if (index >= iElemCount(d->missingChars)) {
2277 return 0;
2278 }
2279 return d->missingChars[index];
2280}
2281
2282void resetMissing_Text(iText *d) {
2283 d->missingGlyphs = iFalse;
2284 iZap(d->missingChars);
2285}
2286
2253SDL_Texture *glyphCache_Text(void) { 2287SDL_Texture *glyphCache_Text(void) {
2254 return activeText_->cache; 2288 return activeText_->cache;
2255} 2289}
diff --git a/src/ui/text.h b/src/ui/text.h
index b952df84..e741880d 100644
--- a/src/ui/text.h
+++ b/src/ui/text.h
@@ -227,6 +227,8 @@ struct Impl_WrapText {
227iTextMetrics measure_WrapText (iWrapText *, int fontId); 227iTextMetrics measure_WrapText (iWrapText *, int fontId);
228iTextMetrics draw_WrapText (iWrapText *, int fontId, iInt2 pos, int color); 228iTextMetrics draw_WrapText (iWrapText *, int fontId, iInt2 pos, int color);
229 229
230iChar missing_Text (size_t index);
231void resetMissing_Text (iText *);
230iBool checkMissing_Text (void); /* returns the flag, and clears it */ 232iBool checkMissing_Text (void); /* returns the flag, and clears it */
231SDL_Texture * glyphCache_Text (void); 233SDL_Texture * glyphCache_Text (void);
232 234
diff --git a/src/ui/touch.c b/src/ui/touch.c
index a178a913..21a92b80 100644
--- a/src/ui/touch.c
+++ b/src/ui/touch.c
@@ -244,7 +244,8 @@ static void dispatchNotification_Touch_(const iTouch *d, int code) {
244 .timestamp = SDL_GetTicks(), 244 .timestamp = SDL_GetTicks(),
245 .code = code, 245 .code = code,
246 .data1 = d->affinity, 246 .data1 = d->affinity,
247 .data2 = d->affinity->root 247 .data2 = d->affinity->root,
248 .windowID = id_Window(window_Widget(d->affinity)),
248 }); 249 });
249 setCurrent_Root(oldRoot); 250 setCurrent_Root(oldRoot);
250 } 251 }
diff --git a/src/ui/util.c b/src/ui/util.c
index 5dd8a0bd..4f5de7f9 100644
--- a/src/ui/util.c
+++ b/src/ui/util.c
@@ -35,6 +35,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
35#include "keys.h" 35#include "keys.h"
36#include "labelwidget.h" 36#include "labelwidget.h"
37#include "root.h" 37#include "root.h"
38#include "sitespec.h"
38#include "text.h" 39#include "text.h"
39#include "touch.h" 40#include "touch.h"
40#include "widget.h" 41#include "widget.h"
@@ -903,6 +904,7 @@ iWidget *makeMenu_Widget(iWidget *parent, const iMenuItem *items, size_t n) {
903#else 904#else
904 /* Non-native custom popup menu. This may still be displayed inside a separate window. */ 905 /* Non-native custom popup menu. This may still be displayed inside a separate window. */
905 setDrawBufferEnabled_Widget(menu, iTrue); 906 setDrawBufferEnabled_Widget(menu, iTrue);
907 setFrameColor_Widget(menu, uiSeparator_ColorId);
906 setBackgroundColor_Widget(menu, uiBackgroundMenu_ColorId); 908 setBackgroundColor_Widget(menu, uiBackgroundMenu_ColorId);
907 if (deviceType_App() != desktop_AppDeviceType) { 909 if (deviceType_App() != desktop_AppDeviceType) {
908 setPadding1_Widget(menu, 2 * gap_UI); 910 setPadding1_Widget(menu, 2 * gap_UI);
@@ -1084,12 +1086,12 @@ void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, int menuOpenFlags) {
1084 setFlags_Widget(d, hidden_WidgetFlag, iFalse); 1086 setFlags_Widget(d, hidden_WidgetFlag, iFalse);
1085 setFlags_Widget(d, commandOnMouseMiss_WidgetFlag, iTrue); 1087 setFlags_Widget(d, commandOnMouseMiss_WidgetFlag, iTrue);
1086 setFlags_Widget(findChild_Widget(d, "menu.cancel"), disabled_WidgetFlag, iFalse); 1088 setFlags_Widget(findChild_Widget(d, "menu.cancel"), disabled_WidgetFlag, iFalse);
1087 if (!isPortraitPhone) { 1089// if (!isPortraitPhone) {
1088 setFrameColor_Widget(d, uiBackgroundSelected_ColorId); 1090// setFrameColor_Widget(d, uiSeparator_ColorId);
1089 } 1091// }
1090 else { 1092// else {
1091 setFrameColor_Widget(d, none_ColorId); 1093// setFrameColor_Widget(d, none_ColorId);
1092 } 1094// }
1093 arrange_Widget(d); /* need to know the height */ 1095 arrange_Widget(d); /* need to know the height */
1094 iBool allowOverflow = iFalse; 1096 iBool allowOverflow = iFalse;
1095 /* A vertical offset determined by a possible selected label in the menu. */ 1097 /* A vertical offset determined by a possible selected label in the menu. */
@@ -1327,6 +1329,7 @@ int checkContextMenu_Widget(iWidget *menu, const SDL_Event *ev) {
1327iLabelWidget *makeMenuButton_LabelWidget(const char *label, const iMenuItem *items, size_t n) { 1329iLabelWidget *makeMenuButton_LabelWidget(const char *label, const iMenuItem *items, size_t n) {
1328 iLabelWidget *button = new_LabelWidget(label, "menu.open"); 1330 iLabelWidget *button = new_LabelWidget(label, "menu.open");
1329 iWidget *menu = makeMenu_Widget(as_Widget(button), items, n); 1331 iWidget *menu = makeMenu_Widget(as_Widget(button), items, n);
1332 setFrameColor_Widget(menu, uiBackgroundSelected_ColorId);
1330 setId_Widget(menu, "menu"); 1333 setId_Widget(menu, "menu");
1331 return button; 1334 return button;
1332} 1335}
@@ -1383,6 +1386,9 @@ void updateDropdownSelection_LabelWidget(iLabelWidget *dropButton, const char *s
1383 updateText_LabelWidget(dropButton, 1386 updateText_LabelWidget(dropButton,
1384 replaceNewlinesWithDash_(text_LabelWidget(item))); 1387 replaceNewlinesWithDash_(text_LabelWidget(item)));
1385 checkIcon_LabelWidget(dropButton); 1388 checkIcon_LabelWidget(dropButton);
1389 if (!icon_LabelWidget(dropButton)) {
1390 setIcon_LabelWidget(dropButton, icon_LabelWidget(item));
1391 }
1386 } 1392 }
1387 } 1393 }
1388 } 1394 }
@@ -1709,13 +1715,14 @@ iLabelWidget *addDialogTitle_Widget(iWidget *dlg, const char *text, const char *
1709} 1715}
1710 1716
1711static void acceptValueInput_(iWidget *dlg) { 1717static void acceptValueInput_(iWidget *dlg) {
1712 const iInputWidget *input = findChild_Widget(dlg, "input"); 1718 iInputWidget *input = findChild_Widget(dlg, "input");
1713 if (!isEmpty_String(id_Widget(dlg))) { 1719 if (!isEmpty_String(id_Widget(dlg))) {
1714 const iString *val = text_InputWidget(input); 1720 const iString *val = text_InputWidget(input);
1715 postCommandf_App("%s arg:%d value:%s", 1721 postCommandf_App("%s arg:%d value:%s",
1716 cstr_String(id_Widget(dlg)), 1722 cstr_String(id_Widget(dlg)),
1717 toInt_String(val), 1723 toInt_String(val),
1718 cstr_String(val)); 1724 cstr_String(val));
1725 setBackupFileName_InputWidget(input, NULL);
1719 } 1726 }
1720} 1727}
1721 1728
@@ -1779,6 +1786,7 @@ iBool valueInputHandler_(iWidget *dlg, const char *cmd) {
1779 else if (equal_Command(cmd, "valueinput.set")) { 1786 else if (equal_Command(cmd, "valueinput.set")) {
1780 iInputWidget *input = findChild_Widget(dlg, "input"); 1787 iInputWidget *input = findChild_Widget(dlg, "input");
1781 setTextUndoableCStr_InputWidget(input, suffixPtr_Command(cmd, "text"), iTrue); 1788 setTextUndoableCStr_InputWidget(input, suffixPtr_Command(cmd, "text"), iTrue);
1789 deselect_InputWidget(input);
1782 validate_InputWidget(input); 1790 validate_InputWidget(input);
1783 return iTrue; 1791 return iTrue;
1784 } 1792 }
@@ -2495,6 +2503,7 @@ iWidget *makePreferences_Widget(void) {
2495 { "input id:prefs.searchurl url:1 noheading:1" }, 2503 { "input id:prefs.searchurl url:1 noheading:1" },
2496 { "padding" }, 2504 { "padding" },
2497 { "toggle id:prefs.bookmarks.addbottom" }, 2505 { "toggle id:prefs.bookmarks.addbottom" },
2506 { "toggle id:prefs.dataurl.openimages" },
2498 { "toggle id:prefs.archive.openindex" }, 2507 { "toggle id:prefs.archive.openindex" },
2499 { "radio device:1 id:prefs.pinsplit", 0, 0, (const void *) pinSplitItems }, 2508 { "radio device:1 id:prefs.pinsplit", 0, 0, (const void *) pinSplitItems },
2500 { "padding" }, 2509 { "padding" },
@@ -2567,6 +2576,7 @@ iWidget *makePreferences_Widget(void) {
2567 const iMenuItem networkPanelItems[] = { 2576 const iMenuItem networkPanelItems[] = {
2568 { "title id:heading.prefs.network" }, 2577 { "title id:heading.prefs.network" },
2569 { "toggle id:prefs.decodeurls" }, 2578 { "toggle id:prefs.decodeurls" },
2579 { "input id:prefs.urlsize maxlen:10 selectall:1" },
2570 { "padding" }, 2580 { "padding" },
2571 { "input id:prefs.cachesize maxlen:4 selectall:1 unit:mb" }, 2581 { "input id:prefs.cachesize maxlen:4 selectall:1 unit:mb" },
2572 { "input id:prefs.memorysize maxlen:4 selectall:1 unit:mb" }, 2582 { "input id:prefs.memorysize maxlen:4 selectall:1 unit:mb" },
@@ -2640,8 +2650,9 @@ iWidget *makePreferences_Widget(void) {
2640 setUrlContent_InputWidget(searchUrl, iTrue); 2650 setUrlContent_InputWidget(searchUrl, iTrue);
2641 addDialogPadding_(headings, values); 2651 addDialogPadding_(headings, values);
2642 addDialogToggle_(headings, values, "${prefs.hoverlink}", "prefs.hoverlink"); 2652 addDialogToggle_(headings, values, "${prefs.hoverlink}", "prefs.hoverlink");
2643 addDialogToggle_(headings, values, "${prefs.bookmarks.addbottom}", "prefs.bookmarks.addbottom"); 2653 addDialogToggle_(headings, values, "${prefs.dataurl.openimages}", "prefs.dataurl.openimages");
2644 addDialogToggle_(headings, values, "${prefs.archive.openindex}", "prefs.archive.openindex"); 2654 addDialogToggle_(headings, values, "${prefs.archive.openindex}", "prefs.archive.openindex");
2655 addDialogToggle_(headings, values, "${prefs.bookmarks.addbottom}", "prefs.bookmarks.addbottom");
2645 if (deviceType_App() != phone_AppDeviceType) { 2656 if (deviceType_App() != phone_AppDeviceType) {
2646 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.pinsplit}"))); 2657 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.pinsplit}")));
2647 iWidget *pinSplit = new_Widget(); 2658 iWidget *pinSplit = new_Widget();
@@ -2900,6 +2911,7 @@ iWidget *makePreferences_Widget(void) {
2900 appendTwoColumnTabPage_Widget(tabs, "${heading.prefs.network}", '6', &headings, &values); 2911 appendTwoColumnTabPage_Widget(tabs, "${heading.prefs.network}", '6', &headings, &values);
2901 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.decodeurls}"))); 2912 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.decodeurls}")));
2902 addChild_Widget(values, iClob(makeToggle_Widget("prefs.decodeurls"))); 2913 addChild_Widget(values, iClob(makeToggle_Widget("prefs.decodeurls")));
2914 addPrefsInputWithHeading_(headings, values, "prefs.urlsize", iClob(new_InputWidget(10)));
2903 /* Cache size. */ { 2915 /* Cache size. */ {
2904 iInputWidget *cache = new_InputWidget(4); 2916 iInputWidget *cache = new_InputWidget(4);
2905 setSelectAllOnFocus_InputWidget(cache, iTrue); 2917 setSelectAllOnFocus_InputWidget(cache, iTrue);
@@ -3120,7 +3132,7 @@ iWidget *makeBookmarkCreation_Widget(const iString *url, const iString *title, i
3120 3132
3121static iBool handleFeedSettingCommands_(iWidget *dlg, const char *cmd) { 3133static iBool handleFeedSettingCommands_(iWidget *dlg, const char *cmd) {
3122 if (equal_Command(cmd, "cancel")) { 3134 if (equal_Command(cmd, "cancel")) {
3123 setupSheetTransition_Mobile(dlg, iFalse); 3135 setupSheetTransition_Mobile(dlg, 0);
3124 destroy_Widget(dlg); 3136 destroy_Widget(dlg);
3125 return iTrue; 3137 return iTrue;
3126 } 3138 }
@@ -3163,15 +3175,14 @@ static iBool handleFeedSettingCommands_(iWidget *dlg, const char *cmd) {
3163} 3175}
3164 3176
3165iWidget *makeFeedSettings_Widget(uint32_t bookmarkId) { 3177iWidget *makeFeedSettings_Widget(uint32_t bookmarkId) {
3166 const char *headingText = bookmarkId ? uiHeading_ColorEscape "${heading.feedcfg}" 3178 iWidget *dlg;
3167 : uiHeading_ColorEscape "${heading.subscribe}"; 3179 const char *headingText = bookmarkId ? "${heading.feedcfg}" : "${heading.subscribe}";
3168 const iMenuItem actions[] = { { "${cancel}" }, 3180 const iMenuItem actions[] = { { "${cancel}" },
3169 { bookmarkId ? uiTextCaution_ColorEscape "${dlg.feed.save}" 3181 { bookmarkId ? uiTextCaution_ColorEscape "${dlg.feed.save}"
3170 : uiTextCaution_ColorEscape "${dlg.feed.sub}", 3182 : uiTextCaution_ColorEscape "${dlg.feed.sub}",
3171 SDLK_RETURN, 3183 SDLK_RETURN,
3172 KMOD_PRIMARY, 3184 KMOD_PRIMARY,
3173 format_CStr("feedcfg.accept bmid:%d", bookmarkId) } }; 3185 format_CStr("feedcfg.accept bmid:%d", bookmarkId) } };
3174 iWidget *dlg;
3175 if (isUsingPanelLayout_Mobile()) { 3186 if (isUsingPanelLayout_Mobile()) {
3176 const iMenuItem typeItems[] = { 3187 const iMenuItem typeItems[] = {
3177 { "button id:feedcfg.type.gemini label:dlg.feed.type.gemini", 0, 0, "feedcfg.type arg:0" }, 3188 { "button id:feedcfg.type.gemini label:dlg.feed.type.gemini", 0, 0, "feedcfg.type arg:0" },
@@ -3228,6 +3239,111 @@ iWidget *makeFeedSettings_Widget(uint32_t bookmarkId) {
3228 return dlg; 3239 return dlg;
3229} 3240}
3230 3241
3242/*----------------------------------------------------------------------------------------------*/
3243
3244static void siteSpecificThemeChanged_(const iWidget *dlg) {
3245 iDocumentWidget *doc = document_App();
3246 setThemeSeed_GmDocument((iGmDocument *) document_DocumentWidget(doc),
3247 urlPaletteSeed_String(url_DocumentWidget(doc)),
3248 urlThemeSeed_String(url_DocumentWidget(doc)));
3249 postCommand_App("theme.changed");
3250}
3251
3252static const iString *siteSpecificRoot_(const iWidget *dlg) {
3253 return collect_String(suffix_Command(cstr_String(id_Widget(dlg)), "site"));
3254}
3255
3256static void updateSiteSpecificTheme_(iInputWidget *palSeed, void *context) {
3257 iWidget *dlg = context;
3258 const iString *siteRoot = siteSpecificRoot_(dlg);
3259 setValueString_SiteSpec(siteRoot, paletteSeed_SiteSpecKey, text_InputWidget(palSeed));
3260 siteSpecificThemeChanged_(dlg);
3261 /* Allow seeing the new theme. */
3262 setFlags_Widget(dlg, noFadeBackground_WidgetFlag, iTrue);
3263}
3264
3265static void closeSiteSpecific_(iWidget *dlg) {
3266 setupSheetTransition_Mobile(dlg, 0);
3267 delete_String(userData_Object(dlg)); /* saved original palette seed */
3268 destroy_Widget(dlg);
3269}
3270
3271static iBool siteSpecificSettingsHandler_(iWidget *dlg, const char *cmd) {
3272 if (equal_Command(cmd, "cancel")) {
3273 const iBool wasNoFade = (flags_Widget(dlg) & noFadeBackground_WidgetFlag) != 0;
3274 iInputWidget *palSeed = findChild_Widget(dlg, "sitespec.palette");
3275 setText_InputWidget(palSeed, userData_Object(dlg));
3276 updateSiteSpecificTheme_(palSeed, dlg);
3277 setFlags_Widget(dlg, noFadeBackground_WidgetFlag, wasNoFade);
3278 closeSiteSpecific_(dlg);
3279 return iTrue;
3280 }
3281 if (startsWith_CStr(cmd, "input.ended id:sitespec.palette")) {
3282 setFlags_Widget(dlg, noFadeBackground_WidgetFlag, iFalse);
3283 refresh_Widget(dlg);
3284 siteSpecificThemeChanged_(dlg);
3285 return iTrue;
3286 }
3287 if (equal_Command(cmd, "sitespec.accept")) {
3288 const iInputWidget *palSeed = findChild_Widget(dlg, "sitespec.palette");
3289 const iBool warnAnsi = isSelected_Widget(findChild_Widget(dlg, "sitespec.ansi"));
3290 const iString *siteRoot = siteSpecificRoot_(dlg);
3291 int dismissed = value_SiteSpec(siteRoot, dismissWarnings_SiteSpecKey);
3292 iChangeFlags(dismissed, ansiEscapes_GmDocumentWarning, !warnAnsi);
3293 setValue_SiteSpec(siteRoot, dismissWarnings_SiteSpecKey, dismissed);
3294 setValueString_SiteSpec(siteRoot, paletteSeed_SiteSpecKey, text_InputWidget(palSeed));
3295 siteSpecificThemeChanged_(dlg);
3296 /* Note: The active DocumentWidget may actually be different than when opening the dialog. */
3297 closeSiteSpecific_(dlg);
3298 return iTrue;
3299 }
3300 return iFalse;
3301}
3302
3303iWidget *makeSiteSpecificSettings_Widget(const iString *url) {
3304 iWidget *dlg;
3305 const iMenuItem actions[] = {
3306 { "${cancel}" },
3307 { "${sitespec.accept}", SDLK_RETURN, KMOD_PRIMARY, "sitespec.accept" }
3308 };
3309 if (isUsingPanelLayout_Mobile()) {
3310 iAssert(iFalse);
3311 }
3312 else {
3313 iWidget *headings, *values;
3314 dlg = makeSheet_Widget(format_CStr("sitespec site:%s", cstr_Rangecc(urlRoot_String(url))));
3315 addDialogTitle_(dlg, "${heading.sitespec}", "heading.sitespec");
3316 addChild_Widget(dlg, iClob(makeTwoColumns_Widget(&headings, &values)));
3317 iInputWidget *palSeed = new_InputWidget(0);
3318 setHint_InputWidget(palSeed, cstr_Block(urlThemeSeed_String(url)));
3319 addPrefsInputWithHeading_(headings, values, "sitespec.palette", iClob(palSeed));
3320 addDialogToggle_(headings, values, "${sitespec.ansi}", "sitespec.ansi");
3321 addChild_Widget(dlg, iClob(makeDialogButtons_Widget(actions, iElemCount(actions))));
3322 addChild_Widget(get_Root()->widget, iClob(dlg));
3323 as_Widget(palSeed)->rect.size.x = 60 * gap_UI;
3324 arrange_Widget(dlg);
3325 }
3326 /* Initialize. */ {
3327 const iString *site = collectNewRange_String(urlRoot_String(url));
3328 setToggle_Widget(findChild_Widget(dlg, "sitespec.ansi"),
3329 ~value_SiteSpec(site, dismissWarnings_SiteSpecKey) & ansiEscapes_GmDocumentWarning);
3330 setText_InputWidget(findChild_Widget(dlg, "sitespec.palette"),
3331 valueString_SiteSpec(site, paletteSeed_SiteSpecKey));
3332 /* Keep a copy of the original palette seed for restoring on cancel. */
3333 setUserData_Object(dlg, copy_String(valueString_SiteSpec(site, paletteSeed_SiteSpecKey)));
3334 if (!isUsingPanelLayout_Mobile()) {
3335 setValidator_InputWidget(findChild_Widget(dlg, "sitespec.palette"),
3336 updateSiteSpecificTheme_, dlg);
3337 }
3338 }
3339 setCommandHandler_Widget(dlg, siteSpecificSettingsHandler_);
3340 setupSheetTransition_Mobile(dlg, incoming_TransitionFlag);
3341 setFocus_Widget(findChild_Widget(dlg, "sitespec.palette"));
3342 return dlg;
3343}
3344
3345/*----------------------------------------------------------------------------------------------*/
3346
3231iWidget *makeIdentityCreation_Widget(void) { 3347iWidget *makeIdentityCreation_Widget(void) {
3232 const iMenuItem actions[] = { { "${dlg.newident.more}", 0, 0, "ident.showmore" }, 3348 const iMenuItem actions[] = { { "${dlg.newident.more}", 0, 0, "ident.showmore" },
3233 { "---" }, 3349 { "---" },
@@ -3451,6 +3567,54 @@ iWidget *makeTranslation_Widget(iWidget *parent) {
3451 return dlg; 3567 return dlg;
3452} 3568}
3453 3569
3570iWidget *makeGlyphFinder_Widget(void) {
3571 iString msg;
3572 iString command;
3573 init_String(&msg);
3574 initCStr_String(&command, "!font.find chars:");
3575 for (size_t i = 0; ; i++) {
3576 iChar ch = missing_Text(i);
3577 if (!ch) break;
3578 appendFormat_String(&msg, " U+%04X", ch);
3579 appendChar_String(&command, ch);
3580 }
3581 iArray items;
3582 init_Array(&items, sizeof(iMenuItem));
3583 if (!isEmpty_String(&msg)) {
3584 prependCStr_String(&msg, "${dlg.glyphfinder.missing} ");
3585 appendCStr_String(&msg, "\n\n${dlg.glyphfinder.help}");
3586 pushBackN_Array(
3587 &items,
3588 (iMenuItem[]){
3589 { "${menu.fonts}", 0, 0, "!open newtab:1 url:about:fonts" },
3590 { "${dlg.glyphfinder.disable}", 0, 0, "prefs.font.warnmissing.changed arg:0" },
3591 { "---" },
3592 { uiTextCaution_ColorEscape magnifyingGlass_Icon " ${dlg.glyphfinder.search}",
3593 0,
3594 0,
3595 cstr_String(&command) },
3596 { "${close}", 0, 0, "cancel" } },
3597 5);
3598 }
3599 else {
3600 setCStr_String(&msg, "${dlg.glyphfinder.help.empty}");
3601 pushBackN_Array(&items,
3602 (iMenuItem[]){ { "${menu.reload}", 0, 0, "navigate.reload" },
3603 { "${close}", 0, 0, "cancel" } },
3604 2);
3605 }
3606 iWidget *dlg = makeQuestion_Widget("${heading.glyphfinder}", cstr_String(&msg),
3607 constData_Array(&items),
3608 size_Array(&items));
3609 arrange_Widget(dlg);
3610 deinit_Array(&items);
3611 deinit_String(&command);
3612 deinit_String(&msg);
3613 return dlg;
3614}
3615
3616/*----------------------------------------------------------------------------------------------*/
3617
3454void init_PerfTimer(iPerfTimer *d) { 3618void init_PerfTimer(iPerfTimer *d) {
3455 d->ticks = SDL_GetPerformanceCounter(); 3619 d->ticks = SDL_GetPerformanceCounter();
3456} 3620}
diff --git a/src/ui/util.h b/src/ui/util.h
index 98ce784c..31c8cedc 100644
--- a/src/ui/util.h
+++ b/src/ui/util.h
@@ -336,12 +336,14 @@ iWidget * makeQuestion_Widget (const char *title, const char *msg,
336iWidget * makePreferences_Widget (void); 336iWidget * makePreferences_Widget (void);
337void updatePreferencesLayout_Widget (iWidget *prefs); 337void updatePreferencesLayout_Widget (iWidget *prefs);
338 338
339iWidget * makeBookmarkEditor_Widget (void); 339iWidget * makeBookmarkEditor_Widget (void);
340void setBookmarkEditorFolder_Widget(iWidget *editor, uint32_t folderId); 340void setBookmarkEditorFolder_Widget (iWidget *editor, uint32_t folderId);
341iWidget * makeBookmarkCreation_Widget (const iString *url, const iString *title, iChar icon); 341iWidget * makeBookmarkCreation_Widget (const iString *url, const iString *title, iChar icon);
342iWidget * makeIdentityCreation_Widget (void); 342iWidget * makeIdentityCreation_Widget (void);
343iWidget * makeFeedSettings_Widget (uint32_t bookmarkId); 343iWidget * makeFeedSettings_Widget (uint32_t bookmarkId);
344iWidget * makeTranslation_Widget (iWidget *parent); 344iWidget * makeSiteSpecificSettings_Widget (const iString *url);
345iWidget * makeTranslation_Widget (iWidget *parent);
346iWidget * makeGlyphFinder_Widget (void);
345 347
346const char * languageId_String (const iString *menuItemLabel); 348const char * languageId_String (const iString *menuItemLabel);
347int languageIndex_CStr (const char *langId); 349int languageIndex_CStr (const char *langId);
diff --git a/src/ui/widget.c b/src/ui/widget.c
index fc754b7a..2e878878 100644
--- a/src/ui/widget.c
+++ b/src/ui/widget.c
@@ -168,7 +168,8 @@ void deinit_Widget(iWidget *d) {
168 if (d->flags & visualOffset_WidgetFlag) { 168 if (d->flags & visualOffset_WidgetFlag) {
169 removeTicker_App(visualOffsetAnimation_Widget_, d); 169 removeTicker_App(visualOffsetAnimation_Widget_, d);
170 } 170 }
171 iWindow *win = get_Window(); 171 iWindow *win = d->root->window;
172 iAssert(win);
172 if (win->lastHover == d) { 173 if (win->lastHover == d) {
173 win->lastHover = NULL; 174 win->lastHover = NULL;
174 } 175 }
diff --git a/src/ui/window.c b/src/ui/window.c
index 47abf878..b0de0557 100644
--- a/src/ui/window.c
+++ b/src/ui/window.c
@@ -80,6 +80,7 @@ iDefineTypeConstructionArgs(MainWindow, (iRect rect), rect)
80#if defined (iHaveNativeMenus) 80#if defined (iHaveNativeMenus)
81/* Using native menus. */ 81/* Using native menus. */
82static const iMenuItem fileMenuItems_[] = { 82static const iMenuItem fileMenuItems_[] = {
83 { "${menu.newwindow}", SDLK_n, KMOD_PRIMARY, "window.new" },
83 { "${menu.newtab}", SDLK_t, KMOD_PRIMARY, "tabs.new" }, 84 { "${menu.newtab}", SDLK_t, KMOD_PRIMARY, "tabs.new" },
84 { "${menu.openlocation}", SDLK_l, KMOD_PRIMARY, "navigate.focus" }, 85 { "${menu.openlocation}", SDLK_l, KMOD_PRIMARY, "navigate.focus" },
85 { "---", 0, 0, NULL }, 86 { "---", 0, 0, NULL },
@@ -210,7 +211,9 @@ static void windowSizeChanged_MainWindow_(iMainWindow *d) {
210 211
211static void setupUserInterface_MainWindow(iMainWindow *d) { 212static void setupUserInterface_MainWindow(iMainWindow *d) {
212#if defined (iHaveNativeMenus) 213#if defined (iHaveNativeMenus)
213 insertMacMenus_(); 214 if (numWindows_App() == 0) {
215 insertMacMenus_(); /* TODO: Shouldn't this be in the App? */
216 }
214#endif 217#endif
215 /* One root is created by default. */ 218 /* One root is created by default. */
216 d->base.roots[0] = new_Root(); 219 d->base.roots[0] = new_Root();
@@ -246,6 +249,7 @@ static void updateSize_MainWindow_(iMainWindow *d, iBool notifyAlways) {
246 249
247void drawWhileResizing_MainWindow(iMainWindow *d, int w, int h) { 250void drawWhileResizing_MainWindow(iMainWindow *d, int w, int h) {
248 if (!isDrawing_) { 251 if (!isDrawing_) {
252 setCurrent_Window(d);
249 draw_MainWindow(d); 253 draw_MainWindow(d);
250 } 254 }
251} 255}
@@ -647,6 +651,7 @@ void init_MainWindow(iMainWindow *d, iRect rect) {
647} 651}
648 652
649void deinit_MainWindow(iMainWindow *d) { 653void deinit_MainWindow(iMainWindow *d) {
654 removeWindow_App(d);
650 if (d->backBuf) { 655 if (d->backBuf) {
651 SDL_DestroyTexture(d->backBuf); 656 SDL_DestroyTexture(d->backBuf);
652 } 657 }
@@ -677,6 +682,7 @@ iBool isFullscreen_MainWindow(const iMainWindow *d) {
677} 682}
678 683
679iRoot *findRoot_Window(const iWindow *d, const iWidget *widget) { 684iRoot *findRoot_Window(const iWindow *d, const iWidget *widget) {
685
680 while (widget->parent) { 686 while (widget->parent) {
681 widget = widget->parent; 687 widget = widget->parent;
682 } 688 }
@@ -830,6 +836,9 @@ static void savePlace_MainWindow_(iAny *mainWindow) {
830} 836}
831 837
832static iBool handleWindowEvent_MainWindow_(iMainWindow *d, const SDL_WindowEvent *ev) { 838static iBool handleWindowEvent_MainWindow_(iMainWindow *d, const SDL_WindowEvent *ev) {
839 if (ev->windowID != SDL_GetWindowID(d->base.win)) {
840 return iFalse;
841 }
833 switch (ev->event) { 842 switch (ev->event) {
834#if defined(iPlatformDesktop) 843#if defined(iPlatformDesktop)
835 case SDL_WINDOWEVENT_EXPOSED: 844 case SDL_WINDOWEVENT_EXPOSED:
@@ -857,7 +866,7 @@ static iBool handleWindowEvent_MainWindow_(iMainWindow *d, const SDL_WindowEvent
857 if (d->base.isMinimized) { 866 if (d->base.isMinimized) {
858 return iFalse; 867 return iFalse;
859 } 868 }
860 closePopups_App(); 869 closePopups_App(iFalse);
861 checkPixelRatioChange_Window_(as_Window(d)); 870 checkPixelRatioChange_Window_(as_Window(d));
862 const iInt2 newPos = init_I2(ev->data1, ev->data2); 871 const iInt2 newPos = init_I2(ev->data1, ev->data2);
863 if (isEqual_I2(newPos, init1_I2(-32000))) { /* magic! */ 872 if (isEqual_I2(newPos, init1_I2(-32000))) { /* magic! */
@@ -907,7 +916,7 @@ static iBool handleWindowEvent_MainWindow_(iMainWindow *d, const SDL_WindowEvent
907 // updateSize_Window_(d, iTrue); 916 // updateSize_Window_(d, iTrue);
908 return iTrue; 917 return iTrue;
909 } 918 }
910 closePopups_App(); 919 closePopups_App(iFalse);
911 if (unsnap_MainWindow_(d, NULL)) { 920 if (unsnap_MainWindow_(d, NULL)) {
912 return iTrue; 921 return iTrue;
913 } 922 }
@@ -929,7 +938,7 @@ static iBool handleWindowEvent_MainWindow_(iMainWindow *d, const SDL_WindowEvent
929 return iTrue; 938 return iTrue;
930 case SDL_WINDOWEVENT_MINIMIZED: 939 case SDL_WINDOWEVENT_MINIMIZED:
931 d->base.isMinimized = iTrue; 940 d->base.isMinimized = iTrue;
932 closePopups_App(); 941 closePopups_App(iTrue);
933 return iTrue; 942 return iTrue;
934#else /* if defined (!iPlatformDesktop) */ 943#else /* if defined (!iPlatformDesktop) */
935 case SDL_WINDOWEVENT_RESIZED: 944 case SDL_WINDOWEVENT_RESIZED:
@@ -953,6 +962,7 @@ static iBool handleWindowEvent_MainWindow_(iMainWindow *d, const SDL_WindowEvent
953 setCapsLockDown_Keys(iFalse); 962 setCapsLockDown_Keys(iFalse);
954 postCommand_App("window.focus.gained"); 963 postCommand_App("window.focus.gained");
955 d->base.isExposed = iTrue; 964 d->base.isExposed = iTrue;
965 setActiveWindow_App(d);
956#if !defined (iPlatformDesktop) 966#if !defined (iPlatformDesktop)
957 /* Returned to foreground, may have lost buffered content. */ 967 /* Returned to foreground, may have lost buffered content. */
958 invalidate_MainWindow_(d, iTrue); 968 invalidate_MainWindow_(d, iTrue);
@@ -964,12 +974,17 @@ static iBool handleWindowEvent_MainWindow_(iMainWindow *d, const SDL_WindowEvent
964#if !defined (iPlatformDesktop) 974#if !defined (iPlatformDesktop)
965 setFreezeDraw_MainWindow(d, iTrue); 975 setFreezeDraw_MainWindow(d, iTrue);
966#endif 976#endif
967 closePopups_App(); 977 closePopups_App(iTrue);
968 return iFalse; 978 return iFalse;
969 case SDL_WINDOWEVENT_TAKE_FOCUS: 979 case SDL_WINDOWEVENT_TAKE_FOCUS:
970 SDL_SetWindowInputFocus(d->base.win); 980 SDL_SetWindowInputFocus(d->base.win);
971 postRefresh_App(); 981 postRefresh_App();
972 return iTrue; 982 return iTrue;
983 case SDL_WINDOWEVENT_CLOSE:
984 if (numWindows_App() > 1) {
985 closeWindow_App(d);
986 }
987 return iTrue;
973 default: 988 default:
974 break; 989 break;
975 } 990 }
@@ -1009,7 +1024,7 @@ iBool processEvent_Window(iWindow *d, const SDL_Event *ev) {
1009 } 1024 }
1010 } 1025 }
1011 case SDL_RENDER_TARGETS_RESET: 1026 case SDL_RENDER_TARGETS_RESET:
1012 case SDL_RENDER_DEVICE_RESET: { 1027 case SDL_RENDER_DEVICE_RESET: {
1013 if (mw) { 1028 if (mw) {
1014 invalidate_MainWindow_(mw, iTrue /* force full reset */); 1029 invalidate_MainWindow_(mw, iTrue /* force full reset */);
1015 } 1030 }
@@ -1095,7 +1110,7 @@ iBool processEvent_Window(iWindow *d, const SDL_Event *ev) {
1095 event.type == SDL_MOUSEBUTTONUP || event.type == SDL_MOUSEBUTTONDOWN) { 1110 event.type == SDL_MOUSEBUTTONUP || event.type == SDL_MOUSEBUTTONDOWN) {
1096 if (mouseGrab_Widget()) { 1111 if (mouseGrab_Widget()) {
1097 iWidget *grabbed = mouseGrab_Widget(); 1112 iWidget *grabbed = mouseGrab_Widget();
1098 setCurrent_Root(findRoot_Window(d, grabbed)); 1113 setCurrent_Root(grabbed->root /* findRoot_Window(d, grabbed)*/);
1099 wasUsed = dispatchEvent_Widget(grabbed, &event); 1114 wasUsed = dispatchEvent_Widget(grabbed, &event);
1100 } 1115 }
1101 } 1116 }
@@ -1164,7 +1179,33 @@ iLocalDef iBool isEscapeKeypress_(const SDL_Event *ev) {
1164 return (ev->type == SDL_KEYDOWN || ev->type == SDL_KEYUP) && ev->key.keysym.sym == SDLK_ESCAPE; 1179 return (ev->type == SDL_KEYDOWN || ev->type == SDL_KEYUP) && ev->key.keysym.sym == SDLK_ESCAPE;
1165} 1180}
1166 1181
1182static uint32_t windowId_SDLEvent_(const SDL_Event *ev) {
1183 switch (ev->type) {
1184 case SDL_MOUSEBUTTONDOWN:
1185 case SDL_MOUSEBUTTONUP:
1186 return ev->button.windowID;
1187 case SDL_MOUSEMOTION:
1188 return ev->motion.windowID;
1189 case SDL_MOUSEWHEEL:
1190 return ev->wheel.windowID;
1191 case SDL_KEYDOWN:
1192 case SDL_KEYUP:
1193 return ev->key.windowID;
1194 case SDL_TEXTINPUT:
1195 return ev->text.windowID;
1196 case SDL_USEREVENT:
1197 return ev->user.windowID;
1198 default:
1199 return 0;
1200 }
1201}
1202
1167iBool dispatchEvent_Window(iWindow *d, const SDL_Event *ev) { 1203iBool dispatchEvent_Window(iWindow *d, const SDL_Event *ev) {
1204 /* For the right window? */
1205 const uint32_t evWin = windowId_SDLEvent_(ev);
1206 if (evWin && evWin != id_Window(d)) {
1207 return iFalse; /* Meant for a different window. */
1208 }
1168 if (ev->type == SDL_MOUSEMOTION) { 1209 if (ev->type == SDL_MOUSEMOTION) {
1169 /* Hover widget may change. */ 1210 /* Hover widget may change. */
1170 setHover_Widget(NULL); 1211 setHover_Widget(NULL);
@@ -1526,6 +1567,23 @@ void setKeyboardHeight_MainWindow(iMainWindow *d, int height) {
1526 } 1567 }
1527} 1568}
1528 1569
1570iObjectList *listDocuments_MainWindow(iMainWindow *d, const iRoot *rootOrNull) {
1571 iObjectList *docs = new_ObjectList();
1572 iForIndices(i, d->base.roots) {
1573 iRoot *root = d->base.roots[i];
1574 if (!root) continue;
1575 if (!rootOrNull || root == rootOrNull) {
1576 const iWidget *tabs = findChild_Widget(root->widget, "doctabs");
1577 iForEach(ObjectList, i, children_Widget(findChild_Widget(tabs, "tabs.pages"))) {
1578 if (isInstance_Object(i.object, &Class_DocumentWidget)) {
1579 pushBack_ObjectList(docs, i.object);
1580 }
1581 }
1582 }
1583 }
1584 return docs;
1585}
1586
1529void checkPendingSplit_MainWindow(iMainWindow *d) { 1587void checkPendingSplit_MainWindow(iMainWindow *d) {
1530 if (d->splitMode != d->pendingSplitMode) { 1588 if (d->splitMode != d->pendingSplitMode) {
1531 setSplitMode_MainWindow(d, d->pendingSplitMode); 1589 setSplitMode_MainWindow(d, d->pendingSplitMode);
@@ -1549,6 +1607,7 @@ void setSplitMode_MainWindow(iMainWindow *d, int splitFlags) {
1549 } 1607 }
1550 iWindow *w = as_Window(d); 1608 iWindow *w = as_Window(d);
1551 iAssert(current_Root() == NULL); 1609 iAssert(current_Root() == NULL);
1610 setCurrent_Window(w);
1552 if (d->splitMode != splitMode) { 1611 if (d->splitMode != splitMode) {
1553 int oldCount = numRoots_Window(w); 1612 int oldCount = numRoots_Window(w);
1554 setFreezeDraw_MainWindow(d, iTrue); 1613 setFreezeDraw_MainWindow(d, iTrue);
@@ -1577,8 +1636,8 @@ void setSplitMode_MainWindow(iMainWindow *d, int splitFlags) {
1577 /* The last child is the [+] button for adding a tab. */ 1636 /* The last child is the [+] button for adding a tab. */
1578 moveTabButtonToEnd_Widget(findChild_Widget(docTabs, "newtab")); 1637 moveTabButtonToEnd_Widget(findChild_Widget(docTabs, "newtab"));
1579 setFlags_Widget(findWidget_Root("navbar.unsplit"), hidden_WidgetFlag, iTrue); 1638 setFlags_Widget(findWidget_Root("navbar.unsplit"), hidden_WidgetFlag, iTrue);
1580 iRelease(tabs);
1581 postCommandf_App("tabs.switch id:%s", cstr_String(id_Widget(constAs_Widget(curPage)))); 1639 postCommandf_App("tabs.switch id:%s", cstr_String(id_Widget(constAs_Widget(curPage))));
1640 iRelease(tabs);
1582 } 1641 }
1583 else if (oldCount == 1 && splitMode) { 1642 else if (oldCount == 1 && splitMode) {
1584 /* Add a second root. */ 1643 /* Add a second root. */
diff --git a/src/ui/window.h b/src/ui/window.h
index 5abf23eb..c3c34e1b 100644
--- a/src/ui/window.h
+++ b/src/ui/window.h
@@ -139,7 +139,7 @@ iAnyObject * hitChild_Window (const iWindow *, iInt2 coord);
139uint32_t frameTime_Window (const iWindow *); 139uint32_t frameTime_Window (const iWindow *);
140SDL_Renderer * renderer_Window (const iWindow *); 140SDL_Renderer * renderer_Window (const iWindow *);
141int numRoots_Window (const iWindow *); 141int numRoots_Window (const iWindow *);
142iRoot * findRoot_Window (const iWindow *, const iWidget *widget); 142//iRoot * findRoot_Window (const iWindow *, const iWidget *widget);
143iRoot * otherRoot_Window (const iWindow *, iRoot *root); 143iRoot * otherRoot_Window (const iWindow *, iRoot *root);
144 144
145iBool processEvent_Window (iWindow *, const SDL_Event *); 145iBool processEvent_Window (iWindow *, const SDL_Event *);
@@ -187,6 +187,7 @@ void setTitle_MainWindow (iMainWindow *, const iString *title
187void setSnap_MainWindow (iMainWindow *, int snapMode); 187void setSnap_MainWindow (iMainWindow *, int snapMode);
188void setFreezeDraw_MainWindow (iMainWindow *, iBool freezeDraw); 188void setFreezeDraw_MainWindow (iMainWindow *, iBool freezeDraw);
189void setKeyboardHeight_MainWindow (iMainWindow *, int height); 189void setKeyboardHeight_MainWindow (iMainWindow *, int height);
190iObjectList *listDocuments_MainWindow (iMainWindow *, const iRoot *rootOrNull);
190void setSplitMode_MainWindow (iMainWindow *, int splitMode); 191void setSplitMode_MainWindow (iMainWindow *, int splitMode);
191void checkPendingSplit_MainWindow (iMainWindow *); 192void checkPendingSplit_MainWindow (iMainWindow *);
192void swapRoots_MainWindow (iMainWindow *); 193void swapRoots_MainWindow (iMainWindow *);