diff options
-rw-r--r-- | res/about/help.gmi | 113 | ||||
-rw-r--r-- | res/about/version.gmi | 32 | ||||
-rw-r--r-- | src/app.c | 7 | ||||
-rw-r--r-- | src/bookmarks.h | 20 | ||||
-rw-r--r-- | src/gempub.c | 88 | ||||
-rw-r--r-- | src/gempub.h | 4 | ||||
-rw-r--r-- | src/gmdocument.c | 2 | ||||
-rw-r--r-- | src/gmutil.c | 7 | ||||
-rw-r--r-- | src/gmutil.h | 3 | ||||
-rw-r--r-- | src/ui/documentwidget.c | 47 | ||||
-rw-r--r-- | src/ui/sidebarwidget.c | 30 | ||||
-rw-r--r-- | src/ui/util.c | 15 | ||||
-rw-r--r-- | src/ui/widget.c | 6 | ||||
-rw-r--r-- | src/ui/widget.h | 1 | ||||
-rw-r--r-- | src/ui/window.c | 24 |
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 | ||
71 | Tip: Try pressing ${CTRL+}5 now to see the page outline. | 75 | Tip: 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 | ||
105 | When 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. | 109 | When 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 | ||
111 | If 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 | ||
109 | When 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. | 115 | When 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 | ||
117 | Holding 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. | 123 | Holding 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 | |||
119 | Right-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. | 127 | Right-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 | |||
131 | Holding 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 | ||
123 | Lagrange 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. | 135 | Lagrange 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 | ||
133 | The set of open tabs and their full contents are saved when you quit the application and restored when relaunch it. | 145 | The 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 | |||
149 | When 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 | ||
137 | A 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. | 153 | A 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 | ||
254 | The location where downloaded files are saved can be changed in Preferences. The default location is "Downloads" in your home directory. | 271 | The 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 | |||
275 | By 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 | |||
277 | Split 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 | |||
279 | View 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 | |||
281 | Another way to activate split view mode is to click on a link while holding ${SHIFT}. | ||
282 | |||
283 | Each 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 | |||
287 | At 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 | |||
289 | To 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 | |||
291 | You may also press Tab to cycle input focus between all the URL input fields. | ||
292 | |||
293 | ### 1.8.2 Pinning | ||
294 | |||
295 | While 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 | |||
297 | This 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 | |||
303 | The 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 | |||
309 | When 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 | |||
311 | Note 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 | |||
315 | Gempub 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 | |||
319 | Lagrange 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 | ||
258 | You can find a number of settings in Preferences to customize the user interface and page contents. | 323 | You 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 | ||
262 | One 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. | 327 | One 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 | ||
329 | The "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 | ||
266 | Lagrange supports configuring dark and light UI modes separately. | 335 | Lagrange 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 | ||
308 | Other style options: | 377 | Other style options: |
@@ -357,7 +426,13 @@ Likewise (on macOS), .gmi/.gemini file extensions are registered as file formats | |||
357 | 426 | ||
358 | You 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. | 427 | You 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 | |||
431 | Only 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 | |||
433 | If 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 | ||
362 | Lagrange stores user-specific persistent files in one of the following locations (depending on the operating system): | 437 | Lagrange 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 | ``` |
450 | The 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. | 525 | The 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 |
455 | Override 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: | 530 | Override 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. | 11 | Browsing: |
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 | |||
19 | Page 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 | |
31 | Bug 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. |
@@ -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 | ||
53 | iLocalDef uint32_t id_Bookmark (const iBookmark *d) { return d->node.key; } | 53 | iLocalDef uint32_t id_Bookmark (const iBookmark *d) { return d->node.key; } |
54 | 54 | ||
55 | iBool hasTag_Bookmark (const iBookmark *d, const char *tag); | 55 | iBool hasTag_Bookmark (const iBookmark *, const char *tag); |
56 | void addTag_Bookmark (iBookmark *d, const char *tag); | 56 | void addTag_Bookmark (iBookmark *, const char *tag); |
57 | void removeTag_Bookmark (iBookmark *d, const char *tag); | 57 | void removeTag_Bookmark (iBookmark *, const char *tag); |
58 | |||
59 | iLocalDef void addTagIfMissing_Bookmark(iBookmark *d, const char *tag) { | ||
60 | if (!hasTag_Bookmark(d, tag)) { | ||
61 | addTag_Bookmark(d, tag); | ||
62 | } | ||
63 | } | ||
64 | iLocalDef 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 | ||
34 | const char *mimeType_Gempub = "application/gpub+zip"; | 37 | const char *mimeType_Gempub = "application/gpub+zip"; |
35 | 38 | ||
39 | /*----------------------------------------------------------------------------------------------*/ | ||
40 | |||
41 | iDeclareType(GempubNavLink) | ||
42 | |||
43 | struct Impl_GempubNavLink { | ||
44 | iString url; | ||
45 | iString label; | ||
46 | }; | ||
47 | |||
48 | static void init_GempubNavLink(iGempubNavLink *d) { | ||
49 | init_String(&d->url); | ||
50 | init_String(&d->label); | ||
51 | } | ||
52 | |||
53 | static void deinit_GempubNavLink(iGempubNavLink *d) { | ||
54 | deinit_String(&d->url); | ||
55 | deinit_String(&d->label); | ||
56 | } | ||
57 | |||
58 | iDefineTypeConstruction(GempubNavLink) | ||
59 | |||
60 | /*----------------------------------------------------------------------------------------------*/ | ||
61 | |||
36 | struct Impl_Gempub { | 62 | struct 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 | ||
42 | iDefineTypeConstruction(Gempub) | 69 | iDefineTypeConstruction(Gempub) |
43 | 70 | ||
71 | static 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 | |||
44 | void init_Gempub(iGempub *d) { | 104 | void 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 | ||
52 | void deinit_Gempub(iGempub *d) { | 113 | void 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 | ||
208 | void setBaseUrl_Gempub(iGempub *d, const iString *url) { | ||
209 | set_String(&d->baseUrl, url); | ||
210 | } | ||
211 | |||
143 | iBool isOpen_Gempub(const iGempub *d) { | 212 | iBool isOpen_Gempub(const iGempub *d) { |
144 | return d->arch != NULL; | 213 | return d->arch != NULL; |
145 | } | 214 | } |
146 | 215 | ||
147 | void setBaseUrl_Gempub(iGempub *d, const iString *url) { | 216 | const 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 | |||
223 | const 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 | ||
151 | static iBool hasProperty_Gempub_(const iGempub *d, enum iGempubProperty prop) { | 231 | static 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 *); | |||
56 | iString * coverPageSource_Gempub (const iGempub *); | 56 | iString * coverPageSource_Gempub (const iGempub *); |
57 | iBool preloadCoverImage_Gempub(const iGempub *, iGmDocument *doc); | 57 | iBool preloadCoverImage_Gempub(const iGempub *, iGmDocument *doc); |
58 | 58 | ||
59 | const iString *property_Gempub (const iGempub *, enum iGempubProperty); | 59 | const iString * property_Gempub (const iGempub *, enum iGempubProperty); |
60 | const iString * indexPageUrl_Gempub (const iGempub *); | ||
61 | const iString * navStartLinkUrl_Gempub (const iGempub *); /* for convenience */ | ||
60 | 62 | ||
61 | extern const char *mimeType_Gempub; | 63 | extern 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 | |||
30 | iRegExp *newGemtextLink_RegExp(void) { | ||
31 | return new_RegExp("=>\\s*([^\\s]+)(\\s.*)?", 0); | ||
32 | } | ||
28 | 33 | ||
29 | void init_Url(iUrl *d, const iString *text) { | 34 | void 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 | ||
28 | iDeclareType(GmError) | 28 | iDeclareType(GmError) |
29 | iDeclareType(RegExp) | ||
29 | iDeclareType(Url) | 30 | iDeclareType(Url) |
30 | 31 | ||
31 | /* Response status codes. */ | 32 | /* Response status codes. */ |
@@ -90,6 +91,8 @@ struct Impl_GmError { | |||
90 | iBool isDefined_GmError (enum iGmStatusCode code); | 91 | iBool isDefined_GmError (enum iGmStatusCode code); |
91 | const iGmError * get_GmError (enum iGmStatusCode code); | 92 | const iGmError * get_GmError (enum iGmStatusCode code); |
92 | 93 | ||
94 | iRegExp * newGemtextLink_RegExp (void); | ||
95 | |||
93 | struct Impl_Url { | 96 | struct 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 | ||
299 | iDefineObjectConstruction(DocumentWidget) | 299 | iDefineObjectConstruction(DocumentWidget) |
300 | 300 | ||
301 | static int docEnum_ = 0; | ||
302 | |||
301 | void init_DocumentWidget(iDocumentWidget *d) { | 303 | void 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 | ||
1096 | static void postProcessRequestContent_DocumentWidget_(iDocumentWidget *d) { | 1098 | static 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 | ||
1349 | iBool 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 | |||
1349 | iBool isSelected_Widget(const iAnyObject *d) { | 1355 | iBool 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 *); | |||
232 | iBool isFocused_Widget (const iAnyObject *); | 232 | iBool isFocused_Widget (const iAnyObject *); |
233 | iBool isHover_Widget (const iAnyObject *); | 233 | iBool isHover_Widget (const iAnyObject *); |
234 | iBool isSelected_Widget (const iAnyObject *); | 234 | iBool isSelected_Widget (const iAnyObject *); |
235 | iBool isUnderKeyRoot_Widget (const iAnyObject *); | ||
235 | iBool isCommand_Widget (const iWidget *d, const SDL_Event *ev, const char *cmd); | 236 | iBool isCommand_Widget (const iWidget *d, const SDL_Event *ev, const char *cmd); |
236 | iBool hasParent_Widget (const iWidget *d, const iWidget *someParent); | 237 | iBool hasParent_Widget (const iWidget *d, const iWidget *someParent); |
237 | iBool isAffectedByVisualOffset_Widget | 238 | iBool 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); |