summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--res/about/help.gmi113
-rw-r--r--res/about/version.gmi32
-rw-r--r--src/app.c7
-rw-r--r--src/bookmarks.h20
-rw-r--r--src/gempub.c88
-rw-r--r--src/gempub.h4
-rw-r--r--src/gmdocument.c2
-rw-r--r--src/gmutil.c7
-rw-r--r--src/gmutil.h3
-rw-r--r--src/ui/documentwidget.c47
-rw-r--r--src/ui/sidebarwidget.c30
-rw-r--r--src/ui/util.c15
-rw-r--r--src/ui/widget.c6
-rw-r--r--src/ui/widget.h1
-rw-r--r--src/ui/window.c24
15 files changed, 328 insertions, 71 deletions
diff --git a/res/about/help.gmi b/res/about/help.gmi
index c710afc9..604f0fed 100644
--- a/res/about/help.gmi
+++ b/res/about/help.gmi
@@ -27,25 +27,29 @@ Like Gemini, Lagrange has been designed with minimalism in mind. It depends on a
27 27
28* Beautiful typography using Unicode fonts 28* Beautiful typography using Unicode fonts
29* Autogenerated page style and Unicode icon for each Gemini domain 29* Autogenerated page style and Unicode icon for each Gemini domain
30* Smart suggestions when typing the URL — search bookmarks, history, identities
31* Sidebar for page outline, managing bookmarks and identities, and viewing history
32* Multiple tabs
33* Identity management — create and use TLS client certificates
34* Subscribe to Gemini and Atom feeds
35* Use Gemini pages as a source of bookmarks
36* Light and dark color themes 30* Light and dark color themes
37* Select and copy text with the mouse
38* Find text on the page
39* Search engine integration
40* Open image links inline on the same page
41* Audio playback: MP3, Ogg Vorbis, WAV
42* Open links via keyboard shortcuts
43* Instant back/forward navigation
44* Smooth scrolling 31* Smooth scrolling
45* Scaling page content (50%...200%) 32* Scaling page content (50%...200%)
46* Scaling factor for the UI (for arbitrary monitor DPI) 33* Scaling factor for the UI (for arbitrary monitor DPI)
34
35* Sidebars for managing bookmarks, identities and subscribed feeds, and viewing browsing history and the page outline
36* Multiple tabs
37* Split view for browsing two pages at once
47* Persistent app state — tabs and history are restored on next run 38* Persistent app state — tabs and history are restored on next run
48* Configurable keybindings 39* Configurable keybindings
40* Open links via keyboard shortcuts
41* Select and copy text with the mouse
42* Find text on the page
43* Open image and audio links inline on the same page
44
45* Instant back/forward navigation
46* Smart suggestions when typing an URL — search bookmarks, history, identities
47* Search engine integration
48* Identity management — create and use TLS client certificates
49* Subscribe to Gemini and Atom feeds
50* Use Gemini pages as a source of bookmarks
51* Audio playback: MP3, Ogg Vorbis, WAV
52* Read Gempub books and view ZIP archive contents
49* Built-in support for Gopher 53* Built-in support for Gopher
50* Use proxy servers for HTTP, Gopher, or Gemini content 54* Use proxy servers for HTTP, Gopher, or Gemini content
51 55
@@ -65,7 +69,7 @@ Lagrange's user interface is modeled after web browsers:
65 69
66* There is a navigation bar at the top with Back and Forward buttons. 70* There is a navigation bar at the top with Back and Forward buttons.
67* There is a tab bar for switching tabs. The tab bar is hidden if there is only one tab open. 71* There is a tab bar for switching tabs. The tab bar is hidden if there is only one tab open.
68* There is a sidebar for managing bookmarks and TLS identities, and viewing history and the page outline. Sidebars are hidden by default. 72* There is a sidebar for managing bookmarks, subscribed feeds, and TLS identities, and viewing history and the page outline. Sidebars are hidden by default.
69* There is a search bar that appears at the bottom when searching text on the page. 73* There is a search bar that appears at the bottom when searching text on the page.
70 74
71Tip: Try pressing ${CTRL+}5 now to see the page outline. 75Tip: Try pressing ${CTRL+}5 now to see the page outline.
@@ -104,6 +108,8 @@ Link colors remain the same regardless of which color theme is being used for pa
104 108
105When 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. 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.
106 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.
112
107### 1.1.3 Page caching 113### 1.1.3 Page caching
108 114
109When 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. 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.
@@ -116,9 +122,15 @@ Maximum size of the cache can be configured on the "Network" tab of Preferences.
116 122
117Holding down ${CTRL} when left-clicking on a link causes it to open in a new tab. Alternatively, middle-clicking a link has the same effect. 123Holding down ${CTRL} when left-clicking on a link causes it to open in a new tab. Alternatively, middle-clicking a link has the same effect.
118 124
125${SHIFT+}${CTRL+}click opens the link in a new background tab.
126
119Right-clicking on a link shows a link-specific context menu. From there, you can also open the link in a new background tab, which will keep the current tab open. 127Right-clicking on a link shows a link-specific context menu. From there, you can also open the link in a new background tab, which will keep the current tab open.
120 128
121### 1.1.5 Opening links using the keyboard 129### 1.1.5 Opening links to the side
130
131Holding down ${SHIFT} when left-clicking on a link opens it in split view mode on the other side of the split. When in split view mode, your tab pinning preference will determine the tab on which clicked links will open. See section 1.8 for more information about split view modes.
132
133### 1.1.6 Opening links using the keyboard
122 134
123Lagrange has two modes for navigating links with the keyboard. The first uses a modifier key (${ALT} by default): while the modifier is pressed, alphanumeric shortcuts are shown for each visible link in the window. You can then press the corresponding key, still holding the modifier, to open a link. The second mode is based on letters only and focuses on the page row keys. You press and release the activation key ("F" by default) to show the link shortcut letters. In this mode the shortcuts are not in alphabetic order, but instead roughly sorted based on proximity to the F and J keys. You may find this easier to use since it is a simple sequence of key presses without using modifiers, allowing one to keep fingers on or near the home row. 135Lagrange has two modes for navigating links with the keyboard. The first uses a modifier key (${ALT} by default): while the modifier is pressed, alphanumeric shortcuts are shown for each visible link in the window. You can then press the corresponding key, still holding the modifier, to open a link. The second mode is based on letters only and focuses on the page row keys. You press and release the activation key ("F" by default) to show the link shortcut letters. In this mode the shortcuts are not in alphabetic order, but instead roughly sorted based on proximity to the F and J keys. You may find this easier to use since it is a simple sequence of key presses without using modifiers, allowing one to keep fingers on or near the home row.
124 136
@@ -132,7 +144,11 @@ Press ${CTRL+}T to open a new tab, and ${CTRL+}W to close the current tab. Right
132 144
133The set of open tabs and their full contents are saved when you quit the application and restored when relaunch it. 145The set of open tabs and their full contents are saved when you quit the application and restored when relaunch it.
134 146
135### 1.2.1 Auto-reloading 147### 1.2.1 Highlighting of open links
148
149When you open a tab, and the URL of that tab is found in a link on another page, that link will be highlighted. This way you can conveniently see which links are currently open in other tabs. These highlights will assist you in navigating hierarchies and index pages.
150
151### 1.2.2 Auto-reloading
136 152
137A tab can be set to auto-reload at given intervals. The setting is remembered until the tab is closed. This is helpful if you keep a periodically updated page open for longer periods of time. 153A tab can be set to auto-reload at given intervals. The setting is remembered until the tab is closed. This is helpful if you keep a periodically updated page open for longer periods of time.
138 154
@@ -191,6 +207,7 @@ Note that remote bookmarks are read-only: they cannot be edited or tagged. This
191* The "remotesource" tag marks the bookmark as a source of remote bookmarks. All links on the source pages are shown as remote bookmarks in the Bookmarks list. 207* The "remotesource" tag marks the bookmark as a source of remote bookmarks. All links on the source pages are shown as remote bookmarks in the Bookmarks list.
192* The "remote" tag is used for remote bookmarks. These bookmarks cannot be edited or tagged, but you can make a local duplicate to turn it into a regular bookmark. 208* The "remote" tag is used for remote bookmarks. These bookmarks cannot be edited or tagged, but you can make a local duplicate to turn it into a regular bookmark.
193* The "usericon" tag prevents a random icon to be selected for the bookmark. This tag is automatically applied when an icon character is entered in the bookmark editor. 209* The "usericon" tag prevents a random icon to be selected for the bookmark. This tag is automatically applied when an icon character is entered in the bookmark editor.
210* The "linksplit" tag causes all links on the bookmarked page to open to the side, automatically enabling split view mode.
194 211
195## 1.5 Subscribing to feeds 212## 1.5 Subscribing to feeds
196 213
@@ -253,6 +270,54 @@ The 🔃 button on the right side of the URL input field is the Reload/Stop butt
253 270
254The location where downloaded files are saved can be changed in Preferences. The default location is "Downloads" in your home directory. 271The location where downloaded files are saved can be changed in Preferences. The default location is "Downloads" in your home directory.
255 272
273## 1.8 Split view mode
274
275By default, only one tab is visible at a time in the application window. However, sometimes it is beneficial to see two pages at once. For example, many capsules have top-level menus or lists of articles, and keeping the menu/index visible on the side makes navigation less cumbersome.
276
277Split view mode divides the UI into two equivalent parts. You can have multiple tabs open in each split. Closing all tabs on one side will remove the split and return back to the normal unsplit mode.
278
279View splitting is primarily controlled using the view split menu. The default keybinding for showing it is ${CTRL+}J. For convenience it is recommended to use the shortcuts listed in the menu to change split modes. For example, the sequence ${CTRL+}J 2 can be used to quickly activate a 50% horizontal split, and ${CTRL+}J 1 will merge all open tabs back into the normal unsplit view.
280
281Another way to activate split view mode is to click on a link while holding ${SHIFT}.
282
283Each split has its own sidebars, which means that in split view mode you can have a total of four sidebars open at the same time.
284
285### 1.8.1 Switching focus
286
287At any given time, one of the splits has keyboard focus. This is indicated by a colored line at the top of the section, and some UI elements will be dimmed out on the unfocused side.
288
289To switch keyboard focus between the sections, you can use the Next/Previous Tab keybindings or Ctrl+Tab. Next/Previous Tab is particularly convenient as it cycles through all open tabs, jumping to the other side of the split when appropriate.
290
291You may also press Tab to cycle input focus between all the URL input fields.
292
293### 1.8.2 Pinning
294
295While it is sometimes useful to simply have two independent browsers open side by side, by default view splitting is meant to assist in navigating hierarchies and lists. In the typical use case, you'll have a menu or an index page on the left, and a content page open on the right. Links clicked on the left will automatically open on the right.
296
297This is called "pinning" and the behavior can be configured in Preferences. The "Split view pinning" setting on the "General" tab of Preferences controls where links get opened in a split view. There are three modes available:
298
299* "None" causes links to open in the tab where the link is clicked. In this mode, both sides of the split can be navigated independently.
300* "Left Tab" causes links clicked on the left tab to open on the right side. The page open in the left tab is therefore "pinned" and does not change unless you enter a new URL or navigate to the parent or root.
301* "Right Tab" is the same but works the other way around. The page open in the right tab is pinned and clicked links open on the left.
302
303The default pinning mode is "Left Tab".
304
305## 1.9 Viewing local files and directories
306
307"file://" URLs can be used for accessing local files and directories. File types known to Lagrange, such as .gmi, .txt., .png, .jpg, and .zip, can be viewed inside the application. In this release, types of local files are detected solely based on the file extension.
308
309When viewing a directory, its contents are shown as a list of links. A similar page is shown when viewing a directory inside a ZIP archive.
310
311Note that ZIP archives are not decompressed while browsing their directory structure. Each request that accesses a compressed file in an archive will cause only that particular file to be decompressed. This may cause slow response times when dealing with large compressed files.
312
313## 1.10 Gempub
314
315Gempub is an e-book/archival format that is essentially a set of Gemtext files stored in a ZIP archive.
316
317=> https://codeberg.org/oppenlab/gempub Gempub specification
318
319Lagrange can be used as a Gempub reader. When a .gpub file has been saved locally, you can open it in the application to see the book cover page. On the cover page you will find a link that opens the contents of the book. When clicked, split view mode is automatically enabled and both the book index page and the first chapter are shown (unless split view pinning has been disabled).
320
256# 2 Customization 321# 2 Customization
257 322
258You can find a number of settings in Preferences to customize the user interface and page contents. 323You can find a number of settings in Preferences to customize the user interface and page contents.
@@ -261,6 +326,10 @@ You can find a number of settings in Preferences to customize the user interface
261 326
262One 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. 327One 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.
263 328
329The "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.
330
331"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.8.
332
264## 2.2 Window and UI options 333## 2.2 Window and UI options
265 334
266Lagrange supports configuring dark and light UI modes separately. 335Lagrange supports configuring dark and light UI modes separately.
@@ -300,9 +369,9 @@ There are six fonts available:
300 369
301* "Nunito" is a friendly rounded sans-serif font. 370* "Nunito" is a friendly rounded sans-serif font.
302* "Fira Sans" is a bolder, narrower, and more angular sans-serif font. 371* "Fira Sans" is a bolder, narrower, and more angular sans-serif font.
372* "Source Sans 3" is the Lagrange UI font (sans-serif).
303* "Literata" is a heavy and serious serif font. 373* "Literata" is a heavy and serious serif font.
304* "Tinos" is a lighter serif font. 374* "Tinos" is a lighter serif font.
305* "Source Sans Pro" is the Lagrange UI font.
306* "Iosevka" is the monospace font. 375* "Iosevka" is the monospace font.
307 376
308Other style options: 377Other style options:
@@ -357,7 +426,13 @@ Likewise (on macOS), .gmi/.gemini file extensions are registered as file formats
357 426
358You can drag and drop .gmi files on the Lagrange window to open them in the current tab. Dropping multiple files opens them in separate tabs. This is the recommended way to view local files, because there is no "Open File" menu item. You may also type "file://" URLs in the URL field. 427You can drag and drop .gmi files on the Lagrange window to open them in the current tab. Dropping multiple files opens them in separate tabs. This is the recommended way to view local files, because there is no "Open File" menu item. You may also type "file://" URLs in the URL field.
359 428
360## 3.4 Runtime files 429## 3.4 Controlling a running instance
430
431Only one instance of the application is allowed to run at once. This is because each instance uses the same runtime files (section 3.5) without any synchronization. A second instance of the application would overwrite any changes made by the first one, leading to lost bookmarks and other state.
432
433If there already is a running instance you can still use the command line to interact with it: you can open and close tabs, change the currently open URL, or list all the open URLs. See --help for a list of the available options.
434
435## 3.5 Runtime files
361 436
362Lagrange stores user-specific persistent files in one of the following locations (depending on the operating system): 437Lagrange stores user-specific persistent files in one of the following locations (depending on the operating system):
363 438
@@ -449,7 +524,7 @@ The combined navigation history of all tabs. Each line specifies one URL:
449``` 524```
450The Transient flag is used to indicate redirections and for marking feed entries as read without actually visiting the URL. URLs with the Transient flag do not appear in the History sidebar tab. 525The Transient flag is used to indicate redirections and for marking feed entries as read without actually visiting the URL. URLs with the Transient flag do not appear in the History sidebar tab.
451 526
452## 3.5 Environment variables 527## 3.6 Environment variables
453 528
454### LAGRANGE_OVERRIDE_DPI 529### LAGRANGE_OVERRIDE_DPI
455Override display DPI detection with a user-provided value. The same value is applied to all displays. This is useful if your display's DPI value is not being detected correctly. Example: 530Override display DPI detection with a user-provided value. The same value is applied to all displays. This is useful if your display's DPI value is not being detected correctly. Example:
diff --git a/res/about/version.gmi b/res/about/version.gmi
index dcc68fa8..440e516d 100644
--- a/res/about/version.gmi
+++ b/res/about/version.gmi
@@ -7,23 +7,33 @@
7# Release notes 7# Release notes
8 8
9## 1.4 9## 1.4
10* Split modes: horizontal/vertical, two tabs at once, 1:1/2:1/1:2 weights, merge tabs, swap sides. 10
11=> https://codeberg.org/oppenlab/gempub Basic Gempub support. 11Browsing:
12* View ZIP archive contents. 12* Added split view modes: two tabs at once, horizontal/vertical split, 1:1/2:1/1:2 weights, merge tabs, swap sides. See section 1.8 on the Help page for details.
13* View local file directories. 13* Split view pinning: keep a page pinned on one side while all opened links go to the other side.
14* Added "Show Downloads" to File menu. 14* "file://" URLs can be used for viewing contents of local directories and ZIP archives.
15* Adde "Open Downloaded File" to the file save dialog. 15* Basic Gempub support: generating a cover page, automatic split view for index and contents.
16* Navigation arrow buttons are disabled at end/beginning of history.
17* Bold link styling is used for unvisited links only.
18
19Page rendering and UI:
16* Optimized page rendering. Now each line of text is rendered into the view buffer only once, and whenever the view is stationary, content is prefilled in the available space outside the viewport. Previously, at least one line of text was rendered every frame whenever the viewport was moved, which was mostly redundant. 20* Optimized page rendering. Now each line of text is rendered into the view buffer only once, and whenever the view is stationary, content is prefilled in the available space outside the viewport. Previously, at least one line of text was rendered every frame whenever the viewport was moved, which was mostly redundant.
17* Preferences: Reorganized the fonts dropdown menu. 21* Added UI languages: Interlingua, Toki Pona.
22* Added toggles for special tags in the bookmark creation/editor dialog.
23* Added "Show Downloads" to the File/main menu.
24* Added "Open Downloaded File" to the file save dialog to make it easy to find the local copy of the file.
18* Updated the UI font to Source Sans 3. It now has all the styles and weights needed for page rendering, too. 25* Updated the UI font to Source Sans 3. It now has all the styles and weights needed for page rendering, too.
19* Added a semibold Fira Sans weight (used for links). 26* Added a semibold Fira Sans weight (used for links).
20* Added UI language: Interlingua. 27* Preferences: Reorganized the fonts dropdown menu.
21* Navigation arrow buttons are disabled at end/beginning of history. 28* All lists support smooth scrolling.
22* Adjustments to how display DPI affects UI scaling. 29* Adjustments to how display DPI affects UI scaling.
23* Don't percent encode equal signs in URL paths. 30
31Bug fixes:
24* Fixed allocation of page rendering buffers. Previously, some buffers may have gone unused or were allocated erroneously to the same position, causing unnecessary work for the page renderer. 32* Fixed allocation of page rendering buffers. Previously, some buffers may have gone unused or were allocated erroneously to the same position, causing unnecessary work for the page renderer.
25* Fixed a possible crash when closing a tab. 33* Fixed a possible crash when closing a tab.
26* Fixed various issues in the UI layout 34* Fixed various issues in the UI layout.
35* Fixed parsing URI scheme (limited set of characters allowed).
36* Don't percent encode equal signs in URL paths.
27 37
28## 1.3.2 38## 1.3.2
29* Fixed crash after updating from v1.2 due to undefined CA file/path configuration. 39* Fixed crash after updating from v1.2 due to undefined CA file/path configuration.
diff --git a/src/app.c b/src/app.c
index 39f0346e..4365f77d 100644
--- a/src/app.c
+++ b/src/app.c
@@ -124,7 +124,6 @@ struct Impl_App {
124 int sleepTimer; 124 int sleepTimer;
125#endif 125#endif
126 iAtomicInt pendingRefresh; 126 iAtomicInt pendingRefresh;
127 int tabEnum; /* IDs for new tabs */
128 iBool isLoadingPrefs; 127 iBool isLoadingPrefs;
129 iStringList *launchCommands; 128 iStringList *launchCommands;
130 iBool isFinishedLaunching; 129 iBool isFinishedLaunching;
@@ -722,7 +721,6 @@ static void init_App_(iApp *d, int argc, char **argv) {
722 d->certs = new_GmCerts(dataDir_App_()); 721 d->certs = new_GmCerts(dataDir_App_());
723 d->visited = new_Visited(); 722 d->visited = new_Visited();
724 d->bookmarks = new_Bookmarks(); 723 d->bookmarks = new_Bookmarks();
725 d->tabEnum = 0; /* generates unique IDs for tab pages */
726 init_Periodic(&d->periodic); 724 init_Periodic(&d->periodic);
727#if defined (iPlatformAppleDesktop) 725#if defined (iPlatformAppleDesktop)
728 setupApplication_MacOS(); 726 setupApplication_MacOS();
@@ -1602,7 +1600,6 @@ iDocumentWidget *newTab_App(const iDocumentWidget *duplicateOf, iBool switchToNe
1602 else { 1600 else {
1603 doc = new_DocumentWidget(); 1601 doc = new_DocumentWidget();
1604 } 1602 }
1605 setId_Widget(as_Widget(doc), format_CStr("document%03d", ++d->tabEnum));
1606 appendTabPage_Widget(tabs, as_Widget(doc), "", 0, 0); 1603 appendTabPage_Widget(tabs, as_Widget(doc), "", 0, 0);
1607 iRelease(doc); /* now owned by the tabs */ 1604 iRelease(doc); /* now owned by the tabs */
1608 addChild_Widget(findChild_Widget(tabs, "tabs.buttons"), iClob(newTabButton)); 1605 addChild_Widget(findChild_Widget(tabs, "tabs.buttons"), iClob(newTabButton));
@@ -2037,7 +2034,9 @@ iBool handleCommand_App(const char *cmd) {
2037 const int newTab = argLabel_Command(cmd, "newtab"); 2034 const int newTab = argLabel_Command(cmd, "newtab");
2038 if (newTab & otherRoot_OpenTabFlag && numRoots_Window(get_Window()) == 1) { 2035 if (newTab & otherRoot_OpenTabFlag && numRoots_Window(get_Window()) == 1) {
2039 /* Need to split first. */ 2036 /* Need to split first. */
2040 postCommandf_App("ui.split arg:3 newtab:%d url:%s", 2037 const iInt2 winSize = get_Window()->size;
2038 postCommandf_App("ui.split arg:3 axis:%d newtab:%d url:%s",
2039 (float) winSize.x / (float) winSize.y < 0.7f ? 1 : 0,
2041 newTab & ~otherRoot_OpenTabFlag, 2040 newTab & ~otherRoot_OpenTabFlag,
2042 cstr_String(url)); 2041 cstr_String(url));
2043 return iTrue; 2042 return iTrue;
diff --git a/src/bookmarks.h b/src/bookmarks.h
index 635682d1..353b4197 100644
--- a/src/bookmarks.h
+++ b/src/bookmarks.h
@@ -52,9 +52,23 @@ struct Impl_Bookmark {
52 52
53iLocalDef uint32_t id_Bookmark (const iBookmark *d) { return d->node.key; } 53iLocalDef uint32_t id_Bookmark (const iBookmark *d) { return d->node.key; }
54 54
55iBool hasTag_Bookmark (const iBookmark *d, const char *tag); 55iBool hasTag_Bookmark (const iBookmark *, const char *tag);
56void addTag_Bookmark (iBookmark *d, const char *tag); 56void addTag_Bookmark (iBookmark *, const char *tag);
57void removeTag_Bookmark (iBookmark *d, const char *tag); 57void removeTag_Bookmark (iBookmark *, const char *tag);
58
59iLocalDef void addTagIfMissing_Bookmark(iBookmark *d, const char *tag) {
60 if (!hasTag_Bookmark(d, tag)) {
61 addTag_Bookmark(d, tag);
62 }
63}
64iLocalDef void addOrRemoveTag_Bookmark(iBookmark *d, const char *tag, iBool add) {
65 if (add) {
66 addTagIfMissing_Bookmark(d, tag);
67 }
68 else {
69 removeTag_Bookmark(d, tag);
70 }
71}
58 72
59/*----------------------------------------------------------------------------------------------*/ 73/*----------------------------------------------------------------------------------------------*/
60 74
diff --git a/src/gempub.c b/src/gempub.c
index c9b1c242..0f0345dc 100644
--- a/src/gempub.c
+++ b/src/gempub.c
@@ -25,31 +25,96 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
25#include "lang.h" 25#include "lang.h"
26#include "defs.h" 26#include "defs.h"
27#include "gmdocument.h" 27#include "gmdocument.h"
28#include "gmrequest.h"
28#include "ui/util.h" 29#include "ui/util.h"
30#include "app.h"
29 31
30#include <the_Foundation/archive.h> 32#include <the_Foundation/archive.h>
31#include <the_Foundation/file.h> 33#include <the_Foundation/file.h>
32#include <the_Foundation/path.h> 34#include <the_Foundation/path.h>
35#include <the_Foundation/regexp.h>
33 36
34const char *mimeType_Gempub = "application/gpub+zip"; 37const char *mimeType_Gempub = "application/gpub+zip";
35 38
39/*----------------------------------------------------------------------------------------------*/
40
41iDeclareType(GempubNavLink)
42
43struct Impl_GempubNavLink {
44 iString url;
45 iString label;
46};
47
48static void init_GempubNavLink(iGempubNavLink *d) {
49 init_String(&d->url);
50 init_String(&d->label);
51}
52
53static void deinit_GempubNavLink(iGempubNavLink *d) {
54 deinit_String(&d->url);
55 deinit_String(&d->label);
56}
57
58iDefineTypeConstruction(GempubNavLink)
59
60/*----------------------------------------------------------------------------------------------*/
61
36struct Impl_Gempub { 62struct Impl_Gempub {
37 iArchive *arch; 63 iArchive *arch;
38 iString baseUrl; 64 iString baseUrl;
39 iString props[max_GempubProperty]; 65 iString props[max_GempubProperty];
66 iArray *navLinks; /* from index page */
40}; 67};
41 68
42iDefineTypeConstruction(Gempub) 69iDefineTypeConstruction(Gempub)
43 70
71static void parseNavigationLinks_Gempub_(const iGempub *d) {
72 if (!isEmpty_Array(d->navLinks)) {
73 return;
74 }
75 iGmRequest *index = iClob(new_GmRequest(certs_App()));
76 setUrl_GmRequest(index, indexPageUrl_Gempub(d));
77 submit_GmRequest(index); /* this is just a local file read */
78 iAssert(isFinished_GmRequest(index));
79 iRangecc src = iNullRange;
80 iRegExp *linkPattern = iClob(newGemtextLink_RegExp());
81 while (nextSplit_Rangecc(range_Block(body_GmRequest(index)), "\n", &src)) {
82 iRangecc line = src;
83 trim_Rangecc(&line);
84 iRegExpMatch m;
85 init_RegExpMatch(&m);
86 if (matchRange_RegExp(linkPattern, line, &m)) {
87 iBeginCollect();
88 const iRangecc url = capturedRange_RegExpMatch(&m, 1);
89 iUrl parts;
90 init_Url(&parts, collectNewRange_String(url));
91 if (isEmpty_Range(&parts.scheme)) {
92 iGempubNavLink link;
93 init_GempubNavLink(&link);
94 set_String(&link.url, absoluteUrl_String(url_GmRequest(index), collectNewRange_String(url)));
95 setRange_String(&link.label, capturedRange_RegExpMatch(&m, 2));
96 trim_String(&link.label);
97 pushBack_Array(d->navLinks, &link);
98 }
99 iEndCollect();
100 }
101 }
102}
103
44void init_Gempub(iGempub *d) { 104void init_Gempub(iGempub *d) {
45 d->arch = NULL; 105 d->arch = NULL;
46 init_String(&d->baseUrl); 106 init_String(&d->baseUrl);
47 iForIndices(i, d->props) { 107 iForIndices(i, d->props) {
48 init_String(&d->props[i]); 108 init_String(&d->props[i]);
49 } 109 }
110 d->navLinks = new_Array(sizeof(iGempubNavLink));
50} 111}
51 112
52void deinit_Gempub(iGempub *d) { 113void deinit_Gempub(iGempub *d) {
114 iForEach(Array, n, d->navLinks) {
115 deinit_GempubNavLink(n.value);
116 }
117 delete_Array(d->navLinks);
53 iForIndices(i, d->props) { 118 iForIndices(i, d->props) {
54 deinit_String(&d->props[i]); 119 deinit_String(&d->props[i]);
55 } 120 }
@@ -140,12 +205,27 @@ void close_Gempub(iGempub *d) {
140 } 205 }
141} 206}
142 207
208void setBaseUrl_Gempub(iGempub *d, const iString *url) {
209 set_String(&d->baseUrl, url);
210}
211
143iBool isOpen_Gempub(const iGempub *d) { 212iBool isOpen_Gempub(const iGempub *d) {
144 return d->arch != NULL; 213 return d->arch != NULL;
145} 214}
146 215
147void setBaseUrl_Gempub(iGempub *d, const iString *url) { 216const iString *indexPageUrl_Gempub(const iGempub *d) {
148 set_String(&d->baseUrl, url); 217 iAssert(!isEmpty_String(&d->baseUrl));
218 iString *dir = collect_String(copy_String(&d->baseUrl));
219 appendCStr_String(dir, "/");
220 return absoluteUrl_String(dir, &d->props[index_GempubProperty]);
221}
222
223const iString *navStartLinkUrl_Gempub(const iGempub *d) {
224 parseNavigationLinks_Gempub_(d);
225 if (isEmpty_Array(d->navLinks)) {
226 return NULL; /* has no navigation structure */
227 }
228 return &((const iGempubNavLink *) constFront_Array(d->navLinks))->url;
149} 229}
150 230
151static iBool hasProperty_Gempub_(const iGempub *d, enum iGempubProperty prop) { 231static iBool hasProperty_Gempub_(const iGempub *d, enum iGempubProperty prop) {
@@ -176,7 +256,7 @@ iString *coverPageSource_Gempub(const iGempub *d) {
176 appendProperty_Gempub_(d, "${gempub.meta.author}:", author_GempubProperty, out); 256 appendProperty_Gempub_(d, "${gempub.meta.author}:", author_GempubProperty, out);
177 if (!isRemote_Gempub_(d)) { 257 if (!isRemote_Gempub_(d)) {
178 appendFormat_String(out, "\n=> %s " book_Icon " ${gempub.cover.view}\n", 258 appendFormat_String(out, "\n=> %s " book_Icon " ${gempub.cover.view}\n",
179 cstrCollect_String(concat_Path(baseUrl, &d->props[index_GempubProperty]))); 259 cstr_String(indexPageUrl_Gempub(d)));
180 if (hasProperty_Gempub_(d, cover_GempubProperty)) { 260 if (hasProperty_Gempub_(d, cover_GempubProperty)) {
181 appendFormat_String(out, "\n=> %s ${gempub.cover.image}\n", 261 appendFormat_String(out, "\n=> %s ${gempub.cover.image}\n",
182 cstrCollect_String(concat_Path(baseUrl, &d->props[cover_GempubProperty]))); 262 cstrCollect_String(concat_Path(baseUrl, &d->props[cover_GempubProperty])));
diff --git a/src/gempub.h b/src/gempub.h
index 6c1103de..fb3c510b 100644
--- a/src/gempub.h
+++ b/src/gempub.h
@@ -56,6 +56,8 @@ iBool isOpen_Gempub (const iGempub *);
56iString * coverPageSource_Gempub (const iGempub *); 56iString * coverPageSource_Gempub (const iGempub *);
57iBool preloadCoverImage_Gempub(const iGempub *, iGmDocument *doc); 57iBool preloadCoverImage_Gempub(const iGempub *, iGmDocument *doc);
58 58
59const iString *property_Gempub (const iGempub *, enum iGempubProperty); 59const iString * property_Gempub (const iGempub *, enum iGempubProperty);
60const iString * indexPageUrl_Gempub (const iGempub *);
61const iString * navStartLinkUrl_Gempub (const iGempub *); /* for convenience */
60 62
61extern const char *mimeType_Gempub; 63extern const char *mimeType_Gempub;
diff --git a/src/gmdocument.c b/src/gmdocument.c
index 2cce9b24..6ea9348e 100644
--- a/src/gmdocument.c
+++ b/src/gmdocument.c
@@ -168,7 +168,7 @@ static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *li
168 /* Returns the human-readable label of the link. */ 168 /* Returns the human-readable label of the link. */
169 static iRegExp *pattern_; 169 static iRegExp *pattern_;
170 if (!pattern_) { 170 if (!pattern_) {
171 pattern_ = new_RegExp("=>\\s*([^\\s]+)(\\s.*)?", caseInsensitive_RegExpOption); 171 pattern_ = newGemtextLink_RegExp();
172 } 172 }
173 iRegExpMatch m; 173 iRegExpMatch m;
174 init_RegExpMatch(&m); 174 init_RegExpMatch(&m);
diff --git a/src/gmutil.c b/src/gmutil.c
index a9d70fbb..2a4f4728 100644
--- a/src/gmutil.c
+++ b/src/gmutil.c
@@ -25,6 +25,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
25#include <the_Foundation/regexp.h> 25#include <the_Foundation/regexp.h>
26#include <the_Foundation/object.h> 26#include <the_Foundation/object.h>
27#include <the_Foundation/path.h> 27#include <the_Foundation/path.h>
28#include <the_Foundation/regexp.h>
29
30iRegExp *newGemtextLink_RegExp(void) {
31 return new_RegExp("=>\\s*([^\\s]+)(\\s.*)?", 0);
32}
28 33
29void init_Url(iUrl *d, const iString *text) { 34void init_Url(iUrl *d, const iString *text) {
30 /* Handle "file:" as a special case since it only has the path part. */ 35 /* Handle "file:" as a special case since it only has the path part. */
@@ -38,7 +43,7 @@ void init_Url(iUrl *d, const iString *text) {
38 static iRegExp *urlPattern_; 43 static iRegExp *urlPattern_;
39 static iRegExp *authPattern_; 44 static iRegExp *authPattern_;
40 if (!urlPattern_) { 45 if (!urlPattern_) {
41 urlPattern_ = new_RegExp("^(([^:/?#]+):)?(//([^/?#]*))?" 46 urlPattern_ = new_RegExp("^(([-.+a-z0-9]+):)?(//([^/?#]*))?"
42 "([^?#]*)(\\?([^#]*))?(#(.*))?", 47 "([^?#]*)(\\?([^#]*))?(#(.*))?",
43 caseInsensitive_RegExpOption); 48 caseInsensitive_RegExpOption);
44 authPattern_ = new_RegExp("(([^@]+)@)?(([^:\\[\\]]+)" 49 authPattern_ = new_RegExp("(([^@]+)@)?(([^:\\[\\]]+)"
diff --git a/src/gmutil.h b/src/gmutil.h
index 84f31e2e..09d333e7 100644
--- a/src/gmutil.h
+++ b/src/gmutil.h
@@ -26,6 +26,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
26#include <the_Foundation/string.h> 26#include <the_Foundation/string.h>
27 27
28iDeclareType(GmError) 28iDeclareType(GmError)
29iDeclareType(RegExp)
29iDeclareType(Url) 30iDeclareType(Url)
30 31
31/* Response status codes. */ 32/* Response status codes. */
@@ -90,6 +91,8 @@ struct Impl_GmError {
90iBool isDefined_GmError (enum iGmStatusCode code); 91iBool isDefined_GmError (enum iGmStatusCode code);
91const iGmError * get_GmError (enum iGmStatusCode code); 92const iGmError * get_GmError (enum iGmStatusCode code);
92 93
94iRegExp * newGemtextLink_RegExp (void);
95
93struct Impl_Url { 96struct Impl_Url {
94 iRangecc scheme; 97 iRangecc scheme;
95 iRangecc host; 98 iRangecc host;
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index ed38ad0b..5a5a4f84 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -298,10 +298,12 @@ struct Impl_DocumentWidget {
298 298
299iDefineObjectConstruction(DocumentWidget) 299iDefineObjectConstruction(DocumentWidget)
300 300
301static int docEnum_ = 0;
302
301void init_DocumentWidget(iDocumentWidget *d) { 303void init_DocumentWidget(iDocumentWidget *d) {
302 iWidget *w = as_Widget(d); 304 iWidget *w = as_Widget(d);
303 init_Widget(w); 305 init_Widget(w);
304 setId_Widget(w, "document000"); 306 setId_Widget(w, format_CStr("document%03d", ++docEnum_));
305 setFlags_Widget(w, hover_WidgetFlag, iTrue); 307 setFlags_Widget(w, hover_WidgetFlag, iTrue);
306 init_PersistentDocumentState(&d->mod); 308 init_PersistentDocumentState(&d->mod);
307 d->flags = 0; 309 d->flags = 0;
@@ -880,7 +882,7 @@ static void updateWindowTitle_DocumentWidget_(const iDocumentWidget *d) {
880 } 882 }
881 /* Take away parts if it doesn't fit. */ 883 /* Take away parts if it doesn't fit. */
882 const int avail = bounds_Widget(as_Widget(tabButton)).size.x - 3 * gap_UI; 884 const int avail = bounds_Widget(as_Widget(tabButton)).size.x - 3 * gap_UI;
883 iBool setWindow = (document_App() == d); 885 iBool setWindow = (document_App() == d && isUnderKeyRoot_Widget(d));
884 for (;;) { 886 for (;;) {
885 iString *text = collect_String(joinCStr_StringArray(title, " \u2014 ")); 887 iString *text = collect_String(joinCStr_StringArray(title, " \u2014 "));
886 if (setWindow) { 888 if (setWindow) {
@@ -1094,6 +1096,7 @@ static const char *zipPageHeading_(const iRangecc mime) {
1094} 1096}
1095 1097
1096static void postProcessRequestContent_DocumentWidget_(iDocumentWidget *d) { 1098static void postProcessRequestContent_DocumentWidget_(iDocumentWidget *d) {
1099 iWidget *w = as_Widget(d);
1097 delete_Gempub(d->sourceGempub); 1100 delete_Gempub(d->sourceGempub);
1098 d->sourceGempub = NULL; 1101 d->sourceGempub = NULL;
1099 if (!cmpCase_String(&d->sourceMime, "application/octet-stream") || 1102 if (!cmpCase_String(&d->sourceMime, "application/octet-stream") ||
@@ -1111,20 +1114,27 @@ static void postProcessRequestContent_DocumentWidget_(iDocumentWidget *d) {
1111 } 1114 }
1112 } 1115 }
1113 if (!d->sourceGempub) { 1116 if (!d->sourceGempub) {
1114 iString *localPath = localFilePathFromUrl_String(d->mod.url); 1117 const iString *localPath = collect_String(localFilePathFromUrl_String(d->mod.url));
1118 iBool isInside = iFalse;
1119 if (localPath && !fileExists_FileInfo(localPath)) {
1120 /* This URL may refer to a file inside the archive. */
1121 localPath = findContainerArchive_Path(localPath);
1122 isInside = iTrue;
1123 }
1115 if (localPath && equal_CStr(mediaType_Path(localPath), "application/gpub+zip")) { 1124 if (localPath && equal_CStr(mediaType_Path(localPath), "application/gpub+zip")) {
1116 iGempub *gempub = new_Gempub(); 1125 iGempub *gempub = new_Gempub();
1117 if (openFile_Gempub(gempub, localPath)) { 1126 if (openFile_Gempub(gempub, localPath)) {
1118 setBaseUrl_Gempub(gempub, d->mod.url); 1127 setBaseUrl_Gempub(gempub, collect_String(makeFileUrl_String(localPath)));
1119 setSource_DocumentWidget(d, collect_String(coverPageSource_Gempub(gempub))); 1128 if (!isInside) {
1120 setCStr_String(&d->sourceMime, mimeType_Gempub); 1129 setSource_DocumentWidget(d, collect_String(coverPageSource_Gempub(gempub)));
1130 setCStr_String(&d->sourceMime, mimeType_Gempub);
1131 }
1121 d->sourceGempub = gempub; 1132 d->sourceGempub = gempub;
1122 } 1133 }
1123 else { 1134 else {
1124 delete_Gempub(gempub); 1135 delete_Gempub(gempub);
1125 } 1136 }
1126 } 1137 }
1127 delete_String(localPath);
1128 } 1138 }
1129 if (d->sourceGempub) { 1139 if (d->sourceGempub) {
1130 if (preloadCoverImage_Gempub(d->sourceGempub, d->doc)) { 1140 if (preloadCoverImage_Gempub(d->sourceGempub, d->doc)) {
@@ -1132,6 +1142,26 @@ static void postProcessRequestContent_DocumentWidget_(iDocumentWidget *d) {
1132 updateVisible_DocumentWidget_(d); 1142 updateVisible_DocumentWidget_(d);
1133 invalidate_DocumentWidget_(d); 1143 invalidate_DocumentWidget_(d);
1134 } 1144 }
1145 if (prefs_App()->pinSplit && equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {
1146 const iString *navStart = navStartLinkUrl_Gempub(d->sourceGempub);
1147 if (navStart) {
1148 iWindow *win = get_Window();
1149 /* Auto-split to show index and the first navigation link. */
1150 if (numRoots_Window(win) == 2) {
1151 /* This document is showing the index page. */
1152 iRoot *other = otherRoot_Window(win, w->root);
1153 postCommandf_Root(other, "open url:%s", cstr_String(navStart));
1154 if (prefs_App()->pinSplit == 1 && w->root == win->roots[1]) {
1155 /* On the wrong side. */
1156 postCommand_App("ui.split swap:1");
1157 }
1158 }
1159 else {
1160 postCommandf_App(
1161 "open newtab:%d url:%s", otherRoot_OpenTabFlag, cstr_String(navStart));
1162 }
1163 }
1164 }
1135 } 1165 }
1136} 1166}
1137 1167
@@ -1925,7 +1955,8 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1925 } 1955 }
1926 return iTrue; 1956 return iTrue;
1927 } 1957 }
1928 else if (equal_Command(cmd, "window.resized") || equal_Command(cmd, "font.changed")) { 1958 else if (equal_Command(cmd, "window.resized") || equal_Command(cmd, "font.changed") ||
1959 equal_Command(cmd, "keyroot.changed")) {
1929 /* Alt/Option key may be involved in window size changes. */ 1960 /* Alt/Option key may be involved in window size changes. */
1930 setLinkNumberMode_DocumentWidget_(d, iFalse); 1961 setLinkNumberMode_DocumentWidget_(d, iFalse);
1931 d->phoneToolbar = findWidget_App("toolbar"); 1962 d->phoneToolbar = findWidget_App("toolbar");
diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c
index 245b76a3..eae0432f 100644
--- a/src/ui/sidebarwidget.c
+++ b/src/ui/sidebarwidget.c
@@ -265,9 +265,10 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {
265 break; 265 break;
266 } 266 }
267 case bookmarks_SidebarMode: { 267 case bookmarks_SidebarMode: {
268 iRegExp *homeTag = iClob(new_RegExp("\\bhomepage\\b", caseSensitive_RegExpOption)); 268 iRegExp *homeTag = iClob(new_RegExp("\\b" homepage_BookmarkTag "\\b", caseSensitive_RegExpOption));
269 iRegExp *subTag = iClob(new_RegExp("\\bsubscribed\\b", caseSensitive_RegExpOption)); 269 iRegExp *subTag = iClob(new_RegExp("\\b" subscribed_BookmarkTag "\\b", caseSensitive_RegExpOption));
270 iRegExp *remoteSourceTag = iClob(new_RegExp("\\bremotesource\\b", caseSensitive_RegExpOption)); 270 iRegExp *remoteSourceTag = iClob(new_RegExp("\\b" remoteSource_BookmarkTag "\\b", caseSensitive_RegExpOption));
271 iRegExp *linkSplitTag = iClob(new_RegExp("\\b" linkSplit_BookmarkTag "\\b", caseSensitive_RegExpOption));
271 iConstForEach(PtrArray, i, list_Bookmarks(bookmarks_App(), cmpTitle_Bookmark_, NULL, NULL)) { 272 iConstForEach(PtrArray, i, list_Bookmarks(bookmarks_App(), cmpTitle_Bookmark_, NULL, NULL)) {
272 const iBookmark *bm = i.ptr; 273 const iBookmark *bm = i.ptr;
273 iSidebarItem *item = new_SidebarItem(); 274 iSidebarItem *item = new_SidebarItem();
@@ -290,6 +291,10 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {
290 appendChar_String(&item->meta, 0x2913); 291 appendChar_String(&item->meta, 0x2913);
291 item->isBold = iTrue; 292 item->isBold = iTrue;
292 } 293 }
294 init_RegExpMatch(&m);
295 if (matchString_RegExp(linkSplitTag, &bm->tags, &m)) {
296 appendChar_String(&item->meta, 0x25e7);
297 }
293 } 298 }
294 addItem_ListWidget(d->list, item); 299 addItem_ListWidget(d->list, item);
295 iRelease(item); 300 iRelease(item);
@@ -828,11 +833,15 @@ iBool handleBookmarkEditorCommands_SidebarWidget_(iWidget *editor, const char *c
828 bm->icon = 0; 833 bm->icon = 0;
829 } 834 }
830 else { 835 else {
831 if (!hasTag_Bookmark(bm, userIcon_BookmarkTag)) { 836 addTagIfMissing_Bookmark(bm, userIcon_BookmarkTag);
832 addTag_Bookmark(bm, userIcon_BookmarkTag);
833 }
834 bm->icon = first_String(icon); 837 bm->icon = first_String(icon);
835 } 838 }
839 addOrRemoveTag_Bookmark(bm, homepage_BookmarkTag,
840 isSelected_Widget(findChild_Widget(editor, "bmed.tag.home")));
841 addOrRemoveTag_Bookmark(bm, remoteSource_BookmarkTag,
842 isSelected_Widget(findChild_Widget(editor, "bmed.tag.remote")));
843 addOrRemoveTag_Bookmark(bm, linkSplit_BookmarkTag,
844 isSelected_Widget(findChild_Widget(editor, "bmed.tag.linksplit")));
836 postCommand_App("bookmarks.changed"); 845 postCommand_App("bookmarks.changed");
837 } 846 }
838 setFlags_Widget(as_Widget(d), disabled_WidgetFlag, iFalse); 847 setFlags_Widget(as_Widget(d), disabled_WidgetFlag, iFalse);
@@ -1012,6 +1021,15 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1012 setText_InputWidget(findChild_Widget(dlg, "bmed.icon"), 1021 setText_InputWidget(findChild_Widget(dlg, "bmed.icon"),
1013 collect_String(newUnicodeN_String(&bm->icon, 1))); 1022 collect_String(newUnicodeN_String(&bm->icon, 1)));
1014 } 1023 }
1024 setFlags_Widget(findChild_Widget(dlg, "bmed.tag.home"),
1025 selected_WidgetFlag,
1026 hasTag_Bookmark(bm, homepage_BookmarkTag));
1027 setFlags_Widget(findChild_Widget(dlg, "bmed.tag.remote"),
1028 selected_WidgetFlag,
1029 hasTag_Bookmark(bm, remoteSource_BookmarkTag));
1030 setFlags_Widget(findChild_Widget(dlg, "bmed.tag.linksplit"),
1031 selected_WidgetFlag,
1032 hasTag_Bookmark(bm, linkSplit_BookmarkTag));
1015 setCommandHandler_Widget(dlg, handleBookmarkEditorCommands_SidebarWidget_); 1033 setCommandHandler_Widget(dlg, handleBookmarkEditorCommands_SidebarWidget_);
1016 setFocus_Widget(findChild_Widget(dlg, "bmed.title")); 1034 setFocus_Widget(findChild_Widget(dlg, "bmed.title"));
1017 } 1035 }
diff --git a/src/ui/util.c b/src/ui/util.c
index d2f27a8b..92cf85b7 100644
--- a/src/ui/util.c
+++ b/src/ui/util.c
@@ -2506,11 +2506,18 @@ static iBool handleBookmarkCreationCommands_SidebarWidget_(iWidget *editor, cons
2506 const iString *tags = text_InputWidget(findChild_Widget(editor, "bmed.tags")); 2506 const iString *tags = text_InputWidget(findChild_Widget(editor, "bmed.tags"));
2507 const iString *icon = collect_String(trimmed_String(text_InputWidget(findChild_Widget(editor, "bmed.icon")))); 2507 const iString *icon = collect_String(trimmed_String(text_InputWidget(findChild_Widget(editor, "bmed.icon"))));
2508 const uint32_t id = add_Bookmarks(bookmarks_App(), url, title, tags, first_String(icon)); 2508 const uint32_t id = add_Bookmarks(bookmarks_App(), url, title, tags, first_String(icon));
2509 iBookmark * bm = get_Bookmarks(bookmarks_App(), id);
2509 if (!isEmpty_String(icon)) { 2510 if (!isEmpty_String(icon)) {
2510 iBookmark *bm = get_Bookmarks(bookmarks_App(), id); 2511 addTagIfMissing_Bookmark(bm, userIcon_BookmarkTag);
2511 if (!hasTag_Bookmark(bm, userIcon_BookmarkTag)) { 2512 }
2512 addTag_Bookmark(bm, userIcon_BookmarkTag); 2513 if (isSelected_Widget(findChild_Widget(editor, "bmed.tag.home"))) {
2513 } 2514 addTag_Bookmark(bm, homepage_BookmarkTag);
2515 }
2516 if (isSelected_Widget(findChild_Widget(editor, "bmed.tag.remote"))) {
2517 addTag_Bookmark(bm, remoteSource_BookmarkTag);
2518 }
2519 if (isSelected_Widget(findChild_Widget(editor, "bmed.tag.linksplit"))) {
2520 addTag_Bookmark(bm, linkSplit_BookmarkTag);
2514 } 2521 }
2515 postCommand_App("bookmarks.changed"); 2522 postCommand_App("bookmarks.changed");
2516 } 2523 }
diff --git a/src/ui/widget.c b/src/ui/widget.c
index 00fe0f5f..67ce1345 100644
--- a/src/ui/widget.c
+++ b/src/ui/widget.c
@@ -1346,6 +1346,12 @@ iBool isHover_Widget(const iAnyObject *d) {
1346 return get_Window()->hover == d; 1346 return get_Window()->hover == d;
1347} 1347}
1348 1348
1349iBool isUnderKeyRoot_Widget(const iAnyObject *d) {
1350 iAssert(isInstance_Object(d, &Class_Widget));
1351 const iWidget *w = d;
1352 return w && get_Window() && w->root == get_Window()->keyRoot;
1353}
1354
1349iBool isSelected_Widget(const iAnyObject *d) { 1355iBool isSelected_Widget(const iAnyObject *d) {
1350 if (d) { 1356 if (d) {
1351 iAssert(isInstance_Object(d, &Class_Widget)); 1357 iAssert(isInstance_Object(d, &Class_Widget));
diff --git a/src/ui/widget.h b/src/ui/widget.h
index 36797210..5b6b18e1 100644
--- a/src/ui/widget.h
+++ b/src/ui/widget.h
@@ -232,6 +232,7 @@ iBool isDisabled_Widget (const iAnyObject *);
232iBool isFocused_Widget (const iAnyObject *); 232iBool isFocused_Widget (const iAnyObject *);
233iBool isHover_Widget (const iAnyObject *); 233iBool isHover_Widget (const iAnyObject *);
234iBool isSelected_Widget (const iAnyObject *); 234iBool isSelected_Widget (const iAnyObject *);
235iBool isUnderKeyRoot_Widget (const iAnyObject *);
235iBool isCommand_Widget (const iWidget *d, const SDL_Event *ev, const char *cmd); 236iBool isCommand_Widget (const iWidget *d, const SDL_Event *ev, const char *cmd);
236iBool hasParent_Widget (const iWidget *d, const iWidget *someParent); 237iBool hasParent_Widget (const iWidget *d, const iWidget *someParent);
237iBool isAffectedByVisualOffset_Widget 238iBool isAffectedByVisualOffset_Widget
diff --git a/src/ui/window.c b/src/ui/window.c
index c4d06d75..a1df2609 100644
--- a/src/ui/window.c
+++ b/src/ui/window.c
@@ -1214,30 +1214,36 @@ void setSplitMode_Window(iWindow *d, int splitFlags) {
1214 /* Add a second root. */ 1214 /* Add a second root. */
1215 iDocumentWidget *moved = document_Root(d->roots[0]); 1215 iDocumentWidget *moved = document_Root(d->roots[0]);
1216 iAssert(d->roots[1] == NULL); 1216 iAssert(d->roots[1] == NULL);
1217 d->roots[1] = new_Root(); 1217 const iBool addToLeft = (prefs_App()->pinSplit == 2);
1218 setCurrent_Root(d->roots[1]); 1218 size_t newRootIndex = 1;
1219 d->keyRoot = d->roots[1]; 1219 if (addToLeft) {
1220 createUserInterface_Root(d->roots[1]); 1220 iSwap(iRoot *, d->roots[0], d->roots[1]);
1221 newRootIndex = 0;
1222 }
1223 d->roots[newRootIndex] = new_Root();
1224 d->keyRoot = d->roots[newRootIndex];
1225 setCurrent_Root(d->roots[newRootIndex]);
1226 createUserInterface_Root(d->roots[newRootIndex]);
1221 if (!isEmpty_String(d->pendingSplitUrl)) { 1227 if (!isEmpty_String(d->pendingSplitUrl)) {
1222 postCommandf_Root(d->roots[1], "open url:%s", 1228 postCommandf_Root(d->roots[newRootIndex], "open url:%s",
1223 cstr_String(d->pendingSplitUrl)); 1229 cstr_String(d->pendingSplitUrl));
1224 clear_String(d->pendingSplitUrl); 1230 clear_String(d->pendingSplitUrl);
1225 } 1231 }
1226 else if (~splitFlags & noEvents_WindowSplit) { 1232 else if (~splitFlags & noEvents_WindowSplit) {
1227 iWidget *docTabs0 = findChild_Widget(d->roots[0]->widget, "doctabs"); 1233 iWidget *docTabs0 = findChild_Widget(d->roots[newRootIndex ^ 1]->widget, "doctabs");
1228 iWidget *docTabs1 = findChild_Widget(d->roots[1]->widget, "doctabs"); 1234 iWidget *docTabs1 = findChild_Widget(d->roots[newRootIndex]->widget, "doctabs");
1229 /* If the old root has multiple tabs, move the current one to the new split. */ 1235 /* If the old root has multiple tabs, move the current one to the new split. */
1230 if (tabCount_Widget(docTabs0) >= 2) { 1236 if (tabCount_Widget(docTabs0) >= 2) {
1231 int movedIndex = tabPageIndex_Widget(docTabs0, moved); 1237 int movedIndex = tabPageIndex_Widget(docTabs0, moved);
1232 removeTabPage_Widget(docTabs0, movedIndex); 1238 removeTabPage_Widget(docTabs0, movedIndex);
1233 showTabPage_Widget(docTabs0, tabPage_Widget(docTabs0, iMax(movedIndex - 1, 0))); 1239 showTabPage_Widget(docTabs0, tabPage_Widget(docTabs0, iMax(movedIndex - 1, 0)));
1234 iRelease(removeTabPage_Widget(docTabs1, 0)); /* delete the default tab */ 1240 iRelease(removeTabPage_Widget(docTabs1, 0)); /* delete the default tab */
1235 setRoot_Widget(as_Widget(moved), d->roots[1]); 1241 setRoot_Widget(as_Widget(moved), d->roots[newRootIndex]);
1236 prependTabPage_Widget(docTabs1, iClob(moved), "", 0, 0); 1242 prependTabPage_Widget(docTabs1, iClob(moved), "", 0, 0);
1237 postCommandf_App("tabs.switch page:%p", moved); 1243 postCommandf_App("tabs.switch page:%p", moved);
1238 } 1244 }
1239 else { 1245 else {
1240 postCommand_Root(d->roots[1], "navigate.home"); 1246 postCommand_Root(d->roots[newRootIndex], "navigate.home");
1241 } 1247 }
1242 } 1248 }
1243 setCurrent_Root(NULL); 1249 setCurrent_Root(NULL);