diff options
author | SolidHal <hal@halemmerich.com> | 2020-12-08 20:46:57 -0600 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-12-08 20:46:57 -0600 |
commit | cd848c14d8c5d1fd66acded70cc17d2a8009383f (patch) | |
tree | b8c932a745f77845f79aea464d5195ce36109ee9 | |
parent | 9fde33bb6f8149cc8dee7ac626b8b56f9f1cd14e (diff) | |
parent | eae5e61bba99c97af3d8b27dab39d50aedeb3b04 (diff) |
Merge pull request #1 from skyjake/dev
bring up to date
-rw-r--r-- | CMakeLists.txt | 6 | ||||
-rwxr-xr-x | debian/rules | 2 | ||||
m--------- | lib/the_Foundation | 0 | ||||
-rw-r--r-- | res/MacOSXBundleInfo.plist.in | 22 | ||||
-rw-r--r-- | res/about/help.gmi | 102 | ||||
-rw-r--r-- | res/about/version.gmi | 29 | ||||
-rw-r--r-- | src/app.c | 106 | ||||
-rw-r--r-- | src/app.h | 1 | ||||
-rw-r--r-- | src/gmrequest.c | 14 | ||||
-rw-r--r-- | src/gmutil.c | 5 | ||||
-rw-r--r-- | src/mimehooks.c | 71 | ||||
-rw-r--r-- | src/mimehooks.h | 4 | ||||
-rw-r--r-- | src/ui/documentwidget.c | 24 | ||||
-rw-r--r-- | src/ui/keys.h | 2 | ||||
-rw-r--r-- | src/ui/sidebarwidget.c | 218 | ||||
-rw-r--r-- | src/ui/sidebarwidget.h | 7 | ||||
-rw-r--r-- | src/ui/text.c | 71 | ||||
-rw-r--r-- | src/ui/text.h | 16 | ||||
-rw-r--r-- | src/ui/util.c | 8 | ||||
-rw-r--r-- | src/ui/widget.c | 13 | ||||
-rw-r--r-- | src/ui/widget.h | 9 | ||||
-rw-r--r-- | src/ui/window.c | 21 |
22 files changed, 562 insertions, 189 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 0c200e21..1033772e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt | |||
@@ -18,7 +18,7 @@ | |||
18 | cmake_minimum_required (VERSION 3.9) | 18 | cmake_minimum_required (VERSION 3.9) |
19 | 19 | ||
20 | project (Lagrange | 20 | project (Lagrange |
21 | VERSION 0.12.0 | 21 | VERSION 0.12.2 |
22 | DESCRIPTION "A Beautiful Gemini Client" | 22 | DESCRIPTION "A Beautiful Gemini Client" |
23 | LANGUAGES C | 23 | LANGUAGES C |
24 | ) | 24 | ) |
@@ -30,6 +30,7 @@ option (ENABLE_X11_SWRENDER "Use software rendering under X11" OFF) | |||
30 | option (ENABLE_KERNING "Enable kerning in font renderer (slower)" ON) | 30 | option (ENABLE_KERNING "Enable kerning in font renderer (slower)" ON) |
31 | option (ENABLE_RESOURCE_EMBED "Embed resources inside the executable" OFF) | 31 | option (ENABLE_RESOURCE_EMBED "Embed resources inside the executable" OFF) |
32 | option (ENABLE_WINDOWPOS_FIX "Set position after showing window (workaround for SDL bug)" OFF) | 32 | option (ENABLE_WINDOWPOS_FIX "Set position after showing window (workaround for SDL bug)" OFF) |
33 | option (ENABLE_IDLE_SLEEP "While idle, sleep in the main thread instead of waiting for events" ON) | ||
33 | 34 | ||
34 | include (BuildType.cmake) | 35 | include (BuildType.cmake) |
35 | include (res/Embed.cmake) | 36 | include (res/Embed.cmake) |
@@ -219,6 +220,9 @@ if (ENABLE_MPG123 AND MPG123_FOUND) | |||
219 | target_compile_definitions (app PUBLIC LAGRANGE_ENABLE_MPG123=1) | 220 | target_compile_definitions (app PUBLIC LAGRANGE_ENABLE_MPG123=1) |
220 | target_link_libraries (app PUBLIC PkgConfig::MPG123) | 221 | target_link_libraries (app PUBLIC PkgConfig::MPG123) |
221 | endif () | 222 | endif () |
223 | if (ENABLE_IDLE_SLEEP) | ||
224 | target_compile_definitions (app PUBLIC LAGRANGE_IDLE_SLEEP=1) | ||
225 | endif () | ||
222 | target_link_libraries (app PUBLIC the_Foundation::the_Foundation) | 226 | target_link_libraries (app PUBLIC the_Foundation::the_Foundation) |
223 | target_link_libraries (app PUBLIC ${SDL2_LDFLAGS}) | 227 | target_link_libraries (app PUBLIC ${SDL2_LDFLAGS}) |
224 | if (APPLE) | 228 | if (APPLE) |
diff --git a/debian/rules b/debian/rules index f4578eea..9e306b0b 100755 --- a/debian/rules +++ b/debian/rules | |||
@@ -6,7 +6,7 @@ export DEB_BUILD_MAINT_OPTIONS=hardening=-format | |||
6 | dh $@ | 6 | dh $@ |
7 | 7 | ||
8 | override_dh_auto_configure: | 8 | override_dh_auto_configure: |
9 | cmake .. -DENABLE_WINDOWPOS_FIX=YES -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=$(pwd)/../usr | 9 | cmake .. -DCMAKE_BUILD_TYPE=Release -DENABLE_WINDOWPOS_FIX=YES -DTFDN_ENABLE_SSE41=NO -DCMAKE_INSTALL_PREFIX=$(pwd)/../usr |
10 | 10 | ||
11 | override_dh_build_configure: | 11 | override_dh_build_configure: |
12 | cmake --build . | 12 | cmake --build . |
diff --git a/lib/the_Foundation b/lib/the_Foundation | |||
Subproject 17fd22771266a562eecd861519d1587885b26c2 | Subproject 4514a181458dfa02d73aafd6498e101b63eaa47 | ||
diff --git a/res/MacOSXBundleInfo.plist.in b/res/MacOSXBundleInfo.plist.in index 79cf7627..daa71574 100644 --- a/res/MacOSXBundleInfo.plist.in +++ b/res/MacOSXBundleInfo.plist.in | |||
@@ -54,16 +54,16 @@ | |||
54 | <false/> | 54 | <false/> |
55 | </dict> | 55 | </dict> |
56 | </array> | 56 | </array> |
57 | <key>CFBundleURLTypes</key> | 57 | <key>CFBundleURLTypes</key> |
58 | <array> | 58 | <array> |
59 | <dict> | 59 | <dict> |
60 | <key>CFBundleURLName</key> | 60 | <key>CFBundleURLName</key> |
61 | <string>Gemini</string> | 61 | <string>Gemini</string> |
62 | <key>CFBundleURLSchemes</key> | 62 | <key>CFBundleURLSchemes</key> |
63 | <array> | 63 | <array> |
64 | <string>gemini</string> | 64 | <string>gemini</string> |
65 | </array> | 65 | </array> |
66 | </dict> | 66 | </dict> |
67 | </array> | 67 | </array> |
68 | </dict> | 68 | </dict> |
69 | </plist> | 69 | </plist> |
diff --git a/res/about/help.gmi b/res/about/help.gmi index 3f21adb8..f71f5164 100644 --- a/res/about/help.gmi +++ b/res/about/help.gmi | |||
@@ -63,7 +63,7 @@ Lagrange's user interface is modeled after web browsers: | |||
63 | 63 | ||
64 | * There is a navigation bar at the top with Back and Forward buttons. | 64 | * There is a navigation bar at the top with Back and Forward buttons. |
65 | * There is a tab bar for switching tabs. The tab bar is hidden if there is only one tab open. | 65 | * There is a tab bar for switching tabs. The tab bar is hidden if there is only one tab open. |
66 | * There is a sidebar for managing bookmarks and TLS identities, and viewing history and the page outline. The sidebar is hidden by default. | 66 | * There is a sidebar for managing bookmarks and TLS identities, and viewing history and the page outline. Sidebars are hidden by default. |
67 | * There is a search bar that appears at the bottom when searching text on the page. | 67 | * There is a search bar that appears at the bottom when searching text on the page. |
68 | 68 | ||
69 | Tip: Try pressing ${CTRL+}5 now to see the page outline. | 69 | Tip: Try pressing ${CTRL+}5 now to see the page outline. |
@@ -126,17 +126,17 @@ Press ${CTRL+}T to open a new tab, and ${CTRL+}W to close the current tab. Right | |||
126 | 126 | ||
127 | The set of open tabs is restored when you launch Lagrange. | 127 | The set of open tabs is restored when you launch Lagrange. |
128 | 128 | ||
129 | ## Sidebar | 129 | ## Sidebars |
130 | 130 | ||
131 | The sidebar can be toggled via menus or by pressing ${SHIFT+}${CTRL+}L. It has five tabs: | 131 | The sidebars can be toggled via menus or by pressing ${SHIFT+}${CTRL+}L or ${SHIFT+}${CTRL+}P (for the left/right sidebar, respectively). Both sidebars have five tabs: |
132 | 132 | ||
133 | * Bookmarks: List of bookmarks that you've created. These appear first in search results for quick and easy access. | 133 | * Bookmarks: List of bookmarks that you've created. These appear first in search results for quick and easy access. |
134 | * Feeds: Entries found on subscribed feed index pages. | 134 | * Feeds: Entries found on subscribed pages. |
135 | * History: Chronological list of visited URLs. This is not a full history of all the URLs you've accessed over time — only unique URLs are shown at the latest access time. | 135 | * History: Chronological list of visited URLs. This is not a full history of all the URLs you've accessed over time — only unique URLs are shown at the latest access time. |
136 | * Identities: TLS client certificates. | 136 | * Identities: TLS client certificates. |
137 | * Outline: List of the headings in the currently open tab. Useful when reading longer documents. | 137 | * Outline: List of the headings in the currently open tab. Useful when reading longer documents. |
138 | 138 | ||
139 | ${CTRL+}1 through ${CTRL+}5 switch between the sidebar tabs, or hide the sidebar if the current tab's key is pressed. You can also press Escape to dismiss the sidebar. | 139 | ${CTRL+}1 through ${CTRL+}5 switch between the left sidebar tabs, or hide the sidebar if the current tab's key is pressed. You can also press Escape to dismiss sidebars. |
140 | 140 | ||
141 | ## Bookmarks | 141 | ## Bookmarks |
142 | 142 | ||
@@ -150,10 +150,11 @@ In addition to a title, bookmarks can have tags. Some tags have a special meanin | |||
150 | 150 | ||
151 | * Set a "homepage" tag on a bookmark to make it one of the pages that will be opened when pressing the 🏠 button. If multiple bookmarks are tagged as homepages, a random one is selected. | 151 | * Set a "homepage" tag on a bookmark to make it one of the pages that will be opened when pressing the 🏠 button. If multiple bookmarks are tagged as homepages, a random one is selected. |
152 | * A "subscribed" tag means that Lagrange will periodically fetch the bookmarked page and look for Gemini feed links. All the found links that match the required style ("YYYY-MM-DD Entry title") will appear in the Feeds sidebar tab. | 152 | * A "subscribed" tag means that Lagrange will periodically fetch the bookmarked page and look for Gemini feed links. All the found links that match the required style ("YYYY-MM-DD Entry title") will appear in the Feeds sidebar tab. |
153 | * "headings" can be used together with "subscribed" to subscribe to new headings instead of Gemini feed links. | ||
153 | 154 | ||
154 | ## Feeds | 155 | ## Feeds |
155 | 156 | ||
156 | You may be familiar with RSS and Atom XML feeds from the web. Lagrange does _not_ support RSS/Atom, only Gemini feeds. A Gemini feed is simply a regular 'text/gemini' page that contains one or more links whose labels are formatted in a particular way. | 157 | You may be familiar with RSS and Atom XML feeds from the web. Lagrange does _not_ support RSS or Atom, only Gemini feeds. A Gemini feed is simply a regular 'text/gemini' page that contains one or more links whose labels are formatted in a particular way. |
157 | 158 | ||
158 | => gemini://gemini.circumlunar.space/docs/companion/subscription.gmi See "Subscribing to Gemini pages" for more information. | 159 | => gemini://gemini.circumlunar.space/docs/companion/subscription.gmi See "Subscribing to Gemini pages" for more information. |
159 | 160 | ||
@@ -213,8 +214,6 @@ You can find a number of settings in Preferences to customize the user interface | |||
213 | 214 | ||
214 | ### Browsing behavior | 215 | ### Browsing behavior |
215 | 216 | ||
216 | The "Outline on scrollbar" option shows the page outline when the mouse cursor is moved over the scrollbar, allowing one to better navigate long documents. The outline will disappear after the mouse cursor is moved away. If you need a persistent outline, one is always available in the sidebar. | ||
217 | |||
218 | 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. | 217 | 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. |
219 | 218 | ||
220 | ### Window and UI options | 219 | ### Window and UI options |
@@ -315,12 +314,99 @@ Other Unix : ~/.config/lagrange/ | |||
315 | 314 | ||
316 | * bindings.txt | 315 | * bindings.txt |
317 | * bookmarks.txt | 316 | * bookmarks.txt |
317 | * feeds.txt | ||
318 | * idents.binary and idents/ | 318 | * idents.binary and idents/ |
319 | * mimehooks.txt | ||
319 | * prefs.cfg | 320 | * prefs.cfg |
320 | * state.binary | 321 | * state.binary |
321 | * trusted.txt | 322 | * trusted.txt |
322 | * visited.txt | 323 | * visited.txt |
323 | 324 | ||
325 | # MIME hooks | ||
326 | |||
327 | MIME hooks enable piping Gemini responses through external programs for arbitrary processing. This allows leveraging scripting languages and binary executables to perform format conversions, content transformation, and data analysis. | ||
328 | |||
329 | Hooks are configured using the file "mimehooks.txt" in Lagrange's config directory. Each hook has a regexp that is matched against the response MIME type and parameters, and each matching hook is offered the response body via stdin. The called external programs are free to rewrite the entire response, including the MIME type. If one of the hooks returns a valid response, it is used as the final response of the Gemini request. | ||
330 | |||
331 | Example use cases: | ||
332 | * Parsing an Atom XML feed and generating a Gemini feed index page | ||
333 | * Rendering a MIDI file using Timidity as PCM WAV | ||
334 | * Generate a graph PNG based on values in a CSV | ||
335 | * Converting HTML or Markdown to 'text/gemini' | ||
336 | * Language translations | ||
337 | |||
338 | ## Interface | ||
339 | |||
340 | When a hook is called, it is given the MIME type and parameters via command line arguments. The response body is provided via stdin. The request's URL is available in the REQUEST_URL environment variable. | ||
341 | |||
342 | The MIME type and parameters are split at semicolons, so "text/gemini; lang=ja" would be called as: | ||
343 | ``` | ||
344 | hookprogram text/gemini lang=ja | ||
345 | ``` | ||
346 | |||
347 | Output from the program (via stdout) must be a valid "20" Gemini response that includes the new MIME type: | ||
348 | ``` | ||
349 | 20 text/gemini; lang=en\r\n | ||
350 | {...body...} | ||
351 | ``` | ||
352 | |||
353 | Any output that does not follow this format is considered to mean that the hook refused to process the contents. The next hook will be offered the response instead. | ||
354 | |||
355 | ## mimehooks.txt syntax | ||
356 | |||
357 | Like other Lagrange configuration lines, mimehooks.txt has a simple line-oriented syntax. Lagrange must be restarted for changes to the configuration file to take effect. | ||
358 | |||
359 | Each hook is specified as three lines: | ||
360 | * A human-readable label (for reporting to the user) | ||
361 | * MIME type/parameter regular expression | ||
362 | * Command to execute, plus additional arguments each separated with semicolons | ||
363 | |||
364 | For example: | ||
365 | ``` | ||
366 | Convert Atom to Gemini feed | ||
367 | application/xml | ||
368 | /usr/bin/python3;/home/jaakko/atomconv.py | ||
369 | ``` | ||
370 | |||
371 | The hook program is executed directly without involving the shell. This means scripts must be invoked via the interpreter executable. | ||
372 | |||
373 | ## Example: Converting from Atom to Gemini | ||
374 | |||
375 | The following simple Python script demonstrates how a MIME hook could be used to parse an Atom XML document using Python 3 and output a Gemini feed index page based on the parsed entries. This is just a simple example; a more robust script could include more content from the Atom feed and handle errors, too. | ||
376 | ``` | ||
377 | import sys | ||
378 | import xml.etree.ElementTree as ET | ||
379 | |||
380 | def atomtag(n): | ||
381 | return '{http://www.w3.org/2005/Atom}' + n | ||
382 | |||
383 | root = ET.fromstring(sys.stdin.read()) | ||
384 | if root.tag != atomtag('feed'): | ||
385 | sys.exit(0) | ||
386 | feed_title = '' | ||
387 | feed_author = '' | ||
388 | feed_entries = [] | ||
389 | for child in root: | ||
390 | if child.tag == atomtag('title'): | ||
391 | feed_title = child.text | ||
392 | elif child.tag == atomtag('entry'): | ||
393 | feed_entries.append(child) | ||
394 | print("20 text/gemini\r") | ||
395 | print(f'# {feed_title}') | ||
396 | for entry in feed_entries: | ||
397 | entry_date = '' | ||
398 | entry_title = '' | ||
399 | entry_link = '' | ||
400 | for child in entry: | ||
401 | if child.tag == atomtag('updated'): | ||
402 | entry_date = child.text[:10] | ||
403 | elif child.tag == atomtag('title'): | ||
404 | entry_title = child.text | ||
405 | elif child.tag == atomtag('link'): | ||
406 | entry_link = child.attrib['href'] | ||
407 | print(f'=> {entry_link} {entry_date} {entry_title}') | ||
408 | ``` | ||
409 | |||
324 | # Open source licenses | 410 | # Open source licenses |
325 | 411 | ||
326 | => about:license | 412 | => about:license |
diff --git a/res/about/version.gmi b/res/about/version.gmi index 85490dc7..2020b46c 100644 --- a/res/about/version.gmi +++ b/res/about/version.gmi | |||
@@ -6,8 +6,35 @@ | |||
6 | ``` | 6 | ``` |
7 | # Release notes | 7 | # Release notes |
8 | 8 | ||
9 | ## 0.12.2 | ||
10 | |||
11 | ## 0.12.1 | ||
12 | * 'text/*' content falls back to plain text. | ||
13 | * Minimized visual artifacts in Unicode box-drawing characters (overlapping/gaps) by fine-tuning glyph scaling. | ||
14 | * Fixed truncated tab titles when opening tabs in background. | ||
15 | * Fixed possible exit if hook program not found (SIGPIPE). | ||
16 | * REQUEST_URL is set in the environment when running MIME hooks. | ||
17 | * "about:debug" lists the configured MIME hooks. | ||
18 | * macOS: Fixed excessive CPU usage while idling. | ||
19 | |||
9 | ## 0.12 | 20 | ## 0.12 |
10 | * Subscribe to new headings on pages. | 21 | * Added MIME hooks: pipe Gemini responses through external programs for arbitrary processing. (See "about:help" for usage.) |
22 | * Added a right-hand sidebar; have a sidebar on the right or on both sides at once. | ||
23 | * Added a clear warning banner when there is an issue with the server's TLS certificate. | ||
24 | * Follow Weiph/pikkulogs — subscribe to new headings on pages. | ||
25 | * Added UI for subscribing: feed name, entry type (Gemini feed or new headings). | ||
26 | * Added keyboard shortcut ${SHIFT+}${CTRL+}D for subscribing to page. | ||
27 | * Feeds sidebar is capped to 100 entries. "about:feeds" shows all known entries. | ||
28 | * Network connections have a timeout in case server doesn't respond at all. | ||
29 | * Adjusted spacing before/after links to reflect use of empty lines in the source. | ||
30 | * Clicking on page area unfocuses URL input field. | ||
31 | * Added keybindings for switching tabs. | ||
32 | * Gopher: Query links have a 🔍 icon. | ||
33 | * Fixed handling of "file:///" URIs on Windows. | ||
34 | * Fixed misaligned Unicode box-drawing characters. | ||
35 | * Fixed missing error page if status code is unknown (torture test 34). | ||
36 | * Fixed detection of invalid headers (torture test 39). | ||
37 | * Fixed rendering of soft hyphens (torture test 50). | ||
11 | 38 | ||
12 | ## 0.11 | 39 | ## 0.11 |
13 | * Added feed subscriptions. A subscription is any bookmark with the "subscribed" tag. Subscribed feeds are refreshed in the background while Lagrange is running. | 40 | * Added feed subscriptions. A subscription is any bookmark with the "subscribed" tag. Subscribed feeds are refreshed in the background while Lagrange is running. |
@@ -89,6 +89,8 @@ static const char *prefsFileName_App_ = "prefs.cfg"; | |||
89 | static const char *stateFileName_App_ = "state.binary"; | 89 | static const char *stateFileName_App_ = "state.binary"; |
90 | static const char *downloadDir_App_ = "~/Downloads"; | 90 | static const char *downloadDir_App_ = "~/Downloads"; |
91 | 91 | ||
92 | static const int idleThreshold_App_ = 1000; /* ms */ | ||
93 | |||
92 | struct Impl_App { | 94 | struct Impl_App { |
93 | iCommandLine args; | 95 | iCommandLine args; |
94 | iString * execPath; | 96 | iString * execPath; |
@@ -100,7 +102,12 @@ struct Impl_App { | |||
100 | iSortedArray tickers; | 102 | iSortedArray tickers; |
101 | uint32_t lastTickerTime; | 103 | uint32_t lastTickerTime; |
102 | uint32_t elapsedSinceLastTicker; | 104 | uint32_t elapsedSinceLastTicker; |
103 | iBool running; | 105 | iBool isRunning; |
106 | #if defined (LAGRANGE_IDLE_SLEEP) | ||
107 | iBool isIdling; | ||
108 | uint32_t lastEventTime; | ||
109 | int sleepTimer; | ||
110 | #endif | ||
104 | iAtomicInt pendingRefresh; | 111 | iAtomicInt pendingRefresh; |
105 | int tabEnum; | 112 | int tabEnum; |
106 | iStringList *launchCommands; | 113 | iStringList *launchCommands; |
@@ -139,7 +146,8 @@ const iString *dateStr_(const iDate *date) { | |||
139 | 146 | ||
140 | static iString *serializePrefs_App_(const iApp *d) { | 147 | static iString *serializePrefs_App_(const iApp *d) { |
141 | iString *str = new_String(); | 148 | iString *str = new_String(); |
142 | const iSidebarWidget *sidebar = findWidget_App("sidebar"); | 149 | const iSidebarWidget *sidebar = findWidget_App("sidebar"); |
150 | const iSidebarWidget *sidebar2 = findWidget_App("sidebar2"); | ||
143 | appendFormat_String(str, "window.retain arg:%d\n", d->prefs.retainWindowSize); | 151 | appendFormat_String(str, "window.retain arg:%d\n", d->prefs.retainWindowSize); |
144 | if (d->prefs.retainWindowSize) { | 152 | if (d->prefs.retainWindowSize) { |
145 | const iBool isMaximized = (SDL_GetWindowFlags(d->window->win) & SDL_WINDOW_MAXIMIZED) != 0; | 153 | const iBool isMaximized = (SDL_GetWindowFlags(d->window->win) & SDL_WINDOW_MAXIMIZED) != 0; |
@@ -150,6 +158,7 @@ static iString *serializePrefs_App_(const iApp *d) { | |||
150 | h = d->window->lastRect.size.y; | 158 | h = d->window->lastRect.size.y; |
151 | appendFormat_String(str, "window.setrect width:%d height:%d coord:%d %d\n", w, h, x, y); | 159 | appendFormat_String(str, "window.setrect width:%d height:%d coord:%d %d\n", w, h, x, y); |
152 | appendFormat_String(str, "sidebar.width arg:%d\n", width_SidebarWidget(sidebar)); | 160 | appendFormat_String(str, "sidebar.width arg:%d\n", width_SidebarWidget(sidebar)); |
161 | appendFormat_String(str, "sidebar2.width arg:%d\n", width_SidebarWidget(sidebar2)); | ||
153 | /* On macOS, maximization should be applied at creation time or the window will take | 162 | /* On macOS, maximization should be applied at creation time or the window will take |
154 | a moment to animate to its maximized size. */ | 163 | a moment to animate to its maximized size. */ |
155 | #if !defined (iPlatformApple) | 164 | #if !defined (iPlatformApple) |
@@ -160,10 +169,16 @@ static iString *serializePrefs_App_(const iApp *d) { | |||
160 | iUnused(isMaximized); | 169 | iUnused(isMaximized); |
161 | #endif | 170 | #endif |
162 | } | 171 | } |
163 | if (isVisible_Widget(sidebar)) { | 172 | /* Sidebars. */ { |
164 | appendCStr_String(str, "sidebar.toggle\n"); | 173 | if (isVisible_Widget(sidebar)) { |
174 | appendCStr_String(str, "sidebar.toggle\n"); | ||
175 | } | ||
176 | appendFormat_String(str, "sidebar.mode arg:%d\n", mode_SidebarWidget(sidebar)); | ||
177 | if (isVisible_Widget(sidebar2)) { | ||
178 | appendCStr_String(str, "sidebar2.toggle\n"); | ||
179 | } | ||
180 | appendFormat_String(str, "sidebar2.mode arg:%d\n", mode_SidebarWidget(sidebar2)); | ||
165 | } | 181 | } |
166 | appendFormat_String(str, "sidebar.mode arg:%d\n", mode_SidebarWidget(sidebar)); | ||
167 | appendFormat_String(str, "uiscale arg:%f\n", uiScale_Window(d->window)); | 182 | appendFormat_String(str, "uiscale arg:%f\n", uiScale_Window(d->window)); |
168 | appendFormat_String(str, "prefs.dialogtab arg:%d\n", d->prefs.dialogTab); | 183 | appendFormat_String(str, "prefs.dialogtab arg:%d\n", d->prefs.dialogTab); |
169 | appendFormat_String(str, "font.set arg:%d\n", d->prefs.font); | 184 | appendFormat_String(str, "font.set arg:%d\n", d->prefs.font); |
@@ -312,6 +327,16 @@ static void saveState_App_(const iApp *d) { | |||
312 | iRelease(f); | 327 | iRelease(f); |
313 | } | 328 | } |
314 | 329 | ||
330 | #if defined (LAGRANGE_IDLE_SLEEP) | ||
331 | static uint32_t checkAsleep_App_(uint32_t interval, void *param) { | ||
332 | iApp *d = param; | ||
333 | SDL_Event ev = { .type = SDL_USEREVENT }; | ||
334 | ev.user.code = asleep_UserEventCode; | ||
335 | SDL_PushEvent(&ev); | ||
336 | return interval; | ||
337 | } | ||
338 | #endif | ||
339 | |||
315 | static void init_App_(iApp *d, int argc, char **argv) { | 340 | static void init_App_(iApp *d, int argc, char **argv) { |
316 | const iBool isFirstRun = !fileExistsCStr_FileInfo(cleanedPath_CStr(dataDir_App_)); | 341 | const iBool isFirstRun = !fileExistsCStr_FileInfo(cleanedPath_CStr(dataDir_App_)); |
317 | d->isFinishedLaunching = iFalse; | 342 | d->isFinishedLaunching = iFalse; |
@@ -341,7 +366,7 @@ static void init_App_(iApp *d, int argc, char **argv) { | |||
341 | #endif | 366 | #endif |
342 | init_Prefs(&d->prefs); | 367 | init_Prefs(&d->prefs); |
343 | setCStr_String(&d->prefs.downloadDir, downloadDir_App_); | 368 | setCStr_String(&d->prefs.downloadDir, downloadDir_App_); |
344 | d->running = iFalse; | 369 | d->isRunning = iFalse; |
345 | d->window = NULL; | 370 | d->window = NULL; |
346 | set_Atomic(&d->pendingRefresh, iFalse); | 371 | set_Atomic(&d->pendingRefresh, iFalse); |
347 | d->mimehooks = new_MimeHooks(); | 372 | d->mimehooks = new_MimeHooks(); |
@@ -350,6 +375,11 @@ static void init_App_(iApp *d, int argc, char **argv) { | |||
350 | d->bookmarks = new_Bookmarks(); | 375 | d->bookmarks = new_Bookmarks(); |
351 | d->tabEnum = 0; /* generates unique IDs for tab pages */ | 376 | d->tabEnum = 0; /* generates unique IDs for tab pages */ |
352 | setThemePalette_Color(d->prefs.theme); | 377 | setThemePalette_Color(d->prefs.theme); |
378 | #if defined (LAGRANGE_IDLE_SLEEP) | ||
379 | d->isIdling = iFalse; | ||
380 | d->lastEventTime = 0; | ||
381 | d->sleepTimer = SDL_AddTimer(1000, checkAsleep_App_, d); | ||
382 | #endif | ||
353 | #if defined (iPlatformApple) | 383 | #if defined (iPlatformApple) |
354 | setupApplication_MacOS(); | 384 | setupApplication_MacOS(); |
355 | #endif | 385 | #endif |
@@ -472,23 +502,31 @@ const iString *debugInfo_App(void) { | |||
472 | iConstForEach(StringList, j, d->launchCommands) { | 502 | iConstForEach(StringList, j, d->launchCommands) { |
473 | appendFormat_String(msg, "%s\n", cstr_String(j.value)); | 503 | appendFormat_String(msg, "%s\n", cstr_String(j.value)); |
474 | } | 504 | } |
505 | appendFormat_String(msg, "## MIME hooks\n"); | ||
506 | append_String(msg, debugInfo_MimeHooks(d->mimehooks)); | ||
475 | return msg; | 507 | return msg; |
476 | } | 508 | } |
477 | 509 | ||
478 | iLocalDef iBool isWaitingAllowed_App_(iApp *d) { | 510 | iLocalDef iBool isWaitingAllowed_App_(iApp *d) { |
511 | #if defined (LAGRANGE_IDLE_SLEEP) | ||
512 | if (d->isIdling) { | ||
513 | return iFalse; | ||
514 | } | ||
515 | #endif | ||
479 | return !value_Atomic(&d->pendingRefresh) && isEmpty_SortedArray(&d->tickers); | 516 | return !value_Atomic(&d->pendingRefresh) && isEmpty_SortedArray(&d->tickers); |
480 | } | 517 | } |
481 | 518 | ||
482 | void processEvents_App(enum iAppEventMode eventMode) { | 519 | void processEvents_App(enum iAppEventMode eventMode) { |
483 | iApp *d = &app_; | 520 | iApp *d = &app_; |
484 | SDL_Event ev; | 521 | SDL_Event ev; |
522 | iBool gotEvents = iFalse; | ||
485 | while ((isWaitingAllowed_App_(d) && eventMode == waitForNewEvents_AppEventMode && | 523 | while ((isWaitingAllowed_App_(d) && eventMode == waitForNewEvents_AppEventMode && |
486 | SDL_WaitEvent(&ev)) || | 524 | SDL_WaitEvent(&ev)) || |
487 | ((!isWaitingAllowed_App_(d) || eventMode == postedEventsOnly_AppEventMode) && | 525 | ((!isWaitingAllowed_App_(d) || eventMode == postedEventsOnly_AppEventMode) && |
488 | SDL_PollEvent(&ev))) { | 526 | SDL_PollEvent(&ev))) { |
489 | switch (ev.type) { | 527 | switch (ev.type) { |
490 | case SDL_QUIT: | 528 | case SDL_QUIT: |
491 | d->running = iFalse; | 529 | d->isRunning = iFalse; |
492 | goto backToMainLoop; | 530 | goto backToMainLoop; |
493 | case SDL_DROPFILE: { | 531 | case SDL_DROPFILE: { |
494 | iBool newTab = iFalse; | 532 | iBool newTab = iFalse; |
@@ -508,6 +546,25 @@ void processEvents_App(enum iAppEventMode eventMode) { | |||
508 | break; | 546 | break; |
509 | } | 547 | } |
510 | default: { | 548 | default: { |
549 | #if defined (LAGRANGE_IDLE_SLEEP) | ||
550 | if (ev.type == SDL_USEREVENT && ev.user.code == asleep_UserEventCode) { | ||
551 | if (SDL_GetTicks() - d->lastEventTime > idleThreshold_App_) { | ||
552 | if (!d->isIdling) { | ||
553 | // printf("[App] idling...\n"); | ||
554 | fflush(stdout); | ||
555 | } | ||
556 | d->isIdling = iTrue; | ||
557 | } | ||
558 | continue; | ||
559 | } | ||
560 | d->lastEventTime = SDL_GetTicks(); | ||
561 | if (d->isIdling) { | ||
562 | // printf("[App] ...woke up\n"); | ||
563 | fflush(stdout); | ||
564 | } | ||
565 | d->isIdling = iFalse; | ||
566 | #endif | ||
567 | gotEvents = iTrue; | ||
511 | iBool wasUsed = processEvent_Window(d->window, &ev); | 568 | iBool wasUsed = processEvent_Window(d->window, &ev); |
512 | if (!wasUsed) { | 569 | if (!wasUsed) { |
513 | /* There may be a key bindings for this. */ | 570 | /* There may be a key bindings for this. */ |
@@ -531,6 +588,14 @@ void processEvents_App(enum iAppEventMode eventMode) { | |||
531 | } | 588 | } |
532 | } | 589 | } |
533 | } | 590 | } |
591 | #if defined (LAGRANGE_IDLE_SLEEP) | ||
592 | if (d->isIdling && !gotEvents) { | ||
593 | /* This is where we spend most of our time when idle. 60 Hz still quite a lot but we | ||
594 | can't wait too long after the user tries to interact again with the app. In any | ||
595 | case, on macOS SDL_WaitEvent() seems to use 10x more CPU time than sleeping. */ | ||
596 | SDL_Delay(1000 / 60); | ||
597 | } | ||
598 | #endif | ||
534 | backToMainLoop:; | 599 | backToMainLoop:; |
535 | } | 600 | } |
536 | 601 | ||
@@ -578,10 +643,10 @@ static int resizeWatcher_(void *user, SDL_Event *event) { | |||
578 | 643 | ||
579 | static int run_App_(iApp *d) { | 644 | static int run_App_(iApp *d) { |
580 | arrange_Widget(findWidget_App("root")); | 645 | arrange_Widget(findWidget_App("root")); |
581 | d->running = iTrue; | 646 | d->isRunning = iTrue; |
582 | SDL_EventState(SDL_DROPFILE, SDL_ENABLE); /* open files via drag'n'drop */ | 647 | SDL_EventState(SDL_DROPFILE, SDL_ENABLE); /* open files via drag'n'drop */ |
583 | SDL_AddEventWatch(resizeWatcher_, d); | 648 | SDL_AddEventWatch(resizeWatcher_, d); |
584 | while (d->running) { | 649 | while (d->isRunning) { |
585 | processEvents_App(waitForNewEvents_AppEventMode); | 650 | processEvents_App(waitForNewEvents_AppEventMode); |
586 | runTickers_App_(d); | 651 | runTickers_App_(d); |
587 | refresh_App(); | 652 | refresh_App(); |
@@ -592,6 +657,9 @@ static int run_App_(iApp *d) { | |||
592 | 657 | ||
593 | void refresh_App(void) { | 658 | void refresh_App(void) { |
594 | iApp *d = &app_; | 659 | iApp *d = &app_; |
660 | #if defined (LAGRANGE_IDLE_SLEEP) | ||
661 | if (d->isIdling) return; | ||
662 | #endif | ||
595 | destroyPending_Widget(); | 663 | destroyPending_Widget(); |
596 | draw_Window(d->window); | 664 | draw_Window(d->window); |
597 | set_Atomic(&d->pendingRefresh, iFalse); | 665 | set_Atomic(&d->pendingRefresh, iFalse); |
@@ -649,6 +717,9 @@ int run_App(int argc, char **argv) { | |||
649 | 717 | ||
650 | void postRefresh_App(void) { | 718 | void postRefresh_App(void) { |
651 | iApp *d = &app_; | 719 | iApp *d = &app_; |
720 | #if defined (LAGRANGE_IDLE_SLEEP) | ||
721 | d->isIdling = iFalse; | ||
722 | #endif | ||
652 | const iBool wasPending = exchange_Atomic(&d->pendingRefresh, iTrue); | 723 | const iBool wasPending = exchange_Atomic(&d->pendingRefresh, iTrue); |
653 | if (!wasPending) { | 724 | if (!wasPending) { |
654 | SDL_Event ev; | 725 | SDL_Event ev; |
@@ -844,6 +915,7 @@ iDocumentWidget *newTab_App(const iDocumentWidget *duplicateOf, iBool switchToNe | |||
844 | } | 915 | } |
845 | arrange_Widget(tabs); | 916 | arrange_Widget(tabs); |
846 | refresh_Widget(tabs); | 917 | refresh_Widget(tabs); |
918 | postCommandf_App("tab.created id:%s", cstr_String(id_Widget(as_Widget(doc)))); | ||
847 | return doc; | 919 | return doc; |
848 | } | 920 | } |
849 | 921 | ||
@@ -964,7 +1036,13 @@ iBool willUseProxy_App(const iRangecc scheme) { | |||
964 | 1036 | ||
965 | iBool handleCommand_App(const char *cmd) { | 1037 | iBool handleCommand_App(const char *cmd) { |
966 | iApp *d = &app_; | 1038 | iApp *d = &app_; |
967 | if (equal_Command(cmd, "prefs.dialogtab")) { | 1039 | if (equal_Command(cmd, "config.error")) { |
1040 | makeMessage_Widget(uiTextCaution_ColorEscape "CONFIG ERROR", | ||
1041 | format_CStr("Error in config file: %s\nSee \"about:debug\" for details.", | ||
1042 | suffixPtr_Command(cmd, "where"))); | ||
1043 | return iTrue; | ||
1044 | } | ||
1045 | else if (equal_Command(cmd, "prefs.dialogtab")) { | ||
968 | d->prefs.dialogTab = arg_Command(cmd); | 1046 | d->prefs.dialogTab = arg_Command(cmd); |
969 | return iTrue; | 1047 | return iTrue; |
970 | } | 1048 | } |
@@ -1076,12 +1154,12 @@ iBool handleCommand_App(const char *cmd) { | |||
1076 | } | 1154 | } |
1077 | else if (equal_Command(cmd, "prefs.sideicon.changed")) { | 1155 | else if (equal_Command(cmd, "prefs.sideicon.changed")) { |
1078 | d->prefs.sideIcon = arg_Command(cmd) != 0; | 1156 | d->prefs.sideIcon = arg_Command(cmd) != 0; |
1079 | refresh_App(); | 1157 | postRefresh_App(); |
1080 | return iTrue; | 1158 | return iTrue; |
1081 | } | 1159 | } |
1082 | else if (equal_Command(cmd, "prefs.hoveroutline.changed")) { | 1160 | else if (equal_Command(cmd, "prefs.hoveroutline.changed")) { |
1083 | d->prefs.hoverOutline = arg_Command(cmd) != 0; | 1161 | d->prefs.hoverOutline = arg_Command(cmd) != 0; |
1084 | refresh_App(); | 1162 | postRefresh_App(); |
1085 | return iTrue; | 1163 | return iTrue; |
1086 | } | 1164 | } |
1087 | else if (equal_Command(cmd, "saturation.set")) { | 1165 | else if (equal_Command(cmd, "saturation.set")) { |
@@ -1364,13 +1442,13 @@ iBool handleCommand_App(const char *cmd) { | |||
1364 | } | 1442 | } |
1365 | else if (equal_Command(cmd, "feeds.update.started")) { | 1443 | else if (equal_Command(cmd, "feeds.update.started")) { |
1366 | setFlags_Widget(findWidget_App("feeds.progress"), hidden_WidgetFlag, iFalse); | 1444 | setFlags_Widget(findWidget_App("feeds.progress"), hidden_WidgetFlag, iFalse); |
1367 | refresh_App(); | 1445 | postRefresh_App(); |
1368 | return iFalse; | 1446 | return iFalse; |
1369 | } | 1447 | } |
1370 | else if (equal_Command(cmd, "feeds.update.finished")) { | 1448 | else if (equal_Command(cmd, "feeds.update.finished")) { |
1371 | setFlags_Widget(findWidget_App("feeds.progress"), hidden_WidgetFlag, iTrue); | 1449 | setFlags_Widget(findWidget_App("feeds.progress"), hidden_WidgetFlag, iTrue); |
1372 | refreshFinished_Feeds(); | 1450 | refreshFinished_Feeds(); |
1373 | refresh_App(); | 1451 | postRefresh_App(); |
1374 | return iFalse; | 1452 | return iFalse; |
1375 | } | 1453 | } |
1376 | else if (equal_Command(cmd, "visited.changed")) { | 1454 | else if (equal_Command(cmd, "visited.changed")) { |
@@ -46,6 +46,7 @@ enum iAppEventMode { | |||
46 | enum iUserEventCode { | 46 | enum iUserEventCode { |
47 | command_UserEventCode = 1, | 47 | command_UserEventCode = 1, |
48 | refresh_UserEventCode = 2, | 48 | refresh_UserEventCode = 2, |
49 | asleep_UserEventCode = 3, | ||
49 | }; | 50 | }; |
50 | 51 | ||
51 | const iString *execPath_App (void); | 52 | const iString *execPath_App (void); |
diff --git a/src/gmrequest.c b/src/gmrequest.c index a1284078..884486b3 100644 --- a/src/gmrequest.c +++ b/src/gmrequest.c | |||
@@ -262,8 +262,9 @@ static void requestFinished_GmRequest_(iGmRequest *d, iTlsRequest *req) { | |||
262 | checkServerCertificate_GmRequest_(d); | 262 | checkServerCertificate_GmRequest_(d); |
263 | unlock_Mutex(d->mtx); | 263 | unlock_Mutex(d->mtx); |
264 | /* Check for mimehooks. */ | 264 | /* Check for mimehooks. */ |
265 | if (d->isRespFiltered && d->state == finished_GmRequestState) { | 265 | if (d->isRespFiltered && d->state == finished_GmRequestState) { |
266 | iBlock *xbody = tryFilter_MimeHooks(mimeHooks_App(), &d->resp->meta, &d->resp->body); | 266 | iBlock *xbody = |
267 | tryFilter_MimeHooks(mimeHooks_App(), &d->resp->meta, &d->resp->body, &d->url); | ||
267 | if (xbody) { | 268 | if (xbody) { |
268 | lock_Mutex(d->mtx); | 269 | lock_Mutex(d->mtx); |
269 | clear_String(&d->resp->meta); | 270 | clear_String(&d->resp->meta); |
@@ -526,6 +527,12 @@ void submit_GmRequest(iGmRequest *d) { | |||
526 | } | 527 | } |
527 | else if (equalCase_Rangecc(url.scheme, "file")) { | 528 | else if (equalCase_Rangecc(url.scheme, "file")) { |
528 | iString *path = collect_String(urlDecode_String(collect_String(newRange_String(url.path)))); | 529 | iString *path = collect_String(urlDecode_String(collect_String(newRange_String(url.path)))); |
530 | #if defined (iPlatformMsys) | ||
531 | /* Remove the extra slash from the beginning. */ | ||
532 | if (startsWith_String(path, "/")) { | ||
533 | remove_Block(&path->chars, 0, 1); | ||
534 | } | ||
535 | #endif | ||
529 | iFile * f = new_File(path); | 536 | iFile * f = new_File(path); |
530 | if (open_File(f, readOnly_FileMode)) { | 537 | if (open_File(f, readOnly_FileMode)) { |
531 | /* TODO: Check supported file types: images, audio */ | 538 | /* TODO: Check supported file types: images, audio */ |
@@ -555,6 +562,9 @@ void submit_GmRequest(iGmRequest *d) { | |||
555 | else if (endsWithCase_String(path, ".mp3")) { | 562 | else if (endsWithCase_String(path, ".mp3")) { |
556 | setCStr_String(&resp->meta, "audio/mpeg"); | 563 | setCStr_String(&resp->meta, "audio/mpeg"); |
557 | } | 564 | } |
565 | else if (endsWithCase_String(path, ".mid")) { | ||
566 | setCStr_String(&resp->meta, "audio/midi"); | ||
567 | } | ||
558 | else { | 568 | else { |
559 | setCStr_String(&resp->meta, "application/octet-stream"); | 569 | setCStr_String(&resp->meta, "application/octet-stream"); |
560 | } | 570 | } |
diff --git a/src/gmutil.c b/src/gmutil.c index 557a82f8..94f00ce1 100644 --- a/src/gmutil.c +++ b/src/gmutil.c | |||
@@ -192,7 +192,10 @@ const iString *absoluteUrl_String(const iString *d, const iString *urlMaybeRelat | |||
192 | iString *makeFileUrl_String(const iString *localFilePath) { | 192 | iString *makeFileUrl_String(const iString *localFilePath) { |
193 | iString *url = cleaned_Path(localFilePath); | 193 | iString *url = cleaned_Path(localFilePath); |
194 | replace_Block(&url->chars, '\\', '/'); /* in case it's a Windows path */ | 194 | replace_Block(&url->chars, '\\', '/'); /* in case it's a Windows path */ |
195 | set_String(url, collect_String(urlEncodeExclude_String(url, "/"))); | 195 | set_String(url, collect_String(urlEncodeExclude_String(url, "/:"))); |
196 | #if defined (iPlatformMsys) | ||
197 | prependChar_String(url, '/'); /* three slashes */ | ||
198 | #endif | ||
196 | prependCStr_String(url, "file://"); | 199 | prependCStr_String(url, "file://"); |
197 | return url; | 200 | return url; |
198 | } | 201 | } |
diff --git a/src/mimehooks.c b/src/mimehooks.c index 8bb838ef..f4ec6bf4 100644 --- a/src/mimehooks.c +++ b/src/mimehooks.c | |||
@@ -1,6 +1,8 @@ | |||
1 | #include "mimehooks.h" | 1 | #include "mimehooks.h" |
2 | #include "app.h" | ||
2 | 3 | ||
3 | #include <the_Foundation/file.h> | 4 | #include <the_Foundation/file.h> |
5 | #include <the_Foundation/fileinfo.h> | ||
4 | #include <the_Foundation/path.h> | 6 | #include <the_Foundation/path.h> |
5 | #include <the_Foundation/process.h> | 7 | #include <the_Foundation/process.h> |
6 | #include <the_Foundation/stringlist.h> | 8 | #include <the_Foundation/stringlist.h> |
@@ -23,6 +25,7 @@ void deinit_FilterHook(iFilterHook *d) { | |||
23 | 25 | ||
24 | void setMimePattern_FilterHook(iFilterHook *d, const iString *pattern) { | 26 | void setMimePattern_FilterHook(iFilterHook *d, const iString *pattern) { |
25 | iReleasePtr(&d->mimeRegex); | 27 | iReleasePtr(&d->mimeRegex); |
28 | set_String(&d->mimePattern, pattern); | ||
26 | d->mimeRegex = new_RegExp(cstr_String(pattern), caseInsensitive_RegExpOption); | 29 | d->mimeRegex = new_RegExp(cstr_String(pattern), caseInsensitive_RegExpOption); |
27 | } | 30 | } |
28 | 31 | ||
@@ -30,7 +33,8 @@ void setCommand_FilterHook(iFilterHook *d, const iString *command) { | |||
30 | set_String(&d->command, command); | 33 | set_String(&d->command, command); |
31 | } | 34 | } |
32 | 35 | ||
33 | iBlock *run_FilterHook_(const iFilterHook *d, const iString *mime, const iBlock *body) { | 36 | iBlock *run_FilterHook_(const iFilterHook *d, const iString *mime, const iBlock *body, |
37 | const iString *requestUrl) { | ||
34 | iProcess * proc = new_Process(); | 38 | iProcess * proc = new_Process(); |
35 | iStringList *args = new_StringList(); | 39 | iStringList *args = new_StringList(); |
36 | iRangecc seg = iNullRange; | 40 | iRangecc seg = iNullRange; |
@@ -43,13 +47,21 @@ iBlock *run_FilterHook_(const iFilterHook *d, const iString *mime, const iBlock | |||
43 | } | 47 | } |
44 | setArguments_Process(proc, args); | 48 | setArguments_Process(proc, args); |
45 | iRelease(args); | 49 | iRelease(args); |
46 | start_Process(proc); | 50 | if (!isEmpty_String(requestUrl)) { |
47 | writeInput_Process(proc, body); | 51 | setEnvironment_Process( |
48 | iBlock *output = readOutputUntilClosed_Process(proc); | 52 | proc, |
49 | if (!startsWith_Rangecc(range_Block(output), "20")) { | 53 | iClob(newStrings_StringList( |
50 | /* Didn't produce valid output. */ | 54 | collectNewFormat_String("REQUEST_URL=%s", cstr_String(requestUrl)), NULL))); |
51 | delete_Block(output); | 55 | } |
52 | output = NULL; | 56 | iBlock *output = NULL; |
57 | if (start_Process(proc)) { | ||
58 | writeInput_Process(proc, body); | ||
59 | output = readOutputUntilClosed_Process(proc); | ||
60 | if (!startsWith_Rangecc(range_Block(output), "20")) { | ||
61 | /* Didn't produce valid output. */ | ||
62 | delete_Block(output); | ||
63 | output = NULL; | ||
64 | } | ||
53 | } | 65 | } |
54 | iRelease(proc); | 66 | iRelease(proc); |
55 | return output; | 67 | return output; |
@@ -87,13 +99,14 @@ iBool willTryFilter_MimeHooks(const iMimeHooks *d, const iString *mime) { | |||
87 | return iFalse; | 99 | return iFalse; |
88 | } | 100 | } |
89 | 101 | ||
90 | iBlock *tryFilter_MimeHooks(const iMimeHooks *d, const iString *mime, const iBlock *body) { | 102 | iBlock *tryFilter_MimeHooks(const iMimeHooks *d, const iString *mime, const iBlock *body, |
103 | const iString *requestUrl) { | ||
91 | iRegExpMatch m; | 104 | iRegExpMatch m; |
92 | iConstForEach(PtrArray, i, &d->filters) { | 105 | iConstForEach(PtrArray, i, &d->filters) { |
93 | const iFilterHook *xc = i.ptr; | 106 | const iFilterHook *xc = i.ptr; |
94 | init_RegExpMatch(&m); | 107 | init_RegExpMatch(&m); |
95 | if (matchString_RegExp(xc->mimeRegex, mime, &m)) { | 108 | if (matchString_RegExp(xc->mimeRegex, mime, &m)) { |
96 | iBlock *result = run_FilterHook_(xc, mime, body); | 109 | iBlock *result = run_FilterHook_(xc, mime, body, requestUrl); |
97 | if (result) { | 110 | if (result) { |
98 | return result; | 111 | return result; |
99 | } | 112 | } |
@@ -105,6 +118,7 @@ iBlock *tryFilter_MimeHooks(const iMimeHooks *d, const iString *mime, const iBlo | |||
105 | static const char *mimeHooksFilename_MimeHooks_ = "mimehooks.txt"; | 118 | static const char *mimeHooksFilename_MimeHooks_ = "mimehooks.txt"; |
106 | 119 | ||
107 | void load_MimeHooks(iMimeHooks *d, const char *saveDir) { | 120 | void load_MimeHooks(iMimeHooks *d, const char *saveDir) { |
121 | iBool reportError = iFalse; | ||
108 | iFile *f = newCStr_File(concatPath_CStr(saveDir, mimeHooksFilename_MimeHooks_)); | 122 | iFile *f = newCStr_File(concatPath_CStr(saveDir, mimeHooksFilename_MimeHooks_)); |
109 | if (open_File(f, read_FileMode | text_FileMode)) { | 123 | if (open_File(f, read_FileMode | text_FileMode)) { |
110 | iBlock * src = readAll_File(f); | 124 | iBlock * src = readAll_File(f); |
@@ -124,6 +138,15 @@ void load_MimeHooks(iMimeHooks *d, const char *saveDir) { | |||
124 | setRange_String(&hook->label, lines[0]); | 138 | setRange_String(&hook->label, lines[0]); |
125 | setMimePattern_FilterHook(hook, collect_String(newRange_String(lines[1]))); | 139 | setMimePattern_FilterHook(hook, collect_String(newRange_String(lines[1]))); |
126 | setCommand_FilterHook(hook, collect_String(newRange_String(lines[2]))); | 140 | setCommand_FilterHook(hook, collect_String(newRange_String(lines[2]))); |
141 | /* Check if commmand is valid. */ { | ||
142 | iRangecc seg = iNullRange; | ||
143 | while (nextSplit_Rangecc(range_String(&hook->command), ";", &seg)) { | ||
144 | if (!fileExistsCStr_FileInfo(cstr_Rangecc(seg))) { | ||
145 | reportError = iTrue; | ||
146 | } | ||
147 | break; | ||
148 | } | ||
149 | } | ||
127 | pushBack_PtrArray(&d->filters, hook); | 150 | pushBack_PtrArray(&d->filters, hook); |
128 | pos = 0; | 151 | pos = 0; |
129 | } | 152 | } |
@@ -131,9 +154,37 @@ void load_MimeHooks(iMimeHooks *d, const char *saveDir) { | |||
131 | delete_Block(src); | 154 | delete_Block(src); |
132 | } | 155 | } |
133 | iRelease(f); | 156 | iRelease(f); |
157 | if (reportError) { | ||
158 | postCommand_App("~config.error where:mimehooks.txt"); | ||
159 | } | ||
134 | } | 160 | } |
135 | 161 | ||
136 | void save_MimeHooks(const iMimeHooks *d) { | 162 | void save_MimeHooks(const iMimeHooks *d) { |
137 | iUnused(d); | 163 | iUnused(d); |
138 | } | 164 | } |
139 | 165 | ||
166 | const iString *debugInfo_MimeHooks(const iMimeHooks *d) { | ||
167 | iString *str = collectNew_String(); | ||
168 | size_t index = 0; | ||
169 | iConstForEach(PtrArray, i, &d->filters) { | ||
170 | const iFilterHook *filter = i.ptr; | ||
171 | appendFormat_String(str, "### %d: %s\n", index, cstr_String(&filter->label)); | ||
172 | appendFormat_String(str, "MIME regex:\n```\n%s\n```\n", cstr_String(&filter->mimePattern)); | ||
173 | iStringList *args = iClob(split_String(&filter->command, ";")); | ||
174 | if (isEmpty_StringList(args)) { | ||
175 | appendFormat_String(str, "\u26a0 Command not specified!\n"); | ||
176 | continue; | ||
177 | } | ||
178 | const iString *exec = constAt_StringList(args, 0); | ||
179 | if (isEmpty_String(exec)) { | ||
180 | appendFormat_String(str, "\u26a0 Command not specified!\n"); | ||
181 | } | ||
182 | else { | ||
183 | appendFormat_String(str, "Executable: %s\n```\n%s\n```\n", | ||
184 | fileExists_FileInfo(exec) ? "" : "\u26a0 FILE NOT FOUND", | ||
185 | cstr_String(exec)); | ||
186 | } | ||
187 | index++; | ||
188 | } | ||
189 | return str; | ||
190 | } | ||
diff --git a/src/mimehooks.h b/src/mimehooks.h index c78a3c86..6da14fdf 100644 --- a/src/mimehooks.h +++ b/src/mimehooks.h | |||
@@ -25,7 +25,9 @@ iDeclareTypeConstruction(MimeHooks) | |||
25 | 25 | ||
26 | iBool willTryFilter_MimeHooks (const iMimeHooks *, const iString *mime); | 26 | iBool willTryFilter_MimeHooks (const iMimeHooks *, const iString *mime); |
27 | iBlock * tryFilter_MimeHooks (const iMimeHooks *, const iString *mime, | 27 | iBlock * tryFilter_MimeHooks (const iMimeHooks *, const iString *mime, |
28 | const iBlock *body); | 28 | const iBlock *body, const iString *requestUrl); |
29 | 29 | ||
30 | void load_MimeHooks (iMimeHooks *, const char *saveDir); | 30 | void load_MimeHooks (iMimeHooks *, const char *saveDir); |
31 | void save_MimeHooks (const iMimeHooks *); | 31 | void save_MimeHooks (const iMimeHooks *); |
32 | |||
33 | const iString *debugInfo_MimeHooks (const iMimeHooks *); | ||
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index a6cc8187..7a297aa7 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c | |||
@@ -302,7 +302,7 @@ static int documentWidth_DocumentWidget_(const iDocumentWidget *d) { | |||
302 | const iWidget *w = constAs_Widget(d); | 302 | const iWidget *w = constAs_Widget(d); |
303 | const iRect bounds = bounds_Widget(w); | 303 | const iRect bounds = bounds_Widget(w); |
304 | const iPrefs * prefs = prefs_App(); | 304 | const iPrefs * prefs = prefs_App(); |
305 | return iMini(bounds.size.x - gap_UI * d->pageMargin * 2, | 305 | return iMini(iMax(50 * gap_UI, bounds.size.x - gap_UI * d->pageMargin * 2), |
306 | fontSize_UI * prefs->lineWidth * prefs->zoomPercent / 100); | 306 | fontSize_UI * prefs->lineWidth * prefs->zoomPercent / 100); |
307 | } | 307 | } |
308 | 308 | ||
@@ -859,12 +859,13 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse | |||
859 | while (nextSplit_Rangecc(mime, ";", &seg)) { | 859 | while (nextSplit_Rangecc(mime, ";", &seg)) { |
860 | iRangecc param = seg; | 860 | iRangecc param = seg; |
861 | trim_Rangecc(¶m); | 861 | trim_Rangecc(¶m); |
862 | if (equal_Rangecc(param, "text/plain")) { | 862 | if (equal_Rangecc(param, "text/gemini")) { |
863 | docFormat = plainText_GmDocumentFormat; | 863 | docFormat = gemini_GmDocumentFormat; |
864 | setRange_String(&d->sourceMime, param); | 864 | setRange_String(&d->sourceMime, param); |
865 | } | 865 | } |
866 | else if (equal_Rangecc(param, "text/gemini")) { | 866 | else if (startsWith_Rangecc(param, "text/") || |
867 | docFormat = gemini_GmDocumentFormat; | 867 | equal_Rangecc(param, "application/json")) { |
868 | docFormat = plainText_GmDocumentFormat; | ||
868 | setRange_String(&d->sourceMime, param); | 869 | setRange_String(&d->sourceMime, param); |
869 | } | 870 | } |
870 | else if (startsWith_Rangecc(param, "image/") || | 871 | else if (startsWith_Rangecc(param, "image/") || |
@@ -1528,6 +1529,11 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
1528 | animatePlayers_DocumentWidget_(d); | 1529 | animatePlayers_DocumentWidget_(d); |
1529 | return iFalse; | 1530 | return iFalse; |
1530 | } | 1531 | } |
1532 | else if (equal_Command(cmd, "tab.created")) { | ||
1533 | /* Space for tab buttons has changed. */ | ||
1534 | updateWindowTitle_DocumentWidget_(d); | ||
1535 | return iFalse; | ||
1536 | } | ||
1531 | else if (equal_Command(cmd, "server.showcert") && d == document_App()) { | 1537 | else if (equal_Command(cmd, "server.showcert") && d == document_App()) { |
1532 | const char *unchecked = red_ColorEscape "\u2610"; | 1538 | const char *unchecked = red_ColorEscape "\u2610"; |
1533 | const char *checked = green_ColorEscape "\u2611"; | 1539 | const char *checked = green_ColorEscape "\u2611"; |
@@ -2227,6 +2233,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
2227 | if (d->menu) { | 2233 | if (d->menu) { |
2228 | destroy_Widget(d->menu); | 2234 | destroy_Widget(d->menu); |
2229 | } | 2235 | } |
2236 | setFocus_Widget(NULL); | ||
2230 | iArray items; | 2237 | iArray items; |
2231 | init_Array(&items, sizeof(iMenuItem)); | 2238 | init_Array(&items, sizeof(iMenuItem)); |
2232 | if (d->contextLink) { | 2239 | if (d->contextLink) { |
@@ -2909,6 +2916,7 @@ static void drawSideElements_DocumentWidget_(const iDocumentWidget *d) { | |||
2909 | iMax(0, scrollMax_DocumentWidget_(d) - value_Anim(&d->scrollY)))), | 2916 | iMax(0, scrollMax_DocumentWidget_(d) - value_Anim(&d->scrollY)))), |
2910 | tmQuoteIcon_ColorId); | 2917 | tmQuoteIcon_ColorId); |
2911 | } | 2918 | } |
2919 | #if 0 | ||
2912 | /* Outline on the right side. */ | 2920 | /* Outline on the right side. */ |
2913 | const float outlineOpacity = value_Anim(&d->outlineOpacity); | 2921 | const float outlineOpacity = value_Anim(&d->outlineOpacity); |
2914 | if (prefs_App()->hoverOutline && !isEmpty_Array(&d->outline) && outlineOpacity > 0.0f) { | 2922 | if (prefs_App()->hoverOutline && !isEmpty_Array(&d->outline) && outlineOpacity > 0.0f) { |
@@ -2964,8 +2972,9 @@ static void drawSideElements_DocumentWidget_(const iDocumentWidget *d) { | |||
2964 | setOpacity_Text(1.0f); | 2972 | setOpacity_Text(1.0f); |
2965 | SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE); | 2973 | SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE); |
2966 | } | 2974 | } |
2975 | #endif | ||
2967 | unsetClip_Paint(&p); | 2976 | unsetClip_Paint(&p); |
2968 | } | 2977 | } |
2969 | 2978 | ||
2970 | static void drawPlayers_DocumentWidget_(const iDocumentWidget *d, iPaint *p) { | 2979 | static void drawPlayers_DocumentWidget_(const iDocumentWidget *d, iPaint *p) { |
2971 | iConstForEach(PtrArray, i, &d->visiblePlayers) { | 2980 | iConstForEach(PtrArray, i, &d->visiblePlayers) { |
@@ -2982,6 +2991,9 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) { | |||
2982 | const iWidget *w = constAs_Widget(d); | 2991 | const iWidget *w = constAs_Widget(d); |
2983 | const iRect bounds = bounds_Widget(w); | 2992 | const iRect bounds = bounds_Widget(w); |
2984 | iVisBuf * visBuf = d->visBuf; /* will be updated now */ | 2993 | iVisBuf * visBuf = d->visBuf; /* will be updated now */ |
2994 | if (width_Rect(bounds) <= 0) { | ||
2995 | return; | ||
2996 | } | ||
2985 | draw_Widget(w); | 2997 | draw_Widget(w); |
2986 | allocVisBuffer_DocumentWidget_(d); | 2998 | allocVisBuffer_DocumentWidget_(d); |
2987 | const iRect ctxWidgetBounds = init_Rect( | 2999 | const iRect ctxWidgetBounds = init_Rect( |
diff --git a/src/ui/keys.h b/src/ui/keys.h index cc56f8d1..8bcd4f53 100644 --- a/src/ui/keys.h +++ b/src/ui/keys.h | |||
@@ -36,6 +36,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
36 | # define navigateRoot_KeyShortcut SDLK_UP, KMOD_SHIFT | KMOD_PRIMARY | 36 | # define navigateRoot_KeyShortcut SDLK_UP, KMOD_SHIFT | KMOD_PRIMARY |
37 | # define byWord_KeyModifier KMOD_ALT | 37 | # define byWord_KeyModifier KMOD_ALT |
38 | # define byLine_KeyModifier KMOD_PRIMARY | 38 | # define byLine_KeyModifier KMOD_PRIMARY |
39 | # define rightSidebar_KeyModifier KMOD_CTRL | ||
39 | # define subscribeToPage_KeyModifier SDLK_d, KMOD_SHIFT | KMOD_PRIMARY | 40 | # define subscribeToPage_KeyModifier SDLK_d, KMOD_SHIFT | KMOD_PRIMARY |
40 | #else | 41 | #else |
41 | # define reload_KeyShortcut SDLK_r, KMOD_PRIMARY | 42 | # define reload_KeyShortcut SDLK_r, KMOD_PRIMARY |
@@ -47,6 +48,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
47 | # define navigateRoot_KeyShortcut SDLK_UP, KMOD_SHIFT | KMOD_ALT | 48 | # define navigateRoot_KeyShortcut SDLK_UP, KMOD_SHIFT | KMOD_ALT |
48 | # define byWord_KeyModifier KMOD_CTRL | 49 | # define byWord_KeyModifier KMOD_CTRL |
49 | # define byLine_KeyModifier 0 | 50 | # define byLine_KeyModifier 0 |
51 | # define rightSidebar_KeyModifier KMOD_SHIFT | KMOD_CTRL | ||
50 | # define subscribeToPage_KeyModifier SDLK_d, KMOD_SHIFT | KMOD_PRIMARY | 52 | # define subscribeToPage_KeyModifier SDLK_d, KMOD_SHIFT | KMOD_PRIMARY |
51 | #endif | 53 | #endif |
52 | 54 | ||
diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index 5baa08f7..e6144744 100644 --- a/src/ui/sidebarwidget.c +++ b/src/ui/sidebarwidget.c | |||
@@ -84,7 +84,9 @@ iDefineObjectConstruction(SidebarItem) | |||
84 | 84 | ||
85 | struct Impl_SidebarWidget { | 85 | struct Impl_SidebarWidget { |
86 | iWidget widget; | 86 | iWidget widget; |
87 | enum iSidebarSide side; | ||
87 | enum iSidebarMode mode; | 88 | enum iSidebarMode mode; |
89 | iString cmdPrefix; | ||
88 | iWidget * blank; | 90 | iWidget * blank; |
89 | iListWidget * list; | 91 | iListWidget * list; |
90 | int modeScroll[max_SidebarMode]; | 92 | int modeScroll[max_SidebarMode]; |
@@ -92,12 +94,11 @@ struct Impl_SidebarWidget { | |||
92 | int maxButtonLabelWidth; | 94 | int maxButtonLabelWidth; |
93 | int width; | 95 | int width; |
94 | iWidget * resizer; | 96 | iWidget * resizer; |
95 | SDL_Cursor * resizeCursor; | ||
96 | iWidget * menu; | 97 | iWidget * menu; |
97 | iSidebarItem * contextItem; /* list item accessed in the context menu */ | 98 | iSidebarItem * contextItem; /* list item accessed in the context menu */ |
98 | }; | 99 | }; |
99 | 100 | ||
100 | iDefineObjectConstruction(SidebarWidget) | 101 | iDefineObjectConstructionArgs(SidebarWidget, (enum iSidebarSide side), side) |
101 | 102 | ||
102 | static iBool isResizing_SidebarWidget_(const iSidebarWidget *d) { | 103 | static iBool isResizing_SidebarWidget_(const iSidebarWidget *d) { |
103 | return (flags_Widget(d->resizer) & pressed_WidgetFlag) != 0; | 104 | return (flags_Widget(d->resizer) & pressed_WidgetFlag) != 0; |
@@ -408,16 +409,19 @@ static const char *tightModeLabels_[max_SidebarMode] = { | |||
408 | "\U0001f5b9", | 409 | "\U0001f5b9", |
409 | }; | 410 | }; |
410 | 411 | ||
411 | void init_SidebarWidget(iSidebarWidget *d) { | 412 | void init_SidebarWidget(iSidebarWidget *d, enum iSidebarSide side) { |
412 | iWidget *w = as_Widget(d); | 413 | iWidget *w = as_Widget(d); |
413 | init_Widget(w); | 414 | init_Widget(w); |
414 | setId_Widget(w, "sidebar"); | 415 | setId_Widget(w, side == left_SideBarSide ? "sidebar" : "sidebar2"); |
416 | initCopy_String(&d->cmdPrefix, id_Widget(w)); | ||
417 | appendChar_String(&d->cmdPrefix, '.'); | ||
415 | setBackgroundColor_Widget(w, none_ColorId); | 418 | setBackgroundColor_Widget(w, none_ColorId); |
416 | setFlags_Widget(w, | 419 | setFlags_Widget(w, |
417 | collapse_WidgetFlag | hidden_WidgetFlag | arrangeHorizontal_WidgetFlag | | 420 | collapse_WidgetFlag | hidden_WidgetFlag | arrangeHorizontal_WidgetFlag | |
418 | resizeWidthOfChildren_WidgetFlag, | 421 | resizeWidthOfChildren_WidgetFlag, |
419 | iTrue); | 422 | iTrue); |
420 | iZap(d->modeScroll); | 423 | iZap(d->modeScroll); |
424 | d->side = side; | ||
421 | d->mode = -1; | 425 | d->mode = -1; |
422 | d->width = 60 * gap_UI; | 426 | d->width = 60 * gap_UI; |
423 | setFlags_Widget(w, fixedWidth_WidgetFlag, iTrue); | 427 | setFlags_Widget(w, fixedWidth_WidgetFlag, iTrue); |
@@ -428,8 +432,9 @@ void init_SidebarWidget(iSidebarWidget *d) { | |||
428 | for (int i = 0; i < max_SidebarMode; i++) { | 432 | for (int i = 0; i < max_SidebarMode; i++) { |
429 | d->modeButtons[i] = addChildFlags_Widget( | 433 | d->modeButtons[i] = addChildFlags_Widget( |
430 | buttons, | 434 | buttons, |
431 | iClob( | 435 | iClob(new_LabelWidget( |
432 | new_LabelWidget(tightModeLabels_[i], format_CStr("sidebar.mode arg:%d", i))), | 436 | tightModeLabels_[i], |
437 | format_CStr("%s.mode arg:%d", cstr_String(id_Widget(w)), i))), | ||
433 | frameless_WidgetFlag); | 438 | frameless_WidgetFlag); |
434 | d->maxButtonLabelWidth = | 439 | d->maxButtonLabelWidth = |
435 | iMaxi(d->maxButtonLabelWidth, | 440 | iMaxi(d->maxButtonLabelWidth, |
@@ -448,21 +453,22 @@ void init_SidebarWidget(iSidebarWidget *d) { | |||
448 | addChildFlags_Widget(content, iClob(d->blank), resizeChildren_WidgetFlag); | 453 | addChildFlags_Widget(content, iClob(d->blank), resizeChildren_WidgetFlag); |
449 | addChildFlags_Widget(vdiv, iClob(content), expand_WidgetFlag); | 454 | addChildFlags_Widget(vdiv, iClob(content), expand_WidgetFlag); |
450 | setMode_SidebarWidget(d, bookmarks_SidebarMode); | 455 | setMode_SidebarWidget(d, bookmarks_SidebarMode); |
451 | d->resizer = addChildFlags_Widget( | 456 | d->resizer = |
452 | w, | 457 | addChildFlags_Widget(w, |
453 | iClob(new_Widget()), | 458 | iClob(new_Widget()), |
454 | hover_WidgetFlag | commandOnClick_WidgetFlag | fixedWidth_WidgetFlag | | 459 | hover_WidgetFlag | commandOnClick_WidgetFlag | fixedWidth_WidgetFlag | |
455 | resizeToParentHeight_WidgetFlag | moveToParentRightEdge_WidgetFlag); | 460 | resizeToParentHeight_WidgetFlag | |
456 | setId_Widget(d->resizer, "sidebar.grab"); | 461 | (side == left_SideBarSide ? moveToParentRightEdge_WidgetFlag |
462 | : moveToParentLeftEdge_WidgetFlag)); | ||
463 | setId_Widget(d->resizer, side == left_SideBarSide ? "sidebar.grab" : "sidebar2.grab"); | ||
457 | d->resizer->rect.size.x = gap_UI; | 464 | d->resizer->rect.size.x = gap_UI; |
458 | setBackgroundColor_Widget(d->resizer, none_ColorId); | 465 | setBackgroundColor_Widget(d->resizer, none_ColorId); |
459 | d->resizeCursor = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_SIZEWE); | ||
460 | d->menu = NULL; | 466 | d->menu = NULL; |
461 | addAction_Widget(w, SDLK_r, KMOD_PRIMARY | KMOD_SHIFT, "feeds.refresh"); | 467 | addAction_Widget(w, SDLK_r, KMOD_PRIMARY | KMOD_SHIFT, "feeds.refresh"); |
462 | } | 468 | } |
463 | 469 | ||
464 | void deinit_SidebarWidget(iSidebarWidget *d) { | 470 | void deinit_SidebarWidget(iSidebarWidget *d) { |
465 | SDL_FreeCursor(d->resizeCursor); | 471 | deinit_String(&d->cmdPrefix); |
466 | } | 472 | } |
467 | 473 | ||
468 | static const iGmIdentity *constHoverIdentity_SidebarWidget_(const iSidebarWidget *d) { | 474 | static const iGmIdentity *constHoverIdentity_SidebarWidget_(const iSidebarWidget *d) { |
@@ -558,8 +564,11 @@ static void checkModeButtonLayout_SidebarWidget_(iSidebarWidget *d) { | |||
558 | } | 564 | } |
559 | 565 | ||
560 | void setWidth_SidebarWidget(iSidebarWidget *d, int width) { | 566 | void setWidth_SidebarWidget(iSidebarWidget *d, int width) { |
561 | iWidget *w = as_Widget(d); | 567 | iWidget * w = as_Widget(d); |
562 | width = iClamp(width, 30 * gap_UI, rootSize_Window(get_Window()).x - 50 * gap_UI); | 568 | /* Even less space if the other sidebar is visible, too. */ |
569 | const int otherWidth = | ||
570 | width_Widget(findWidget_App(d->side == left_SideBarSide ? "sidebar2" : "sidebar")); | ||
571 | width = iClamp(width, 30 * gap_UI, rootSize_Window(get_Window()).x - 50 * gap_UI - otherWidth); | ||
563 | d->width = width; | 572 | d->width = width; |
564 | if (isVisible_Widget(w)) { | 573 | if (isVisible_Widget(w)) { |
565 | w->rect.size.x = width; | 574 | w->rect.size.x = width; |
@@ -574,7 +583,8 @@ void setWidth_SidebarWidget(iSidebarWidget *d, int width) { | |||
574 | 583 | ||
575 | iBool handleBookmarkEditorCommands_SidebarWidget_(iWidget *editor, const char *cmd) { | 584 | iBool handleBookmarkEditorCommands_SidebarWidget_(iWidget *editor, const char *cmd) { |
576 | if (equal_Command(cmd, "bmed.accept") || equal_Command(cmd, "cancel")) { | 585 | if (equal_Command(cmd, "bmed.accept") || equal_Command(cmd, "cancel")) { |
577 | iSidebarWidget *d = findWidget_App("sidebar"); | 586 | iAssert(startsWith_String(id_Widget(editor), "bmed.")); |
587 | iSidebarWidget *d = findWidget_App(cstr_String(id_Widget(editor)) + 5); /* bmed.sidebar */ | ||
578 | if (equal_Command(cmd, "bmed.accept")) { | 588 | if (equal_Command(cmd, "bmed.accept")) { |
579 | const iString *title = text_InputWidget(findChild_Widget(editor, "bmed.title")); | 589 | const iString *title = text_InputWidget(findChild_Widget(editor, "bmed.title")); |
580 | const iString *url = text_InputWidget(findChild_Widget(editor, "bmed.url")); | 590 | const iString *url = text_InputWidget(findChild_Widget(editor, "bmed.url")); |
@@ -594,6 +604,43 @@ iBool handleBookmarkEditorCommands_SidebarWidget_(iWidget *editor, const char *c | |||
594 | return iFalse; | 604 | return iFalse; |
595 | } | 605 | } |
596 | 606 | ||
607 | static iBool handleSidebarCommand_SidebarWidget_(iSidebarWidget *d, const char *cmd) { | ||
608 | iWidget *w = as_Widget(d); | ||
609 | if (equal_Command(cmd, "width")) { | ||
610 | setWidth_SidebarWidget(d, arg_Command(cmd)); | ||
611 | return iTrue; | ||
612 | } | ||
613 | else if (equal_Command(cmd, "mode")) { | ||
614 | const iBool wasChanged = setMode_SidebarWidget(d, arg_Command(cmd)); | ||
615 | updateItems_SidebarWidget_(d); | ||
616 | if ((argLabel_Command(cmd, "show") && !isVisible_Widget(w)) || | ||
617 | (argLabel_Command(cmd, "toggle") && (!isVisible_Widget(w) || !wasChanged))) { | ||
618 | postCommandf_App("%s.toggle", cstr_String(id_Widget(w))); | ||
619 | } | ||
620 | scrollOffset_ListWidget(d->list, 0); | ||
621 | return iTrue; | ||
622 | } | ||
623 | else if (equal_Command(cmd, "toggle")) { | ||
624 | if (arg_Command(cmd) && isVisible_Widget(w)) { | ||
625 | return iTrue; | ||
626 | } | ||
627 | setFlags_Widget(w, hidden_WidgetFlag, isVisible_Widget(w)); | ||
628 | if (isVisible_Widget(w)) { | ||
629 | w->rect.size.x = d->width; | ||
630 | invalidate_ListWidget(d->list); | ||
631 | } | ||
632 | arrange_Widget(w->parent); | ||
633 | updateSize_DocumentWidget(document_App()); | ||
634 | if (isVisible_Widget(w)) { | ||
635 | updateItems_SidebarWidget_(d); | ||
636 | scrollOffset_ListWidget(d->list, 0); | ||
637 | } | ||
638 | refresh_Widget(w->parent); | ||
639 | return iTrue; | ||
640 | } | ||
641 | return iFalse; | ||
642 | } | ||
643 | |||
597 | static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) { | 644 | static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) { |
598 | iWidget *w = as_Widget(d); | 645 | iWidget *w = as_Widget(d); |
599 | /* Handle commands. */ | 646 | /* Handle commands. */ |
@@ -602,7 +649,27 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
602 | } | 649 | } |
603 | else if (ev->type == SDL_USEREVENT && ev->user.code == command_UserEventCode) { | 650 | else if (ev->type == SDL_USEREVENT && ev->user.code == command_UserEventCode) { |
604 | const char *cmd = command_UserEvent(ev); | 651 | const char *cmd = command_UserEvent(ev); |
605 | if (isCommand_Widget(w, ev, "mouse.clicked")) { | 652 | if (equal_Command(cmd, "tabs.changed") || equal_Command(cmd, "document.changed")) { |
653 | updateItems_SidebarWidget_(d); | ||
654 | scrollOffset_ListWidget(d->list, 0); | ||
655 | } | ||
656 | else if (equal_Command(cmd, "visited.changed") && | ||
657 | (d->mode == history_SidebarMode || d->mode == feeds_SidebarMode)) { | ||
658 | updateItems_SidebarWidget_(d); | ||
659 | } | ||
660 | else if (equal_Command(cmd, "bookmarks.changed") && (d->mode == bookmarks_SidebarMode || | ||
661 | d->mode == feeds_SidebarMode)) { | ||
662 | updateItems_SidebarWidget_(d); | ||
663 | } | ||
664 | else if (equal_Command(cmd, "idents.changed") && d->mode == identities_SidebarMode) { | ||
665 | updateItems_SidebarWidget_(d); | ||
666 | } | ||
667 | else if (startsWith_CStr(cmd, cstr_String(&d->cmdPrefix))) { | ||
668 | if (handleSidebarCommand_SidebarWidget_(d, cmd + size_String(&d->cmdPrefix))) { | ||
669 | return iTrue; | ||
670 | } | ||
671 | } | ||
672 | else if (isCommand_Widget(w, ev, "mouse.clicked")) { | ||
606 | if (argLabel_Command(cmd, "button") == SDL_BUTTON_LEFT) { | 673 | if (argLabel_Command(cmd, "button") == SDL_BUTTON_LEFT) { |
607 | if (arg_Command(cmd)) { | 674 | if (arg_Command(cmd)) { |
608 | setFlags_Widget(d->resizer, pressed_WidgetFlag, iTrue); | 675 | setFlags_Widget(d->resizer, pressed_WidgetFlag, iTrue); |
@@ -624,7 +691,13 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
624 | else if (isCommand_Widget(w, ev, "mouse.moved")) { | 691 | else if (isCommand_Widget(w, ev, "mouse.moved")) { |
625 | if (isResizing_SidebarWidget_(d)) { | 692 | if (isResizing_SidebarWidget_(d)) { |
626 | const iInt2 local = localCoord_Widget(w, coord_Command(cmd)); | 693 | const iInt2 local = localCoord_Widget(w, coord_Command(cmd)); |
627 | setWidth_SidebarWidget(d, local.x + d->resizer->rect.size.x / 2); | 694 | const int resMid = d->resizer->rect.size.x / 2; |
695 | setWidth_SidebarWidget( | ||
696 | d, | ||
697 | (d->side == left_SideBarSide | ||
698 | ? local.x | ||
699 | : (rootSize_Window(get_Window()).x - coord_Command(cmd).x)) + | ||
700 | resMid); | ||
628 | } | 701 | } |
629 | return iTrue; | 702 | return iTrue; |
630 | } | 703 | } |
@@ -638,54 +711,19 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
638 | else if (isCommand_Widget(w, ev, "menu.closed")) { | 711 | else if (isCommand_Widget(w, ev, "menu.closed")) { |
639 | setFlags_Widget(as_Widget(d->list), disabled_WidgetFlag, iFalse); | 712 | setFlags_Widget(as_Widget(d->list), disabled_WidgetFlag, iFalse); |
640 | } | 713 | } |
641 | else if (equal_Command(cmd, "sidebar.width")) { | 714 | else if (isCommand_Widget(w, ev, "bookmark.copy")) { |
642 | setWidth_SidebarWidget(d, arg_Command(cmd)); | ||
643 | return iTrue; | ||
644 | } | ||
645 | else if (equal_Command(cmd, "sidebar.mode")) { | ||
646 | const iBool wasChanged = setMode_SidebarWidget(d, arg_Command(cmd)); | ||
647 | updateItems_SidebarWidget_(d); | ||
648 | if ((argLabel_Command(cmd, "show") && !isVisible_Widget(w)) || | ||
649 | (argLabel_Command(cmd, "toggle") && (!isVisible_Widget(w) || !wasChanged))) { | ||
650 | postCommand_App("sidebar.toggle"); | ||
651 | } | ||
652 | scrollOffset_ListWidget(d->list, 0); | ||
653 | return iTrue; | ||
654 | } | ||
655 | else if (equal_Command(cmd, "sidebar.toggle")) { | ||
656 | if (arg_Command(cmd) && isVisible_Widget(w)) { | ||
657 | return iTrue; | ||
658 | } | ||
659 | setFlags_Widget(w, hidden_WidgetFlag, isVisible_Widget(w)); | ||
660 | if (isVisible_Widget(w)) { | ||
661 | w->rect.size.x = d->width; | ||
662 | invalidate_ListWidget(d->list); | ||
663 | } | ||
664 | arrange_Widget(w->parent); | ||
665 | updateSize_DocumentWidget(document_App()); | ||
666 | if (isVisible_Widget(w)) { | ||
667 | updateItems_SidebarWidget_(d); | ||
668 | scrollOffset_ListWidget(d->list, 0); | ||
669 | } | ||
670 | refresh_Widget(w->parent); | ||
671 | return iTrue; | ||
672 | } | ||
673 | else if (equal_Command(cmd, "tabs.changed") || equal_Command(cmd, "document.changed")) { | ||
674 | updateItems_SidebarWidget_(d); | ||
675 | scrollOffset_ListWidget(d->list, 0); | ||
676 | } | ||
677 | else if (equal_Command(cmd, "bookmark.copy")) { | ||
678 | const iSidebarItem *item = d->contextItem; | 715 | const iSidebarItem *item = d->contextItem; |
679 | if (d->mode == bookmarks_SidebarMode && item) { | 716 | if (d->mode == bookmarks_SidebarMode && item) { |
680 | SDL_SetClipboardText(cstr_String(&item->url)); | 717 | SDL_SetClipboardText(cstr_String(&item->url)); |
681 | } | 718 | } |
682 | return iTrue; | 719 | return iTrue; |
683 | } | 720 | } |
684 | else if (equal_Command(cmd, "bookmark.edit")) { | 721 | else if (isCommand_Widget(w, ev, "bookmark.edit")) { |
685 | const iSidebarItem *item = d->contextItem; | 722 | const iSidebarItem *item = d->contextItem; |
686 | if (d->mode == bookmarks_SidebarMode && item) { | 723 | if (d->mode == bookmarks_SidebarMode && item) { |
687 | setFlags_Widget(w, disabled_WidgetFlag, iTrue); | 724 | setFlags_Widget(w, disabled_WidgetFlag, iTrue); |
688 | iWidget *dlg = makeBookmarkEditor_Widget(); | 725 | iWidget *dlg = makeBookmarkEditor_Widget(); |
726 | setId_Widget(dlg, format_CStr("bmed.%s", cstr_String(id_Widget(w)))); | ||
689 | iBookmark *bm = get_Bookmarks(bookmarks_App(), item->id); | 727 | iBookmark *bm = get_Bookmarks(bookmarks_App(), item->id); |
690 | setText_InputWidget(findChild_Widget(dlg, "bmed.title"), &bm->title); | 728 | setText_InputWidget(findChild_Widget(dlg, "bmed.title"), &bm->title); |
691 | setText_InputWidget(findChild_Widget(dlg, "bmed.url"), &bm->url); | 729 | setText_InputWidget(findChild_Widget(dlg, "bmed.url"), &bm->url); |
@@ -695,7 +733,7 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
695 | } | 733 | } |
696 | return iTrue; | 734 | return iTrue; |
697 | } | 735 | } |
698 | else if (equal_Command(cmd, "bookmark.tag")) { | 736 | else if (isCommand_Widget(w, ev, "bookmark.tag")) { |
699 | const iSidebarItem *item = d->contextItem; | 737 | const iSidebarItem *item = d->contextItem; |
700 | if (d->mode == bookmarks_SidebarMode && item) { | 738 | if (d->mode == bookmarks_SidebarMode && item) { |
701 | const char *tag = cstr_String(string_Command(cmd, "tag")); | 739 | const char *tag = cstr_String(string_Command(cmd, "tag")); |
@@ -713,7 +751,7 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
713 | } | 751 | } |
714 | return iTrue; | 752 | return iTrue; |
715 | } | 753 | } |
716 | else if (equal_Command(cmd, "bookmark.delete")) { | 754 | else if (isCommand_Widget(w, ev, "bookmark.delete")) { |
717 | const iSidebarItem *item = d->contextItem; | 755 | const iSidebarItem *item = d->contextItem; |
718 | if (d->mode == bookmarks_SidebarMode && item && remove_Bookmarks(bookmarks_App(), item->id)) { | 756 | if (d->mode == bookmarks_SidebarMode && item && remove_Bookmarks(bookmarks_App(), item->id)) { |
719 | removeEntries_Feeds(item->id); | 757 | removeEntries_Feeds(item->id); |
@@ -721,10 +759,6 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
721 | } | 759 | } |
722 | return iTrue; | 760 | return iTrue; |
723 | } | 761 | } |
724 | else if (equal_Command(cmd, "visited.changed") && | ||
725 | (d->mode == history_SidebarMode || d->mode == feeds_SidebarMode)) { | ||
726 | updateItems_SidebarWidget_(d); | ||
727 | } | ||
728 | else if (equal_Command(cmd, "feeds.update.finished") && d->mode == feeds_SidebarMode) { | 762 | else if (equal_Command(cmd, "feeds.update.finished") && d->mode == feeds_SidebarMode) { |
729 | updateItems_SidebarWidget_(d); | 763 | updateItems_SidebarWidget_(d); |
730 | } | 764 | } |
@@ -742,11 +776,11 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
742 | else if (startsWith_CStr(cmd, "feed.entry.") && d->mode == feeds_SidebarMode) { | 776 | else if (startsWith_CStr(cmd, "feed.entry.") && d->mode == feeds_SidebarMode) { |
743 | const iSidebarItem *item = d->contextItem; | 777 | const iSidebarItem *item = d->contextItem; |
744 | if (item) { | 778 | if (item) { |
745 | if (equal_Command(cmd, "feed.entry.opentab")) { | 779 | if (isCommand_Widget(w, ev, "feed.entry.opentab")) { |
746 | postCommandf_App("open newtab:1 url:%s", cstr_String(&item->url)); | 780 | postCommandf_App("open newtab:1 url:%s", cstr_String(&item->url)); |
747 | return iTrue; | 781 | return iTrue; |
748 | } | 782 | } |
749 | if (equal_Command(cmd, "feed.entry.toggleread")) { | 783 | if (isCommand_Widget(w, ev, "feed.entry.toggleread")) { |
750 | iVisited *vis = visited_App(); | 784 | iVisited *vis = visited_App(); |
751 | if (containsUrl_Visited(vis, &item->url)) { | 785 | if (containsUrl_Visited(vis, &item->url)) { |
752 | removeUrl_Visited(vis, &item->url); | 786 | removeUrl_Visited(vis, &item->url); |
@@ -757,20 +791,21 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
757 | postCommand_App("visited.changed"); | 791 | postCommand_App("visited.changed"); |
758 | return iTrue; | 792 | return iTrue; |
759 | } | 793 | } |
760 | if (equal_Command(cmd, "feed.entry.bookmark")) { | 794 | if (isCommand_Widget(w, ev, "feed.entry.bookmark")) { |
761 | makeBookmarkCreation_Widget(&item->url, &item->label, item->icon); | 795 | makeBookmarkCreation_Widget(&item->url, &item->label, item->icon); |
762 | postCommand_App("focus.set id:bmed.title"); | 796 | postCommand_App("focus.set id:bmed.title"); |
763 | return iTrue; | 797 | return iTrue; |
764 | } | 798 | } |
765 | iBookmark *feedBookmark = get_Bookmarks(bookmarks_App(), item->id); | 799 | iBookmark *feedBookmark = get_Bookmarks(bookmarks_App(), item->id); |
766 | if (feedBookmark) { | 800 | if (feedBookmark) { |
767 | if (equal_Command(cmd, "feed.entry.openfeed")) { | 801 | if (isCommand_Widget(w, ev, "feed.entry.openfeed")) { |
768 | postCommandf_App("open url:%s", cstr_String(&feedBookmark->url)); | 802 | postCommandf_App("open url:%s", cstr_String(&feedBookmark->url)); |
769 | return iTrue; | 803 | return iTrue; |
770 | } | 804 | } |
771 | if (equal_Command(cmd, "feed.entry.edit")) { | 805 | if (isCommand_Widget(w, ev, "feed.entry.edit")) { |
772 | setFlags_Widget(w, disabled_WidgetFlag, iTrue); | 806 | setFlags_Widget(w, disabled_WidgetFlag, iTrue); |
773 | iWidget *dlg = makeBookmarkEditor_Widget(); | 807 | iWidget *dlg = makeBookmarkEditor_Widget(); |
808 | setId_Widget(dlg, format_CStr("bmed.%s", cstr_String(id_Widget(w)))); | ||
774 | setText_InputWidget(findChild_Widget(dlg, "bmed.title"), &feedBookmark->title); | 809 | setText_InputWidget(findChild_Widget(dlg, "bmed.title"), &feedBookmark->title); |
775 | setText_InputWidget(findChild_Widget(dlg, "bmed.url"), &feedBookmark->url); | 810 | setText_InputWidget(findChild_Widget(dlg, "bmed.url"), &feedBookmark->url); |
776 | setText_InputWidget(findChild_Widget(dlg, "bmed.tags"), &feedBookmark->tags); | 811 | setText_InputWidget(findChild_Widget(dlg, "bmed.tags"), &feedBookmark->tags); |
@@ -778,7 +813,7 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
778 | setFocus_Widget(findChild_Widget(dlg, "bmed.title")); | 813 | setFocus_Widget(findChild_Widget(dlg, "bmed.title")); |
779 | return iTrue; | 814 | return iTrue; |
780 | } | 815 | } |
781 | if (equal_Command(cmd, "feed.entry.unsubscribe")) { | 816 | if (isCommand_Widget(w, ev, "feed.entry.unsubscribe")) { |
782 | if (arg_Command(cmd)) { | 817 | if (arg_Command(cmd)) { |
783 | removeTag_Bookmark(feedBookmark, "subscribed"); | 818 | removeTag_Bookmark(feedBookmark, "subscribed"); |
784 | removeEntries_Feeds(id_Bookmark(feedBookmark)); | 819 | removeEntries_Feeds(id_Bookmark(feedBookmark)); |
@@ -791,7 +826,9 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
791 | cstr_String(&feedBookmark->title)), | 826 | cstr_String(&feedBookmark->title)), |
792 | (const char *[]){ "Cancel", | 827 | (const char *[]){ "Cancel", |
793 | uiTextCaution_ColorEscape "Unsubscribe" }, | 828 | uiTextCaution_ColorEscape "Unsubscribe" }, |
794 | (const char *[]){ "cancel", "feed.entry.unsubscribe arg:1" }, | 829 | (const char *[]){ |
830 | "cancel", | ||
831 | format_CStr("!feed.entry.unsubscribe arg:1 ptr:%p", d) }, | ||
795 | 2); | 832 | 2); |
796 | } | 833 | } |
797 | return iTrue; | 834 | return iTrue; |
@@ -799,13 +836,6 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
799 | } | 836 | } |
800 | } | 837 | } |
801 | } | 838 | } |
802 | else if (equal_Command(cmd, "bookmarks.changed") && (d->mode == bookmarks_SidebarMode || | ||
803 | d->mode == feeds_SidebarMode)) { | ||
804 | updateItems_SidebarWidget_(d); | ||
805 | } | ||
806 | else if (equal_Command(cmd, "idents.changed") && d->mode == identities_SidebarMode) { | ||
807 | updateItems_SidebarWidget_(d); | ||
808 | } | ||
809 | else if (isCommand_Widget(w, ev, "ident.use")) { | 839 | else if (isCommand_Widget(w, ev, "ident.use")) { |
810 | iGmIdentity * ident = menuIdentity_SidebarWidget_(d); | 840 | iGmIdentity * ident = menuIdentity_SidebarWidget_(d); |
811 | const iString *tabUrl = url_DocumentWidget(document_App()); | 841 | const iString *tabUrl = url_DocumentWidget(document_App()); |
@@ -843,7 +873,7 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
843 | } | 873 | } |
844 | return iTrue; | 874 | return iTrue; |
845 | } | 875 | } |
846 | else if (equal_Command(cmd, "ident.setnotes")) { | 876 | else if (isCommand_Widget(w, ev, "ident.setnotes")) { |
847 | iGmIdentity *ident = pointerLabel_Command(cmd, "ident"); | 877 | iGmIdentity *ident = pointerLabel_Command(cmd, "ident"); |
848 | if (ident) { | 878 | if (ident) { |
849 | setCStr_String(&ident->notes, suffixPtr_Command(cmd, "value")); | 879 | setCStr_String(&ident->notes, suffixPtr_Command(cmd, "value")); |
@@ -864,27 +894,27 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
864 | } | 894 | } |
865 | return iTrue; | 895 | return iTrue; |
866 | } | 896 | } |
867 | else if (equal_Command(cmd, "ident.delete")) { | 897 | else if (isCommand_Widget(w, ev, "ident.delete")) { |
868 | iSidebarItem *item = d->contextItem; | 898 | iSidebarItem *item = d->contextItem; |
869 | if (argLabel_Command(cmd, "confirm")) { | 899 | if (argLabel_Command(cmd, "confirm")) { |
870 | makeQuestion_Widget(uiTextCaution_ColorEscape "DELETE IDENTITY", | 900 | makeQuestion_Widget( |
871 | format_CStr("Do you really want to delete the identity\n" | 901 | uiTextCaution_ColorEscape "DELETE IDENTITY", |
872 | uiTextAction_ColorEscape "%s\n" | 902 | format_CStr( |
873 | uiText_ColorEscape | 903 | "Do you really want to delete the identity\n" uiTextAction_ColorEscape |
874 | "including its certificate and private key files?", | 904 | "%s\n" uiText_ColorEscape |
875 | cstr_String(&item->label)), | 905 | "including its certificate and private key files?", |
876 | (const char *[]){ "Cancel", | 906 | cstr_String(&item->label)), |
877 | uiTextCaution_ColorEscape | 907 | (const char *[]){ "Cancel", |
878 | "Delete Identity and Files" }, | 908 | uiTextCaution_ColorEscape "Delete Identity and Files" }, |
879 | (const char *[]){ "cancel", "ident.delete confirm:0" }, | 909 | (const char *[]){ "cancel", format_CStr("!ident.delete confirm:0 ptr:%p", d) }, |
880 | 2); | 910 | 2); |
881 | return iTrue; | 911 | return iTrue; |
882 | } | 912 | } |
883 | deleteIdentity_GmCerts(certs_App(), hoverIdentity_SidebarWidget_(d)); | 913 | deleteIdentity_GmCerts(certs_App(), hoverIdentity_SidebarWidget_(d)); |
884 | updateItems_SidebarWidget_(d); | 914 | postCommand_App("idents.changed"); |
885 | return iTrue; | 915 | return iTrue; |
886 | } | 916 | } |
887 | else if (equal_Command(cmd, "history.delete")) { | 917 | else if (isCommand_Widget(w, ev, "history.delete")) { |
888 | if (d->contextItem && !isEmpty_String(&d->contextItem->url)) { | 918 | if (d->contextItem && !isEmpty_String(&d->contextItem->url)) { |
889 | removeUrl_Visited(visited_App(), &d->contextItem->url); | 919 | removeUrl_Visited(visited_App(), &d->contextItem->url); |
890 | updateItems_SidebarWidget_(d); | 920 | updateItems_SidebarWidget_(d); |
@@ -892,14 +922,14 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev) | |||
892 | } | 922 | } |
893 | return iTrue; | 923 | return iTrue; |
894 | } | 924 | } |
895 | else if (equal_Command(cmd, "history.copy")) { | 925 | else if (isCommand_Widget(w, ev, "history.copy")) { |
896 | const iSidebarItem *item = d->contextItem; | 926 | const iSidebarItem *item = d->contextItem; |
897 | if (item && !isEmpty_String(&item->url)) { | 927 | if (item && !isEmpty_String(&item->url)) { |
898 | SDL_SetClipboardText(cstr_String(&item->url)); | 928 | SDL_SetClipboardText(cstr_String(&item->url)); |
899 | } | 929 | } |
900 | return iTrue; | 930 | return iTrue; |
901 | } | 931 | } |
902 | else if (equal_Command(cmd, "history.addbookmark")) { | 932 | else if (isCommand_Widget(w, ev, "history.addbookmark")) { |
903 | const iSidebarItem *item = d->contextItem; | 933 | const iSidebarItem *item = d->contextItem; |
904 | if (!isEmpty_String(&item->url)) { | 934 | if (!isEmpty_String(&item->url)) { |
905 | makeBookmarkCreation_Widget( | 935 | makeBookmarkCreation_Widget( |
diff --git a/src/ui/sidebarwidget.h b/src/ui/sidebarwidget.h index 0d4ed9c8..fa74e049 100644 --- a/src/ui/sidebarwidget.h +++ b/src/ui/sidebarwidget.h | |||
@@ -33,8 +33,13 @@ enum iSidebarMode { | |||
33 | max_SidebarMode | 33 | max_SidebarMode |
34 | }; | 34 | }; |
35 | 35 | ||
36 | enum iSidebarSide { | ||
37 | left_SideBarSide, | ||
38 | right_SideBarSide, | ||
39 | }; | ||
40 | |||
36 | iDeclareWidgetClass(SidebarWidget) | 41 | iDeclareWidgetClass(SidebarWidget) |
37 | iDeclareObjectConstruction(SidebarWidget) | 42 | iDeclareObjectConstructionArgs(SidebarWidget, enum iSidebarSide side) |
38 | 43 | ||
39 | iBool setMode_SidebarWidget (iSidebarWidget *, enum iSidebarMode mode); | 44 | iBool setMode_SidebarWidget (iSidebarWidget *, enum iSidebarMode mode); |
40 | 45 | ||
diff --git a/src/ui/text.c b/src/ui/text.c index 686927b1..ae4b1714 100644 --- a/src/ui/text.c +++ b/src/ui/text.c | |||
@@ -85,7 +85,7 @@ iDefineTypeConstructionArgs(Glyph, (iChar ch), ch) | |||
85 | struct Impl_Font { | 85 | struct Impl_Font { |
86 | iBlock * data; | 86 | iBlock * data; |
87 | stbtt_fontinfo font; | 87 | stbtt_fontinfo font; |
88 | float scale; | 88 | float xScale, yScale; |
89 | int vertOffset; /* offset due to scaling */ | 89 | int vertOffset; /* offset due to scaling */ |
90 | int height; | 90 | int height; |
91 | int baseline; | 91 | int baseline; |
@@ -100,21 +100,33 @@ struct Impl_Font { | |||
100 | 100 | ||
101 | static iFont *font_Text_(enum iFontId id); | 101 | static iFont *font_Text_(enum iFontId id); |
102 | 102 | ||
103 | static void init_Font(iFont *d, const iBlock *data, int height, float scale, enum iFontId symbolsFont) { | 103 | static void init_Font(iFont *d, const iBlock *data, int height, float scale, |
104 | enum iFontId symbolsFont, iBool isMonospaced) { | ||
104 | init_Hash(&d->glyphs); | 105 | init_Hash(&d->glyphs); |
105 | d->data = NULL; | 106 | d->data = NULL; |
107 | d->isMonospaced = isMonospaced; | ||
106 | d->height = height; | 108 | d->height = height; |
107 | iZap(d->font); | 109 | iZap(d->font); |
108 | stbtt_InitFont(&d->font, constData_Block(data), 0); | 110 | stbtt_InitFont(&d->font, constData_Block(data), 0); |
109 | d->scale = stbtt_ScaleForPixelHeight(&d->font, height) * scale; | 111 | d->xScale = d->yScale = stbtt_ScaleForPixelHeight(&d->font, height) * scale; |
112 | if (d->isMonospaced) { | ||
113 | /* It is important that monospaced fonts align 1:1 with the pixel grid so that | ||
114 | box-drawing characters don't have partially occupied edge pixels, leading to seams | ||
115 | between adjacent glyphs. */ | ||
116 | int adv; | ||
117 | stbtt_GetCodepointHMetrics(&d->font, 'M', &adv, NULL); | ||
118 | const float advance = (float) adv * d->xScale; | ||
119 | if (advance > 4) { /* not too tiny */ | ||
120 | d->xScale *= floorf(advance) / advance; | ||
121 | } | ||
122 | } | ||
110 | d->vertOffset = height * (1.0f - scale) / 2; | 123 | d->vertOffset = height * (1.0f - scale) / 2; |
111 | int ascent; | 124 | int ascent; |
112 | stbtt_GetFontVMetrics(&d->font, &ascent, NULL, NULL); | 125 | stbtt_GetFontVMetrics(&d->font, &ascent, NULL, NULL); |
113 | d->baseline = (int) ascent * d->scale; | 126 | d->baseline = ceil(ascent * d->yScale); |
114 | d->symbolsFont = symbolsFont; | 127 | d->symbolsFont = symbolsFont; |
115 | d->japaneseFont = regularJapanese_FontId; | 128 | d->japaneseFont = regularJapanese_FontId; |
116 | d->koreanFont = regularKorean_FontId; | 129 | d->koreanFont = regularKorean_FontId; |
117 | d->isMonospaced = iFalse; | ||
118 | memset(d->indexTable, 0xff, sizeof(d->indexTable)); | 130 | memset(d->indexTable, 0xff, sizeof(d->indexTable)); |
119 | } | 131 | } |
120 | 132 | ||
@@ -271,11 +283,12 @@ static void initFonts_Text_(iText *d) { | |||
271 | }; | 283 | }; |
272 | iForIndices(i, fontData) { | 284 | iForIndices(i, fontData) { |
273 | iFont *font = &d->fonts[i]; | 285 | iFont *font = &d->fonts[i]; |
274 | init_Font( | 286 | init_Font(font, |
275 | font, fontData[i].ttf, fontData[i].size, fontData[i].scaling, fontData[i].symbolsFont); | 287 | fontData[i].ttf, |
276 | if (fontData[i].ttf == &fontFiraMonoRegular_Embedded) { | 288 | fontData[i].size, |
277 | font->isMonospaced = iTrue; | 289 | fontData[i].scaling, |
278 | } | 290 | fontData[i].symbolsFont, |
291 | fontData[i].ttf == &fontFiraMonoRegular_Embedded); | ||
279 | if (i == default_FontId || i == defaultMedium_FontId) { | 292 | if (i == default_FontId || i == defaultMedium_FontId) { |
280 | font->manualKernOnly = iTrue; | 293 | font->manualKernOnly = iTrue; |
281 | } | 294 | } |
@@ -423,7 +436,7 @@ static void freeBmp_(void *ptr) { | |||
423 | static SDL_Surface *rasterizeGlyph_Font_(const iFont *d, uint32_t glyphIndex, float xShift) { | 436 | static SDL_Surface *rasterizeGlyph_Font_(const iFont *d, uint32_t glyphIndex, float xShift) { |
424 | int w, h; | 437 | int w, h; |
425 | uint8_t *bmp = stbtt_GetGlyphBitmapSubpixel( | 438 | uint8_t *bmp = stbtt_GetGlyphBitmapSubpixel( |
426 | &d->font, d->scale, d->scale, xShift, 0.0f, glyphIndex, &w, &h, 0, 0); | 439 | &d->font, d->xScale, d->yScale, xShift, 0.0f, glyphIndex, &w, &h, 0, 0); |
427 | collect_Garbage(bmp, freeBmp_); /* `bmp` must be freed afterwards. */ | 440 | collect_Garbage(bmp, freeBmp_); /* `bmp` must be freed afterwards. */ |
428 | SDL_Surface *surface8 = | 441 | SDL_Surface *surface8 = |
429 | SDL_CreateRGBSurfaceWithFormatFrom(bmp, w, h, 8, w, SDL_PIXELFORMAT_INDEX8); | 442 | SDL_CreateRGBSurfaceWithFormatFrom(bmp, w, h, 8, w, SDL_PIXELFORMAT_INDEX8); |
@@ -478,12 +491,12 @@ static void cache_Font_(iFont *d, iGlyph *glyph, int hoff) { | |||
478 | int adv; | 491 | int adv; |
479 | const uint32_t gIndex = glyph->glyphIndex; | 492 | const uint32_t gIndex = glyph->glyphIndex; |
480 | stbtt_GetGlyphHMetrics(&d->font, gIndex, &adv, NULL); | 493 | stbtt_GetGlyphHMetrics(&d->font, gIndex, &adv, NULL); |
481 | glyph->advance = d->scale * adv; | 494 | glyph->advance = d->xScale * adv; |
482 | } | 495 | } |
483 | stbtt_GetGlyphBitmapBoxSubpixel(&d->font, | 496 | stbtt_GetGlyphBitmapBoxSubpixel(&d->font, |
484 | glyph->glyphIndex, | 497 | glyph->glyphIndex, |
485 | d->scale, | 498 | d->xScale, |
486 | d->scale, | 499 | d->yScale, |
487 | hoff * 0.5f, | 500 | hoff * 0.5f, |
488 | 0.0f, | 501 | 0.0f, |
489 | &glyph->d[hoff].x, | 502 | &glyph->d[hoff].x, |
@@ -652,6 +665,17 @@ static iRect run_Font_(iFont *d, enum iRunMode mode, iRangecc text, size_t maxLe | |||
652 | } | 665 | } |
653 | } | 666 | } |
654 | iChar ch = nextChar_(&chPos, text.end); | 667 | iChar ch = nextChar_(&chPos, text.end); |
668 | if (ch == 0x200d) { /* zero-width joiner */ | ||
669 | /* We don't have the composited Emojis. */ | ||
670 | if (isEmoji_Char(prevCh)) { | ||
671 | /* skip */ | ||
672 | ch = nextChar_(&chPos, text.end); | ||
673 | ch = nextChar_(&chPos, text.end); | ||
674 | } | ||
675 | else { | ||
676 | printf("it's %x\n", prevCh); | ||
677 | } | ||
678 | } | ||
655 | if (isVariationSelector_Char(ch)) { | 679 | if (isVariationSelector_Char(ch)) { |
656 | /* TODO: VS15: Should peek ahead for this and prefer the Emoji font. */ | 680 | /* TODO: VS15: Should peek ahead for this and prefer the Emoji font. */ |
657 | ch = nextChar_(&chPos, text.end); /* just ignore */ | 681 | ch = nextChar_(&chPos, text.end); /* just ignore */ |
@@ -699,6 +723,9 @@ static iRect run_Font_(iFont *d, enum iRunMode mode, iRangecc text, size_t maxLe | |||
699 | prevCh = 0; | 723 | prevCh = 0; |
700 | continue; | 724 | continue; |
701 | } | 725 | } |
726 | if (isDefaultIgnorable_Char(ch) || isFitzpatrickType_Char(ch)) { | ||
727 | continue; | ||
728 | } | ||
702 | } | 729 | } |
703 | const iGlyph *glyph = glyph_Font_(d, ch); | 730 | const iGlyph *glyph = glyph_Font_(d, ch); |
704 | int x1 = xpos; | 731 | int x1 = xpos; |
@@ -714,6 +741,7 @@ static iRect run_Font_(iFont *d, enum iRunMode mode, iRangecc text, size_t maxLe | |||
714 | } | 741 | } |
715 | break; | 742 | break; |
716 | } | 743 | } |
744 | const int yLineMax = pos.y + d->height; | ||
717 | SDL_Rect dst = { x1 + glyph->d[hoff].x, | 745 | SDL_Rect dst = { x1 + glyph->d[hoff].x, |
718 | pos.y + glyph->font->baseline + glyph->d[hoff].y, | 746 | pos.y + glyph->font->baseline + glyph->d[hoff].y, |
719 | glyph->rect[hoff].size.x, | 747 | glyph->rect[hoff].size.x, |
@@ -739,7 +767,16 @@ static iRect run_Font_(iFont *d, enum iRunMode mode, iRangecc text, size_t maxLe | |||
739 | /* Glyphs from a different font may need recentering to look better. */ | 767 | /* Glyphs from a different font may need recentering to look better. */ |
740 | dst.x -= (dst.w - advance) / 2; | 768 | dst.x -= (dst.w - advance) / 2; |
741 | } | 769 | } |
742 | SDL_RenderCopy(text_.render, text_.cache, (const SDL_Rect *) &glyph->rect[hoff], &dst); | 770 | SDL_Rect src; |
771 | memcpy(&src, &glyph->rect[hoff], sizeof(SDL_Rect)); | ||
772 | /* Clip the glyphs to the font's height. This is useful when the font's line spacing | ||
773 | has been reduced or when the glyph is from a different font. */ | ||
774 | if (dst.y + dst.h > yLineMax) { | ||
775 | const int over = dst.y + dst.h - yLineMax; | ||
776 | src.h -= over; | ||
777 | dst.h -= over; | ||
778 | } | ||
779 | SDL_RenderCopy(text_.render, text_.cache, &src, &dst); | ||
743 | } | 780 | } |
744 | /* Symbols and emojis are NOT monospaced, so must conform when the primary font | 781 | /* Symbols and emojis are NOT monospaced, so must conform when the primary font |
745 | is monospaced. Except with Japanese script, that's larger than the normal monospace. */ | 782 | is monospaced. Except with Japanese script, that's larger than the normal monospace. */ |
@@ -755,7 +792,7 @@ static iRect run_Font_(iFont *d, enum iRunMode mode, iRangecc text, size_t maxLe | |||
755 | const char *peek = chPos; | 792 | const char *peek = chPos; |
756 | const iChar next = nextChar_(&peek, text.end); | 793 | const iChar next = nextChar_(&peek, text.end); |
757 | if (enableKerning_Text && !d->manualKernOnly && next) { | 794 | if (enableKerning_Text && !d->manualKernOnly && next) { |
758 | xpos += d->scale * stbtt_GetGlyphKernAdvance(&d->font, glyph->glyphIndex, next); | 795 | xpos += d->xScale * stbtt_GetGlyphKernAdvance(&d->font, glyph->glyphIndex, next); |
759 | } | 796 | } |
760 | } | 797 | } |
761 | #endif | 798 | #endif |
@@ -977,7 +1014,7 @@ iString *renderBlockChars_Text(const iBlock *fontData, int height, enum iTextBlo | |||
977 | size_t strRemain = length_String(text); | 1014 | size_t strRemain = length_String(text); |
978 | iConstForEach(String, i, text) { | 1015 | iConstForEach(String, i, text) { |
979 | if (!strRemain) break; | 1016 | if (!strRemain) break; |
980 | if (i.value == variationSelectorEmoji_Char) { | 1017 | if (isVariationSelector_Char(i.value) || isDefaultIgnorable_Char(i.value)) { |
981 | strRemain--; | 1018 | strRemain--; |
982 | continue; | 1019 | continue; |
983 | } | 1020 | } |
diff --git a/src/ui/text.h b/src/ui/text.h index 5bae8e2a..a0b2dc1a 100644 --- a/src/ui/text.h +++ b/src/ui/text.h | |||
@@ -114,11 +114,21 @@ enum iFontId { | |||
114 | iLocalDef iBool isJapanese_FontId(enum iFontId id) { | 114 | iLocalDef iBool isJapanese_FontId(enum iFontId id) { |
115 | return id >= defaultJapanese_FontId && id <= hugeJapanese_FontId; | 115 | return id >= defaultJapanese_FontId && id <= hugeJapanese_FontId; |
116 | } | 116 | } |
117 | iLocalDef iBool isVariationSelector_Char(iChar ch) { | 117 | iLocalDef iBool isVariationSelector_Char(iChar c) { |
118 | return ch >= 0xfe00 && ch <= 0xfe0f; | 118 | return (c >= 0xfe00 && c <= 0xfe0f) || (c >= 0xe0100 && c <= 0xe0121); |
119 | } | ||
120 | iLocalDef iBool isFitzpatrickType_Char(iChar c) { | ||
121 | return c >= 0x1f3fb && c <= 0x1f3ff; | ||
122 | } | ||
123 | iLocalDef iBool isDefaultIgnorable_Char(iChar c) { | ||
124 | return c == 0x115f || (c >= 0x200b && c <= 0x200e) || c == 0x2060 || c == 0x2061 || | ||
125 | c == 0xfeff; | ||
126 | } | ||
127 | iLocalDef iBool isEmoji_Char(iChar c) { | ||
128 | return (c >= 0x1f300 && c < 0x1f700) || (c >= 0x1f900 && c <= 0x1f9ff); | ||
119 | } | 129 | } |
120 | 130 | ||
121 | #define variationSelectorEmoji_Char ((iChar) 0xfe0f) | 131 | #define emojiVariationSelector_Char ((iChar) 0xfe0f) |
122 | 132 | ||
123 | enum iTextFont { | 133 | enum iTextFont { |
124 | nunito_TextFont, | 134 | nunito_TextFont, |
diff --git a/src/ui/util.c b/src/ui/util.c index d9997004..1ad3f30e 100644 --- a/src/ui/util.c +++ b/src/ui/util.c | |||
@@ -456,14 +456,14 @@ void openMenu_Widget(iWidget *d, iInt2 coord) { | |||
456 | if (leftExcess > 0) { | 456 | if (leftExcess > 0) { |
457 | d->rect.pos.x += leftExcess; | 457 | d->rect.pos.x += leftExcess; |
458 | } | 458 | } |
459 | refresh_App(); | 459 | postRefresh_App(); |
460 | postCommand_Widget(d, "menu.opened"); | 460 | postCommand_Widget(d, "menu.opened"); |
461 | } | 461 | } |
462 | 462 | ||
463 | void closeMenu_Widget(iWidget *d) { | 463 | void closeMenu_Widget(iWidget *d) { |
464 | setFlags_Widget(d, hidden_WidgetFlag, iTrue); | 464 | setFlags_Widget(d, hidden_WidgetFlag, iTrue); |
465 | setFlags_Widget(findChild_Widget(d, "menu.cancel"), disabled_WidgetFlag, iTrue); | 465 | setFlags_Widget(findChild_Widget(d, "menu.cancel"), disabled_WidgetFlag, iTrue); |
466 | refresh_App(); | 466 | postRefresh_App(); |
467 | postCommand_Widget(d, "menu.closed"); | 467 | postCommand_Widget(d, "menu.closed"); |
468 | } | 468 | } |
469 | 469 | ||
@@ -1031,8 +1031,8 @@ iWidget *makePreferences_Widget(void) { | |||
1031 | appendTwoColumnPage_(tabs, "General", '1', &headings, &values); | 1031 | appendTwoColumnPage_(tabs, "General", '1', &headings, &values); |
1032 | addChild_Widget(headings, iClob(makeHeading_Widget("Downloads folder:"))); | 1032 | addChild_Widget(headings, iClob(makeHeading_Widget("Downloads folder:"))); |
1033 | setId_Widget(addChild_Widget(values, iClob(new_InputWidget(0))), "prefs.downloads"); | 1033 | setId_Widget(addChild_Widget(values, iClob(new_InputWidget(0))), "prefs.downloads"); |
1034 | addChild_Widget(headings, iClob(makeHeading_Widget("Outline on scrollbar:"))); | 1034 | /*addChild_Widget(headings, iClob(makeHeading_Widget("Outline on scrollbar:"))); |
1035 | addChild_Widget(values, iClob(makeToggle_Widget("prefs.hoveroutline"))); | 1035 | addChild_Widget(values, iClob(makeToggle_Widget("prefs.hoveroutline")));*/ |
1036 | addChild_Widget(headings, iClob(makeHeading_Widget("Smooth scrolling:"))); | 1036 | addChild_Widget(headings, iClob(makeHeading_Widget("Smooth scrolling:"))); |
1037 | addChild_Widget(values, iClob(makeToggle_Widget("prefs.smoothscroll"))); | 1037 | addChild_Widget(values, iClob(makeToggle_Widget("prefs.smoothscroll"))); |
1038 | addChild_Widget(headings, iClob(makeHeading_Widget("Load image on scroll:"))); | 1038 | addChild_Widget(headings, iClob(makeHeading_Widget("Load image on scroll:"))); |
diff --git a/src/ui/widget.c b/src/ui/widget.c index 4d50da38..f3e73ee7 100644 --- a/src/ui/widget.c +++ b/src/ui/widget.c | |||
@@ -263,10 +263,13 @@ void arrange_Widget(iWidget *d) { | |||
263 | setFlags_Widget(d, wasCollapsed_WidgetFlag, iTrue); | 263 | setFlags_Widget(d, wasCollapsed_WidgetFlag, iTrue); |
264 | return; | 264 | return; |
265 | } | 265 | } |
266 | if (d->flags & moveToParentRightEdge_WidgetFlag) { | 266 | if (d->flags & moveToParentLeftEdge_WidgetFlag) { |
267 | d->rect.pos.x = d->padding[0]; | ||
268 | } | ||
269 | else if (d->flags & moveToParentRightEdge_WidgetFlag) { | ||
267 | d->rect.pos.x = width_Rect(innerRect_Widget_(d->parent)) - width_Rect(d->rect); | 270 | d->rect.pos.x = width_Rect(innerRect_Widget_(d->parent)) - width_Rect(d->rect); |
268 | } | 271 | } |
269 | if (d->flags & centerHorizontal_WidgetFlag) { | 272 | else if (d->flags & centerHorizontal_WidgetFlag) { |
270 | centerHorizontal_Widget_(d); | 273 | centerHorizontal_Widget_(d); |
271 | } | 274 | } |
272 | if (d->flags & resizeToParentWidth_WidgetFlag) { | 275 | if (d->flags & resizeToParentWidth_WidgetFlag) { |
@@ -388,7 +391,8 @@ void arrange_Widget(iWidget *d) { | |||
388 | continue; | 391 | continue; |
389 | } | 392 | } |
390 | if (d->flags & (arrangeHorizontal_WidgetFlag | arrangeVertical_WidgetFlag)) { | 393 | if (d->flags & (arrangeHorizontal_WidgetFlag | arrangeVertical_WidgetFlag)) { |
391 | if (child->flags & moveToParentRightEdge_WidgetFlag) { | 394 | if (child->flags & |
395 | (moveToParentLeftEdge_WidgetFlag | moveToParentRightEdge_WidgetFlag)) { | ||
392 | continue; /* Not part of the sequential arrangement .*/ | 396 | continue; /* Not part of the sequential arrangement .*/ |
393 | } | 397 | } |
394 | child->rect.pos = pos; | 398 | child->rect.pos = pos; |
@@ -422,7 +426,8 @@ void arrange_Widget(iWidget *d) { | |||
422 | iForEach(ObjectList, j, d->children) { | 426 | iForEach(ObjectList, j, d->children) { |
423 | iWidget *child = as_Widget(j.object); | 427 | iWidget *child = as_Widget(j.object); |
424 | if (child->flags & | 428 | if (child->flags & |
425 | (resizeToParentWidth_WidgetFlag | moveToParentRightEdge_WidgetFlag)) { | 429 | (resizeToParentWidth_WidgetFlag | moveToParentLeftEdge_WidgetFlag | |
430 | moveToParentRightEdge_WidgetFlag)) { | ||
426 | arrange_Widget(child); | 431 | arrange_Widget(child); |
427 | } | 432 | } |
428 | } | 433 | } |
diff --git a/src/ui/widget.h b/src/ui/widget.h index 278ae081..f06a6607 100644 --- a/src/ui/widget.h +++ b/src/ui/widget.h | |||
@@ -85,10 +85,11 @@ enum iWidgetFlag { | |||
85 | /* 64-bit extended flags */ | 85 | /* 64-bit extended flags */ |
86 | #define wasCollapsed_WidgetFlag iBit64(32) | 86 | #define wasCollapsed_WidgetFlag iBit64(32) |
87 | #define centerHorizontal_WidgetFlag iBit64(33) | 87 | #define centerHorizontal_WidgetFlag iBit64(33) |
88 | #define moveToParentRightEdge_WidgetFlag iBit64(34) | 88 | #define moveToParentLeftEdge_WidgetFlag iBit64(34) |
89 | #define wrapText_WidgetFlag iBit64(35) | 89 | #define moveToParentRightEdge_WidgetFlag iBit64(35) |
90 | #define borderTop_WidgetFlag iBit64(36) | 90 | #define wrapText_WidgetFlag iBit64(36) |
91 | #define overflowScrollable_WidgetFlag iBit64(37) | 91 | #define borderTop_WidgetFlag iBit64(37) |
92 | #define overflowScrollable_WidgetFlag iBit64(38) | ||
92 | 93 | ||
93 | enum iWidgetAddPos { | 94 | enum iWidgetAddPos { |
94 | back_WidgetAddPos, | 95 | back_WidgetAddPos, |
diff --git a/src/ui/window.c b/src/ui/window.c index 199e35a7..1bf9065e 100644 --- a/src/ui/window.c +++ b/src/ui/window.c | |||
@@ -109,7 +109,8 @@ static const iMenuItem navMenuItems_[] = { | |||
109 | { "---", 0, 0, NULL }, | 109 | { "---", 0, 0, NULL }, |
110 | { "Show Feed Entries", 0, 0, "!open url:about:feeds" }, | 110 | { "Show Feed Entries", 0, 0, "!open url:about:feeds" }, |
111 | { "---", 0, 0, NULL }, | 111 | { "---", 0, 0, NULL }, |
112 | { "Toggle Sidebar", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" }, | 112 | { "Toggle Left Sidebar", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" }, |
113 | { "Toggle Right Sidebar", SDLK_p, KMOD_PRIMARY | KMOD_SHIFT, "sidebar2.toggle" }, | ||
113 | { "Zoom In", SDLK_EQUALS, KMOD_PRIMARY, "zoom.delta arg:10" }, | 114 | { "Zoom In", SDLK_EQUALS, KMOD_PRIMARY, "zoom.delta arg:10" }, |
114 | { "Zoom Out", SDLK_MINUS, KMOD_PRIMARY, "zoom.delta arg:-10" }, | 115 | { "Zoom Out", SDLK_MINUS, KMOD_PRIMARY, "zoom.delta arg:-10" }, |
115 | { "Reset Zoom", SDLK_0, KMOD_PRIMARY, "zoom.set arg:100" }, | 116 | { "Reset Zoom", SDLK_0, KMOD_PRIMARY, "zoom.set arg:100" }, |
@@ -144,7 +145,8 @@ static const iMenuItem viewMenuItems_[] = { | |||
144 | { "Show History", '3', KMOD_PRIMARY, "sidebar.mode arg:2 toggle:1" }, | 145 | { "Show History", '3', KMOD_PRIMARY, "sidebar.mode arg:2 toggle:1" }, |
145 | { "Show Identities", '4', KMOD_PRIMARY, "sidebar.mode arg:3 toggle:1" }, | 146 | { "Show Identities", '4', KMOD_PRIMARY, "sidebar.mode arg:3 toggle:1" }, |
146 | { "Show Page Outline", '5', KMOD_PRIMARY, "sidebar.mode arg:4 toggle:1" }, | 147 | { "Show Page Outline", '5', KMOD_PRIMARY, "sidebar.mode arg:4 toggle:1" }, |
147 | { "Toggle Sidebar", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" }, | 148 | { "Toggle Left Sidebar", SDLK_l, KMOD_PRIMARY | KMOD_SHIFT, "sidebar.toggle" }, |
149 | { "Toggle Right Sidebar", SDLK_p, KMOD_PRIMARY | KMOD_SHIFT, "sidebar2.toggle" }, | ||
148 | { "---", 0, 0, NULL }, | 150 | { "---", 0, 0, NULL }, |
149 | { "Go Back", SDLK_LEFTBRACKET, KMOD_PRIMARY, "navigate.back" }, | 151 | { "Go Back", SDLK_LEFTBRACKET, KMOD_PRIMARY, "navigate.back" }, |
150 | { "Go Forward", SDLK_RIGHTBRACKET, KMOD_PRIMARY, "navigate.forward" }, | 152 | { "Go Forward", SDLK_RIGHTBRACKET, KMOD_PRIMARY, "navigate.forward" }, |
@@ -343,7 +345,7 @@ static iBool handleSearchBarCommands_(iWidget *searchBar, const char *cmd) { | |||
343 | if (!isVisible_Widget(searchBar)) { | 345 | if (!isVisible_Widget(searchBar)) { |
344 | setFlags_Widget(searchBar, hidden_WidgetFlag | disabled_WidgetFlag, iFalse); | 346 | setFlags_Widget(searchBar, hidden_WidgetFlag | disabled_WidgetFlag, iFalse); |
345 | arrange_Widget(get_Window()->root); | 347 | arrange_Widget(get_Window()->root); |
346 | refresh_App(); | 348 | postRefresh_App(); |
347 | } | 349 | } |
348 | } | 350 | } |
349 | } | 351 | } |
@@ -459,10 +461,12 @@ static void setupUserInterface_Window(iWindow *d) { | |||
459 | addChild_Widget(buttons, iClob(newIcon_LabelWidget("\u2795", 0, 0, "tabs.new"))), | 461 | addChild_Widget(buttons, iClob(newIcon_LabelWidget("\u2795", 0, 0, "tabs.new"))), |
460 | "newtab"); | 462 | "newtab"); |
461 | } | 463 | } |
462 | /* Side bar. */ { | 464 | /* Side bars. */ { |
463 | iWidget *content = findChild_Widget(d->root, "tabs.content"); | 465 | iWidget *content = findChild_Widget(d->root, "tabs.content"); |
464 | iSidebarWidget *sidebar = new_SidebarWidget(); | 466 | iSidebarWidget *sidebar1 = new_SidebarWidget(left_SideBarSide); |
465 | addChildPos_Widget(content, iClob(sidebar), front_WidgetAddPos); | 467 | addChildPos_Widget(content, iClob(sidebar1), front_WidgetAddPos); |
468 | iSidebarWidget *sidebar2 = new_SidebarWidget(right_SideBarSide); | ||
469 | addChildPos_Widget(content, iClob(sidebar2), back_WidgetAddPos); | ||
466 | } | 470 | } |
467 | /* Lookup results. */ { | 471 | /* Lookup results. */ { |
468 | iLookupWidget *lookup = new_LookupWidget(); | 472 | iLookupWidget *lookup = new_LookupWidget(); |
@@ -507,6 +511,11 @@ static void setupUserInterface_Window(iWindow *d) { | |||
507 | addAction_Widget(d->root, '3', KMOD_PRIMARY, "sidebar.mode arg:2 toggle:1"); | 511 | addAction_Widget(d->root, '3', KMOD_PRIMARY, "sidebar.mode arg:2 toggle:1"); |
508 | addAction_Widget(d->root, '4', KMOD_PRIMARY, "sidebar.mode arg:3 toggle:1"); | 512 | addAction_Widget(d->root, '4', KMOD_PRIMARY, "sidebar.mode arg:3 toggle:1"); |
509 | addAction_Widget(d->root, '5', KMOD_PRIMARY, "sidebar.mode arg:4 toggle:1"); | 513 | addAction_Widget(d->root, '5', KMOD_PRIMARY, "sidebar.mode arg:4 toggle:1"); |
514 | addAction_Widget(d->root, '1', rightSidebar_KeyModifier, "sidebar2.mode arg:0 toggle:1"); | ||
515 | addAction_Widget(d->root, '2', rightSidebar_KeyModifier, "sidebar2.mode arg:1 toggle:1"); | ||
516 | addAction_Widget(d->root, '3', rightSidebar_KeyModifier, "sidebar2.mode arg:2 toggle:1"); | ||
517 | addAction_Widget(d->root, '4', rightSidebar_KeyModifier, "sidebar2.mode arg:3 toggle:1"); | ||
518 | addAction_Widget(d->root, '5', rightSidebar_KeyModifier, "sidebar2.mode arg:4 toggle:1"); | ||
510 | } | 519 | } |
511 | } | 520 | } |
512 | 521 | ||