diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | .gitmodules | 3 | ||||
-rw-r--r-- | CMakeLists.txt | 37 | ||||
-rw-r--r-- | README.md | 28 | ||||
-rw-r--r-- | lagrange_about.png | bin | 493681 -> 492436 bytes | |||
m--------- | lib/the_Foundation | 0 | ||||
-rw-r--r-- | res/about/help.gmi | 173 | ||||
-rw-r--r-- | res/about/license.gmi | 105 | ||||
-rw-r--r-- | res/about/version.gmi | 15 | ||||
-rw-r--r-- | res/urlopen.bat | 2 | ||||
-rw-r--r-- | sdl2-macos-mouse-scrolling-patch.diff | 28 | ||||
-rw-r--r-- | src/app.c | 70 | ||||
-rw-r--r-- | src/app.h | 4 | ||||
-rw-r--r-- | src/bookmarks.c | 7 | ||||
-rw-r--r-- | src/bookmarks.h | 2 | ||||
-rw-r--r-- | src/gmdocument.c | 84 | ||||
-rw-r--r-- | src/gmdocument.h | 10 | ||||
-rw-r--r-- | src/gmrequest.c | 4 | ||||
-rw-r--r-- | src/gmutil.c | 6 | ||||
-rw-r--r-- | src/macos.m | 145 | ||||
-rw-r--r-- | src/ui/color.h | 3 | ||||
-rw-r--r-- | src/ui/documentwidget.c | 268 | ||||
-rw-r--r-- | src/ui/inputwidget.c | 2 | ||||
-rw-r--r-- | src/ui/labelwidget.c | 49 | ||||
-rw-r--r-- | src/ui/lookupwidget.c | 6 | ||||
-rw-r--r-- | src/ui/util.c | 80 | ||||
-rw-r--r-- | src/ui/util.h | 1 | ||||
-rw-r--r-- | src/ui/visbuf.c | 1 | ||||
-rw-r--r-- | src/ui/widget.c | 9 | ||||
-rw-r--r-- | src/ui/widget.h | 1 | ||||
-rw-r--r-- | src/ui/window.c | 69 | ||||
-rw-r--r-- | src/ui/window.h | 1 |
32 files changed, 853 insertions, 362 deletions
@@ -4,5 +4,5 @@ | |||
4 | build-* | 4 | build-* |
5 | /.vsbuild | 5 | /.vsbuild |
6 | /.vscode | 6 | /.vscode |
7 | /lib | 7 | |
8 | 8 | ||
diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..f869c7ae --- /dev/null +++ b/.gitmodules | |||
@@ -0,0 +1,3 @@ | |||
1 | [submodule "lib/the_Foundation"] | ||
2 | path = lib/the_Foundation | ||
3 | url = https://git.skyjake.fi/skyjake/the_Foundation | ||
diff --git a/CMakeLists.txt b/CMakeLists.txt index 8065cd25..b18ead27 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt | |||
@@ -1,20 +1,40 @@ | |||
1 | # Lagrange - A Beautiful Gemini Client | ||
2 | # Copyright: 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
3 | # | ||
4 | # Notes: | ||
5 | # - Required dependencies: SDL 2, OpenSSL 1.1.1, libpcre, | ||
6 | # GNU libunistring, zlib. | ||
7 | # - the_Foundation is built as a static library from 'lib/the_Foundation', | ||
8 | # if it exists in that location. The Git repository has it as a submodule. | ||
9 | # - Windows builds require MSYS2. In theory, Clang could be set up on | ||
10 | # Windows for compiling everything, but the_Foundation still lacks | ||
11 | # native Win32 implementations for the Socket and Process classes. | ||
12 | # - Windows builds should use the SDL 2 library precompiled for native | ||
13 | # Windows (MSVC variant) instead the version from MSYS2 (get it from | ||
14 | # https://libsdl.org/). To make configuration easier, consider writing | ||
15 | # for your personal use a pkg-config sdl2.pc file that uses the Windows | ||
16 | # version of the library. | ||
17 | # - `cat` is relied upon for merging all the resource files together. | ||
18 | |||
1 | cmake_minimum_required (VERSION 3.9) | 19 | cmake_minimum_required (VERSION 3.9) |
2 | set (CMAKE_OSX_DEPLOYMENT_TARGET 10.14) | 20 | set (CMAKE_OSX_DEPLOYMENT_TARGET 10.14) |
3 | 21 | ||
4 | project (Lagrange | 22 | project (Lagrange |
5 | VERSION 0.1.1 | 23 | VERSION 0.2.0 |
6 | DESCRIPTION "Beautiful Gemini Client" | 24 | DESCRIPTION "Beautiful Gemini Client" |
7 | LANGUAGES C | 25 | LANGUAGES C |
8 | ) | 26 | ) |
9 | set (COPYRIGHT_YEAR 2020) | 27 | set (COPYRIGHT_YEAR 2020) |
10 | 28 | ||
11 | # Build configuration. | 29 | # Build configuration. |
30 | option (ENABLE_X11_SWRENDER "Use software rendering under X11" OFF) | ||
12 | option (ENABLE_KERNING "Enable kerning in font renderer (slower)" ON) | 31 | option (ENABLE_KERNING "Enable kerning in font renderer (slower)" ON) |
13 | option (ENABLE_RESOURCE_EMBED "Embed resources inside the executable" OFF) | 32 | option (ENABLE_RESOURCE_EMBED "Embed resources inside the executable" OFF) |
33 | option (ENABLE_WINDOWPOS_FIX "Set position after showing window (workaround for SDL bug)" OFF) | ||
14 | 34 | ||
15 | include (BuildType.cmake) | 35 | include (BuildType.cmake) |
16 | include (Embed.cmake) | 36 | include (Embed.cmake) |
17 | if (NOT EXISTS ${CMAKE_SOURCE_DIR}/lib/the_Foundation) | 37 | if (NOT EXISTS ${CMAKE_SOURCE_DIR}/lib/the_Foundation/CMakeLists.txt) |
18 | set (INSTALL_THE_FOUNDATION YES) | 38 | set (INSTALL_THE_FOUNDATION YES) |
19 | find_package (the_Foundation REQUIRED) | 39 | find_package (the_Foundation REQUIRED) |
20 | else () | 40 | else () |
@@ -35,6 +55,7 @@ message (STATUS "Preparing embedded resources...") | |||
35 | set (EMBED_RESOURCES | 55 | set (EMBED_RESOURCES |
36 | res/about/help.gmi | 56 | res/about/help.gmi |
37 | res/about/lagrange.gmi | 57 | res/about/lagrange.gmi |
58 | res/about/license.gmi | ||
38 | res/about/version.gmi | 59 | res/about/version.gmi |
39 | res/FiraMono-Regular.ttf | 60 | res/FiraMono-Regular.ttf |
40 | res/FiraSans-Bold.ttf | 61 | res/FiraSans-Bold.ttf |
@@ -151,8 +172,14 @@ target_compile_options (app PUBLIC | |||
151 | ${SDL2_CFLAGS} | 172 | ${SDL2_CFLAGS} |
152 | ) | 173 | ) |
153 | target_compile_definitions (app PUBLIC LAGRANGE_APP_VERSION="${PROJECT_VERSION}") | 174 | target_compile_definitions (app PUBLIC LAGRANGE_APP_VERSION="${PROJECT_VERSION}") |
175 | if (ENABLE_X11_SWRENDER) | ||
176 | target_compile_definitions (app PUBLIC LAGRANGE_ENABLE_X11_SWRENDER=1) | ||
177 | endif () | ||
154 | if (ENABLE_KERNING) | 178 | if (ENABLE_KERNING) |
155 | target_compile_definitions (app PUBLIC LAGRANGE_ENABLE_KERNING=1) | 179 | target_compile_definitions (app PUBLIC LAGRANGE_ENABLE_KERNING=1) |
180 | if (ENABLE_WINDOWPOS_FIX) | ||
181 | endif () | ||
182 | target_compile_definitions (app PUBLIC LAGRANGE_ENABLE_WINDOWPOS_FIX=1) | ||
156 | endif () | 183 | endif () |
157 | target_link_libraries (app PUBLIC the_Foundation::the_Foundation) | 184 | target_link_libraries (app PUBLIC the_Foundation::the_Foundation) |
158 | target_link_libraries (app PUBLIC ${SDL2_LDFLAGS}) | 185 | target_link_libraries (app PUBLIC ${SDL2_LDFLAGS}) |
@@ -188,7 +215,11 @@ if (MSYS) | |||
188 | if (NOT ENABLE_RESOURCE_EMBED) | 215 | if (NOT ENABLE_RESOURCE_EMBED) |
189 | install (FILES ${EMB_BIN} DESTINATION .) | 216 | install (FILES ${EMB_BIN} DESTINATION .) |
190 | endif () | 217 | endif () |
191 | install (PROGRAMS ${SDL2_LIBDIR}/SDL2.dll DESTINATION .) | 218 | install (PROGRAMS |
219 | ${SDL2_LIBDIR}/SDL2.dll | ||
220 | res/urlopen.bat | ||
221 | DESTINATION . | ||
222 | ) | ||
192 | if (INSTALL_THE_FOUNDATION) | 223 | if (INSTALL_THE_FOUNDATION) |
193 | install (PROGRAMS $<TARGET_FILE:the_Foundation::the_Foundation> DESTINATION .) | 224 | install (PROGRAMS $<TARGET_FILE:the_Foundation::the_Foundation> DESTINATION .) |
194 | endif () | 225 | endif () |
@@ -24,11 +24,11 @@ Prebuilt binaries for Windows and macOS can be found in [Releases][rel]. | |||
24 | 24 | ||
25 | This is how to build Lagrange in a Unix-like environment. The required tools are a C11 compiler (e.g., Clang or GCC), CMake and `pkg-config`. | 25 | This is how to build Lagrange in a Unix-like environment. The required tools are a C11 compiler (e.g., Clang or GCC), CMake and `pkg-config`. |
26 | 26 | ||
27 | 1. Download and extract a source tarball from [Releases][rel]. (If you just clone this Git repository, [the_Foundation][tf] is expected to be already available on the system.) | 27 | 1. Download and extract a source tarball from [Releases][rel]. Alternatively, you may also clone the repository and its submodules: `git clone --recursive --branch release https://git.skyjake.fi/skyjake/lagrange` |
28 | 2. Check that you have the dependencies installed: SDL2, OpenSSL, libpcre, zlib, libunistring. For example, on macOS this would do the trick (using Homebrew): ```brew install sdl2 openssl@1.1 pcre libunistring``` Or on Ubuntu: ```sudo apt install libsdl2-dev libssl-dev libpcre3-dev zlib1g-dev libunistring-dev``` | 28 | 2. Check that you have the dependencies installed: CMake, SDL 2, OpenSSL 1.1.1, libpcre, zlib, libunistring. For example, on macOS this would do the trick (using Homebrew): ```brew install cmake sdl2 openssl@1.1 pcre libunistring``` Or on Ubuntu: ```sudo apt install cmake libsdl2-dev libssl-dev libpcre3-dev zlib1g-dev libunistring-dev``` |
29 | 3. Create a build directory. | 29 | 3. Create a build directory. |
30 | 4. In your empty build directory, run CMake: ```cmake {path_of_lagrange_sources}``` | 30 | 4. In your empty build directory, run CMake: ```cmake {path_of_lagrange_sources} -DCMAKE_BUILD_TYPE=Release``` |
31 | 5. Built it: ```cmake --build .``` | 31 | 5. Build it: ```cmake --build .``` |
32 | 6. Now you can run `lagrange`, `lagrange.exe`, or `Lagrange.app`. | 32 | 6. Now you can run `lagrange`, `lagrange.exe`, or `Lagrange.app`. |
33 | 33 | ||
34 | ### Installing to a directory | 34 | ### Installing to a directory |
@@ -40,5 +40,25 @@ To install to "/dest/path": | |||
40 | 40 | ||
41 | This will also install an XDG .desktop file for launching the app. | 41 | This will also install an XDG .desktop file for launching the app. |
42 | 42 | ||
43 | ### macOS-specific notes | ||
44 | |||
45 | When using OpenSSL 1.1.1 from Homebrew, you must add its pkgconfig path to your `PKG_CONFIG_PATH` environment variable, for example: | ||
46 | |||
47 | export PKG_CONFIG_PATH=/usr/local/Cellar/openssl@1.1/1.1.1g/lib/pkgconfig | ||
48 | |||
49 | Also, SDL's trackpad scrolling behavior on macOS is not optimal for regular GUI apps because it emulates a physical mouse wheel. This may change in a future release of SDL, but at least in 2.0.12 a [small patch](https://git.skyjake.fi/skyjake/lagrange/raw/branch/dev/sdl2-macos-mouse-scrolling-patch.diff) is required to allow momentum scrolling to come through as single-pixel mouse wheel events. | ||
50 | |||
51 | ### Raspberry Pi notes | ||
52 | |||
53 | You should use a version of SDL that is compiled to take advantage of the Broadcom VideoCore OpenGL ES hardware. This provides the best performance when running Lagrange in a console. | ||
54 | |||
55 | When running under X11, software rendering is the best choice and in that case the SDL from Raspbian etc. is sufficient. | ||
56 | |||
57 | The following build options are recommended on Raspberry Pi: | ||
58 | |||
59 | * `ENABLE_KERNING=NO`: faster text rendering without noticeable loss of quality | ||
60 | * `ENABLE_WINDOWPOS_FIX=YES`: workaround for window position restore issues (SDL bug) | ||
61 | * `ENABLE_X11_SWRENDER=YES`: use software rendering under X11 | ||
62 | |||
43 | [rel]: https://git.skyjake.fi/skyjake/lagrange/releases | 63 | [rel]: https://git.skyjake.fi/skyjake/lagrange/releases |
44 | [tf]: https://git.skyjake.fi/skyjake/the_Foundation | 64 | [tf]: https://git.skyjake.fi/skyjake/the_Foundation |
diff --git a/lagrange_about.png b/lagrange_about.png index d342262b..7867344d 100644 --- a/lagrange_about.png +++ b/lagrange_about.png | |||
Binary files differ | |||
diff --git a/lib/the_Foundation b/lib/the_Foundation new file mode 160000 | |||
Subproject ab8f6e0769db2ec74846beea0f36561b85bf2c7 | |||
diff --git a/res/about/help.gmi b/res/about/help.gmi index dccef9f8..ca5e164f 100644 --- a/res/about/help.gmi +++ b/res/about/help.gmi | |||
@@ -12,6 +12,7 @@ Lagrange is a GUI client for browsing Geminispace. It offers modern conveniences | |||
12 | 12 | ||
13 | Like Gemini, Lagrange has been designed with minimalism in mind. It depends on a small number of essential libraries. It is written in C and uses SDL for hardware-accelerated graphics. OpenSSL is used for secure communications. | 13 | Like Gemini, Lagrange has been designed with minimalism in mind. It depends on a small number of essential libraries. It is written in C and uses SDL for hardware-accelerated graphics. OpenSSL is used for secure communications. |
14 | 14 | ||
15 | => about:lagrange About Lagrange | ||
15 | => https://www.libsdl.org SDL: Simple DirectMedia Layer | 16 | => https://www.libsdl.org SDL: Simple DirectMedia Layer |
16 | => https://www.openssl.org OpenSSL: Cryptography and SSL/TLS Toolkit | 17 | => https://www.openssl.org OpenSSL: Cryptography and SSL/TLS Toolkit |
17 | 18 | ||
@@ -59,13 +60,17 @@ Lagrange's user interface is modeled after web browsers: | |||
59 | * There is a sidebar for managing bookmarks and TLS identities, and viewing history and the page outline. The sidebar is hidden by default. | 60 | * There is a sidebar for managing bookmarks and TLS identities, and viewing history and the page outline. The sidebar is hidden by default. |
60 | * There is a search bar that appears at the bottom when searching text on the page. | 61 | * There is a search bar that appears at the bottom when searching text on the page. |
61 | 62 | ||
63 | Tip: Try pressing ${CTRL+}4 now to see the page outline. | ||
64 | |||
62 | ## URL entry and quick search | 65 | ## URL entry and quick search |
63 | 66 | ||
64 | The URL input field is in its typical location in the navigation bar. It can be accessed quickly by pressing ${CTRL+}L. | 67 | The URL input field is in its typical location in the navigation bar. It can be accessed quickly by pressing ${CTRL+}L. |
65 | 68 | ||
66 | As you enter text, Lagrange starts looking for matches in bookmarks, history, content of cached pages, and identities. Search terms are case insensitive, and if many words are entered, they are all required to appear in the specified order in any matched content. Search of cached pages is limited to the (small) set of pages that Lagrange keeps in memory for back navigation. | 69 | As you enter text, Lagrange starts looking for matches in bookmarks, history, content of cached pages, and identities. Search results appear below the URL input field in a popup. Press Tab or ↓ to switch input focus to the results. |
70 | |||
71 | Search within cached pages is limited to the (small) set of pages that Lagrange keeps in memory for back navigation. Search terms are case insensitive, and if many words are entered, they are all required to appear in the specified order in any matched content. | ||
67 | 72 | ||
68 | Press Tab or ↓ to switch input focus to the search results. | 73 | Note that the navigation stack is saved to a file when Lagrange is shut down and restored on the next launch. This means the next time you launch Lagrange, you can still search the contents of past pages. However, navigation stacks are tab-specific, so closing a tab will delete its history as well. |
69 | 74 | ||
70 | ## Tabs | 75 | ## Tabs |
71 | 76 | ||
@@ -86,6 +91,8 @@ ${CTRL+}1 through ${CTRL+}4 switch between the sidebar tabs, or hide the sidebar | |||
86 | 91 | ||
87 | ## Navigation | 92 | ## Navigation |
88 | 93 | ||
94 | When navigating to a new page, the old page is cached in memory. If you navigate back, the cached copy of the page is restored. Think of it as rewinding time — you return to a past time as if nothing had happened. The same applies to forward navigation; cached pages are loaded if available. This allows back and forward navigation to happen instantly, without any network requests. | ||
95 | |||
89 | ### Link icons | 96 | ### Link icons |
90 | 97 | ||
91 | The type and destination of a link are indicated by the link's icon and color: ➤ links to the same domain, and 🌐 to a different domain. The colors are: | 98 | The type and destination of a link are indicated by the link's icon and color: ➤ links to the same domain, and 🌐 to a different domain. The colors are: |
@@ -103,17 +110,60 @@ When navigating via keyboard, hold down ${ALT} to see link shortcut keys. Try do | |||
103 | 110 | ||
104 | Each visible link on the page gets an alphanumeric shortcut. For example, the first link can be opened by pressing ${ALT+}1. The tenth link is ${ALT+}A. Additionally hold down ${CTRL} to open the link in a new tab. | 111 | Each visible link on the page gets an alphanumeric shortcut. For example, the first link can be opened by pressing ${ALT+}1. The tenth link is ${ALT+}A. Additionally hold down ${CTRL} to open the link in a new tab. |
105 | 112 | ||
113 | ## Downloads | ||
114 | |||
115 | Press ${CTRL+}S to save the contents of the current page to the Downloads folder. This works on all pages regardless of whether Lagrange can display the contents or not. | ||
116 | |||
117 | The location where downloaded files are saved can be set in Preferences. | ||
118 | |||
119 | The 🔃 button on the right side of the URL input field is the Reload/Stop button — it reloads the current page or stops an ongoing download. During large downloads, an additional progress indicator appears next to the Stop button. | ||
120 | |||
106 | ## Bookmarks | 121 | ## Bookmarks |
107 | 122 | ||
108 | ## Managing and using identities | 123 | Press ${CTRL+}D to bookmark the currently open URL. |
124 | |||
125 | In addition to a title, bookmarks can have tags. Some tags have a special meaning, but you are free to enter whatever you want in the tags field. In quick search results, tags are given extra weight so they appear higher in results. | ||
126 | |||
127 | ### Special tags | ||
128 | |||
129 | * Set a "homepage" tag on a bookmark to make it one of the pages that will be opened when pressing the 🏠 button. | ||
130 | |||
131 | ## Identities (TLS client certificates) | ||
132 | |||
133 | Gemini uses TLS client certificates for manual user/session identification purposes. This is analogous to logging into a web site, except you are in full control of the information. The term "Identity" is used in Lagrange to refer to client certificates. | ||
134 | |||
135 | Lagrange can easily create a new identity. The shortcut for this is ${SHIFT+}${CTRL+}N. Consider any information you enter in the certificate as public; only the Common Name is required and will appear as the issuer and subject of the certificate. | ||
136 | |||
137 | Clicking on an identity in the sidebar will toggle it on/off for the currently open URL. On subsequent page loads, the certificate will then be sent to the server when the URL or any URL under it is fetched. You can click on the 👤 button in the navigation bar to see which identity is being used for the current page. | ||
109 | 138 | ||
110 | TLS client certificates that you can identify yourself with. Consider any information you enter in the certificate as public; only the Common Name is required and will appear as the issuer and subject of the certificate. | 139 | ### Importing existing certificates |
140 | |||
141 | At launch, Lagrange looks through its "idents" directory to see if any new certificates have been copied there. (See "Runtime files" below for the location.) The file format must be PEM. Both a certificate (.crt) and its private key (.key) must be found in "idents" and they must have matching file names. For example: | ||
142 | * mycert.crt | ||
143 | * mycert.key | ||
144 | Lagrange will add a note to the imported identities to mark them as "Imported". | ||
145 | |||
146 | # OS integration | ||
147 | |||
148 | ## Opening Gemini URLs (macOS) | ||
149 | |||
150 | Lagrange registers itself as a "gemini:" URL scheme handler, so you can click on Gemini links in any application to open Lagrange with that URL. | ||
151 | |||
152 | Likewise, .gmi/.gemini file extensions are registered as file formats that Lagrange can view so Finder will know how to open those automatically using Lagrange. | ||
111 | 153 | ||
112 | ## Drop and drop | 154 | ## Drop and drop |
113 | 155 | ||
114 | You can drag and drop .gmi files on the Lagrange window to open them in the current tab. Dropping multiple files opens them in separate tabs. This is the recommended way to view local files, because there is no "Open File" menu item. You may also type "file://" URLs in the URL field. | 156 | You can drag and drop .gmi files on the Lagrange window to open them in the current tab. Dropping multiple files opens them in separate tabs. This is the recommended way to view local files, because there is no "Open File" menu item. You may also type "file://" URLs in the URL field. |
115 | 157 | ||
116 | # Runtime files | 158 | ## Runtime files |
159 | |||
160 | Lagrange stores user-specific persistent files in one of the following locations (depending on the operating system): | ||
161 | |||
162 | ``` | ||
163 | Windows : C:\Users\Name\AppData\Roaming\fi.skyjake.Lagrange\ | ||
164 | macOS : ~/Library/Application Support/fi.skyjake.Lagrange/ | ||
165 | Other Unix : ~/.config/lagrange/ | ||
166 | ``` | ||
117 | 167 | ||
118 | * bookmarks.txt | 168 | * bookmarks.txt |
119 | * idents.binary and idents/ | 169 | * idents.binary and idents/ |
@@ -122,108 +172,13 @@ You can drag and drop .gmi files on the Lagrange window to open them in the curr | |||
122 | * trusted.txt | 172 | * trusted.txt |
123 | * visited.txt | 173 | * visited.txt |
124 | 174 | ||
125 | # Open source licenses | 175 | ## Command line options |
126 | 176 | ||
127 | Lagrange itself is distributed under the BSD 2-clause license: | 177 | ### --echo |
128 | => https://opensource.org/licenses/BSD-2-Clause The 2-Clause BSD License | 178 | Debugging utility: internal events are printed to stdout. |
129 | => https://git.skyjake.fi/skyjake/lagrange.git Lagrange Git Repository | 179 | |
130 | 180 | ### --sw | |
131 | > Copyright 2020 Jaakko Keränen | 181 | Disable hardware accelerated graphics. Note that software rendering is anyway used as a fallback, so usually this option should not be necessary. |
132 | > | 182 | |
133 | > Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: | 183 | # Open source licenses |
134 | > | 184 | => about:license |
135 | > 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. | ||
136 | > | ||
137 | > 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. | ||
138 | > | ||
139 | > THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
140 | |||
141 | ## SDL | ||
142 | |||
143 | SDL 2.0 and newer are available under the zlib license: | ||
144 | => https://www.zlib.net/zlib_license.html ZLIB License | ||
145 | |||
146 | > This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. | ||
147 | > | ||
148 | > Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: | ||
149 | > | ||
150 | > 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. | ||
151 | > | ||
152 | > 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. | ||
153 | > | ||
154 | > 3. This notice may not be removed or altered from any source distribution. | ||
155 | |||
156 | ## the_Foundation | ||
157 | |||
158 | the_Foundation is an opinionated C11 library by Jaakko Keränen. It is under the BSD 2-clause license. Note that the rest of the libraries in this section are used by the_Foundation, and not directly by Lagrange. | ||
159 | => https://opensource.org/licenses/BSD-2-Clause The 2-Clause BSD License | ||
160 | => https://git.skyjake.fi/skyjake/the_Foundation.git the_Foundation Git Repository | ||
161 | |||
162 | ## OpenSSL | ||
163 | |||
164 | OpenSSL 1.1.1 is under a double license, which both apply to the library. | ||
165 | => https://www.openssl.org/source/license-openssl-ssleay.txt OpenSSL and SSLeay Licenses | ||
166 | |||
167 | > Copyright (c) 1998-2019 The OpenSSL Project. All rights reserved. | ||
168 | > | ||
169 | > Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: | ||
170 | > | ||
171 | > 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. | ||
172 | > | ||
173 | > 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. | ||
174 | > | ||
175 | > 3. All advertising materials mentioning features or use of this software must display the following acknowledgment: "This product includes software developed by the OpenSSL Project for use in the OpenSSL Toolkit. (http://www.openssl.org/)" | ||
176 | > | ||
177 | > 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to endorse or promote products derived from this software without prior written permission. For written permission, please contact openssl-core@openssl.org. | ||
178 | > | ||
179 | > 5. Products derived from this software may not be called "OpenSSL" nor may "OpenSSL" appear in their names without prior written permission of the OpenSSL Project. | ||
180 | > | ||
181 | > 6. Redistributions of any form whatsoever must retain the following acknowledgment: "This product includes software developed by the OpenSSL Project for use in the OpenSSL Toolkit (http://www.openssl.org/)" | ||
182 | > | ||
183 | > THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
184 | > | ||
185 | > This product includes cryptographic software written by Eric Young (eay@cryptsoft.com). This product includes software written by Tim Hudson (tjh@cryptsoft.com). | ||
186 | |||
187 | SSLeay license: | ||
188 | |||
189 | > Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com) | ||
190 | > All rights reserved. | ||
191 | > | ||
192 | > This package is an SSL implementation written by Eric Young (eay@cryptsoft.com). The implementation was written so as to conform with Netscapes SSL. | ||
193 | > | ||
194 | > This library is free for commercial and non-commercial use as long as the following conditions are aheared to. The following conditions apply to all code found in this distribution, be it the RC4, RSA, lhash, DES, etc., code; not just the SSL code. The SSL documentation included with this distribution is covered by the same copyright terms except that the holder is Tim Hudson (tjh@cryptsoft.com). | ||
195 | > | ||
196 | > Copyright remains Eric Young's, and as such any Copyright notices in the code are not to be removed. If this package is used in a product, Eric Young should be given attribution as the author of the parts of the library used. This can be in the form of a textual message at program startup or in documentation (online or textual) provided with the package. | ||
197 | > | ||
198 | > Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: | ||
199 | > | ||
200 | > 1. Redistributions of source code must retain the copyright notice, this list of conditions and the following disclaimer. | ||
201 | > | ||
202 | > 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. | ||
203 | > | ||
204 | > 3. All advertising materials mentioning features or use of this software must display the following acknowledgement: "This product includes cryptographic software written by Eric Young (eay@cryptsoft.com)" The word 'cryptographic' can be left out if the rouines from the library being used are not cryptographic related :-). | ||
205 | > | ||
206 | > 4. If you include any Windows specific code (or a derivative thereof) from the apps directory (application code) you must include an acknowledgement: "This product includes software written by Tim Hudson (tjh@cryptsoft.com)" | ||
207 | > | ||
208 | > THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
209 | > | ||
210 | > The licence and distribution terms for any publically available version or derivative of this code cannot be changed. i.e. this code cannot simply be copied and put under another distribution licence [including the GNU Public Licence.] | ||
211 | |||
212 | ## GNU libunistring | ||
213 | |||
214 | The libunistring library is covered by the GNU Lesser General Public License (LGPL): | ||
215 | => https://www.gnu.org/software/libunistring/manual/libunistring.html#GNU-LGPL GNU LGPL License | ||
216 | |||
217 | ## Fonts | ||
218 | |||
219 | This application uses fonts licensed under the Open Font License. | ||
220 | |||
221 | => https://github.com/mozilla/Fira/blob/master/LICENSE Fira Sans, Fira Mono | ||
222 | => https://github.com/googlefonts/nunito/blob/master/OFL.txt Nunito | ||
223 | => https://github.com/adobe-fonts/source-sans-pro/blob/release/LICENSE.md Source Sans Pro | ||
224 | |||
225 | Additional fonts: | ||
226 | |||
227 | => https://fonts.google.com/specimen/Kosugi+Maru#license Kosugi Maru (Apache License 2.0) | ||
228 | => https://github.com/googlefonts/noto-emoji/blob/master/LICENSE Noto Emoji (Apache License 2.0) | ||
229 | => https://dn-works.com/ufas/ Symbola (Public Domain) \ No newline at end of file | ||
diff --git a/res/about/license.gmi b/res/about/license.gmi new file mode 100644 index 00000000..4f654540 --- /dev/null +++ b/res/about/license.gmi | |||
@@ -0,0 +1,105 @@ | |||
1 | # Open source licenses | ||
2 | |||
3 | Lagrange is distributed under the BSD 2-clause license. | ||
4 | => https://opensource.org/licenses/BSD-2-Clause The 2-Clause BSD License | ||
5 | => https://git.skyjake.fi/skyjake/lagrange.git Lagrange Git Repository | ||
6 | |||
7 | > Copyright 2020 Jaakko Keränen | ||
8 | > | ||
9 | > Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: | ||
10 | > | ||
11 | > 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. | ||
12 | > | ||
13 | > 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. | ||
14 | > | ||
15 | > THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
16 | |||
17 | ## SDL | ||
18 | |||
19 | SDL 2.0 and newer are available under the zlib license: | ||
20 | => https://www.zlib.net/zlib_license.html ZLIB License | ||
21 | |||
22 | > This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. | ||
23 | > | ||
24 | > Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: | ||
25 | > | ||
26 | > 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. | ||
27 | > | ||
28 | > 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. | ||
29 | > | ||
30 | > 3. This notice may not be removed or altered from any source distribution. | ||
31 | |||
32 | ## the_Foundation | ||
33 | |||
34 | the_Foundation is an opinionated C11 library by Jaakko Keränen. It is under the BSD 2-clause license. Note that the rest of the libraries in this section are used by the_Foundation, and not directly by Lagrange. | ||
35 | => https://opensource.org/licenses/BSD-2-Clause The 2-Clause BSD License | ||
36 | => https://git.skyjake.fi/skyjake/the_Foundation.git the_Foundation Git Repository | ||
37 | |||
38 | ## OpenSSL | ||
39 | |||
40 | OpenSSL 1.1.1 is under a double license, which both apply to the library. | ||
41 | => https://www.openssl.org/source/license-openssl-ssleay.txt OpenSSL and SSLeay Licenses | ||
42 | |||
43 | > Copyright (c) 1998-2019 The OpenSSL Project. All rights reserved. | ||
44 | > | ||
45 | > Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: | ||
46 | > | ||
47 | > 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. | ||
48 | > | ||
49 | > 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. | ||
50 | > | ||
51 | > 3. All advertising materials mentioning features or use of this software must display the following acknowledgment: "This product includes software developed by the OpenSSL Project for use in the OpenSSL Toolkit. (http://www.openssl.org/)" | ||
52 | > | ||
53 | > 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to endorse or promote products derived from this software without prior written permission. For written permission, please contact openssl-core@openssl.org. | ||
54 | > | ||
55 | > 5. Products derived from this software may not be called "OpenSSL" nor may "OpenSSL" appear in their names without prior written permission of the OpenSSL Project. | ||
56 | > | ||
57 | > 6. Redistributions of any form whatsoever must retain the following acknowledgment: "This product includes software developed by the OpenSSL Project for use in the OpenSSL Toolkit (http://www.openssl.org/)" | ||
58 | > | ||
59 | > THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
60 | > | ||
61 | > This product includes cryptographic software written by Eric Young (eay@cryptsoft.com). This product includes software written by Tim Hudson (tjh@cryptsoft.com). | ||
62 | |||
63 | SSLeay license: | ||
64 | |||
65 | > Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com) | ||
66 | > All rights reserved. | ||
67 | > | ||
68 | > This package is an SSL implementation written by Eric Young (eay@cryptsoft.com). The implementation was written so as to conform with Netscapes SSL. | ||
69 | > | ||
70 | > This library is free for commercial and non-commercial use as long as the following conditions are aheared to. The following conditions apply to all code found in this distribution, be it the RC4, RSA, lhash, DES, etc., code; not just the SSL code. The SSL documentation included with this distribution is covered by the same copyright terms except that the holder is Tim Hudson (tjh@cryptsoft.com). | ||
71 | > | ||
72 | > Copyright remains Eric Young's, and as such any Copyright notices in the code are not to be removed. If this package is used in a product, Eric Young should be given attribution as the author of the parts of the library used. This can be in the form of a textual message at program startup or in documentation (online or textual) provided with the package. | ||
73 | > | ||
74 | > Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: | ||
75 | > | ||
76 | > 1. Redistributions of source code must retain the copyright notice, this list of conditions and the following disclaimer. | ||
77 | > | ||
78 | > 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. | ||
79 | > | ||
80 | > 3. All advertising materials mentioning features or use of this software must display the following acknowledgement: "This product includes cryptographic software written by Eric Young (eay@cryptsoft.com)" The word 'cryptographic' can be left out if the rouines from the library being used are not cryptographic related :-). | ||
81 | > | ||
82 | > 4. If you include any Windows specific code (or a derivative thereof) from the apps directory (application code) you must include an acknowledgement: "This product includes software written by Tim Hudson (tjh@cryptsoft.com)" | ||
83 | > | ||
84 | > THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
85 | > | ||
86 | > The licence and distribution terms for any publically available version or derivative of this code cannot be changed. i.e. this code cannot simply be copied and put under another distribution licence [including the GNU Public Licence.] | ||
87 | |||
88 | ## GNU libunistring | ||
89 | |||
90 | The libunistring library is covered by the GNU Lesser General Public License (LGPL): | ||
91 | => https://www.gnu.org/software/libunistring/manual/libunistring.html#GNU-LGPL GNU LGPL License | ||
92 | |||
93 | ## Fonts | ||
94 | |||
95 | This application uses fonts licensed under the Open Font License. | ||
96 | |||
97 | => https://github.com/mozilla/Fira/blob/master/LICENSE Fira Sans, Fira Mono | ||
98 | => https://github.com/googlefonts/nunito/blob/master/OFL.txt Nunito | ||
99 | => https://github.com/adobe-fonts/source-sans-pro/blob/release/LICENSE.md Source Sans Pro | ||
100 | |||
101 | Additional fonts: | ||
102 | |||
103 | => https://fonts.google.com/specimen/Kosugi+Maru#license Kosugi Maru (Apache License 2.0) | ||
104 | => https://github.com/googlefonts/noto-emoji/blob/master/LICENSE Noto Emoji (Apache License 2.0) | ||
105 | => https://dn-works.com/ufas/ Symbola (Public Domain) | ||
diff --git a/res/about/version.gmi b/res/about/version.gmi index 2bbf673a..32f1e49f 100644 --- a/res/about/version.gmi +++ b/res/about/version.gmi | |||
@@ -6,6 +6,21 @@ | |||
6 | ``` | 6 | ``` |
7 | # Release notes | 7 | # Release notes |
8 | 8 | ||
9 | ## 0.2 | ||
10 | * Added an icon for quote paragraphs. | ||
11 | * Added Downloads folder to Preferences. | ||
12 | * Added "Save to Downloads" menu item (${CTRL+}S) for saving page contents. | ||
13 | * Added a download progress indicator in the URL input field. | ||
14 | * Added a progress indicator for inline image fetching. | ||
15 | * Added `--sw` option to force software rendering. | ||
16 | * Added macOS touch bar buttons for Back, Forward, Find, New Tab, and sidebar modes. | ||
17 | * Home button opens a random bookmark with the "homepage" tag. | ||
18 | * Improved context menu when right-clicking on links or the page. | ||
19 | * Recognize and handle "mailto:" links. | ||
20 | * Fixed behavior of images on single-image pages; cannot be hidden like inline images. | ||
21 | * Fall back to software rendering automatically if accelerated graphics are not available. | ||
22 | * Minor bug fixes. | ||
23 | |||
9 | ## 0.1.1 | 24 | ## 0.1.1 |
10 | * Fixed a potential crash at startup. | 25 | * Fixed a potential crash at startup. |
11 | * Fixed bug where user's query input is handled by all tabs. | 26 | * Fixed bug where user's query input is handled by all tabs. |
diff --git a/res/urlopen.bat b/res/urlopen.bat new file mode 100644 index 00000000..82935824 --- /dev/null +++ b/res/urlopen.bat | |||
@@ -0,0 +1,2 @@ | |||
1 | @echo off | ||
2 | start %1 | ||
diff --git a/sdl2-macos-mouse-scrolling-patch.diff b/sdl2-macos-mouse-scrolling-patch.diff new file mode 100644 index 00000000..633243ed --- /dev/null +++ b/sdl2-macos-mouse-scrolling-patch.diff | |||
@@ -0,0 +1,28 @@ | |||
1 | diff -r 4f06c06b6d19 src/events/SDL_mouse.c | ||
2 | --- a/src/events/SDL_mouse.c Wed Aug 05 15:28:51 2020 +0200 | ||
3 | +++ b/src/events/SDL_mouse.c Tue Sep 15 07:54:17 2020 +0300 | ||
4 | @@ -642,8 +642,8 @@ | ||
5 | event.wheel.preciseX = x; | ||
6 | event.wheel.preciseY = y; | ||
7 | #endif | ||
8 | - event.wheel.x = integral_x; | ||
9 | - event.wheel.y = integral_y; | ||
10 | + event.wheel.x = x; //integral_x; | ||
11 | + event.wheel.y = y; //integral_y; | ||
12 | event.wheel.direction = (Uint32)direction; | ||
13 | posted = (SDL_PushEvent(&event) > 0); | ||
14 | } | ||
15 | diff -r 4f06c06b6d19 src/video/cocoa/SDL_cocoamouse.m | ||
16 | --- a/src/video/cocoa/SDL_cocoamouse.m Wed Aug 05 15:28:51 2020 +0200 | ||
17 | +++ b/src/video/cocoa/SDL_cocoamouse.m Tue Sep 15 07:54:17 2020 +0300 | ||
18 | @@ -424,8 +424,8 @@ | ||
19 | } | ||
20 | |||
21 | SDL_MouseID mouseID = mouse->mouseID; | ||
22 | - CGFloat x = -[event deltaX]; | ||
23 | - CGFloat y = [event deltaY]; | ||
24 | + CGFloat x = -[event scrollingDeltaX]; | ||
25 | + CGFloat y = [event scrollingDeltaY]; | ||
26 | SDL_MouseWheelDirection direction = SDL_MOUSEWHEEL_NORMAL; | ||
27 | |||
28 | if ([event respondsToSelector:@selector(isDirectionInvertedFromDevice)]) { | ||
@@ -73,8 +73,9 @@ static const char *dataDir_App_ = "~/AppData/Roaming/fi.skyjake.Lagrange"; | |||
73 | static const char *dataDir_App_ = "~/.config/lagrange"; | 73 | static const char *dataDir_App_ = "~/.config/lagrange"; |
74 | #endif | 74 | #endif |
75 | #define EMB_BIN2 "../resources.binary" /* fallback from build/executable dir */ | 75 | #define EMB_BIN2 "../resources.binary" /* fallback from build/executable dir */ |
76 | static const char *prefsFileName_App_ = "prefs.cfg"; | 76 | static const char *prefsFileName_App_ = "prefs.cfg"; |
77 | static const char *stateFileName_App_ = "state.binary"; | 77 | static const char *stateFileName_App_ = "state.binary"; |
78 | static const char *downloadDir_App_ = "~/Downloads"; | ||
78 | 79 | ||
79 | struct Impl_App { | 80 | struct Impl_App { |
80 | iCommandLine args; | 81 | iCommandLine args; |
@@ -98,10 +99,12 @@ struct Impl_App { | |||
98 | float uiScale; | 99 | float uiScale; |
99 | int zoomPercent; | 100 | int zoomPercent; |
100 | iBool forceWrap; | 101 | iBool forceWrap; |
102 | iBool forceSoftwareRender; | ||
101 | enum iColorTheme theme; | 103 | enum iColorTheme theme; |
102 | iBool useSystemTheme; | 104 | iBool useSystemTheme; |
103 | iString gopherProxy; | 105 | iString gopherProxy; |
104 | iString httpProxy; | 106 | iString httpProxy; |
107 | iString downloadDir; | ||
105 | }; | 108 | }; |
106 | 109 | ||
107 | static iApp app_; | 110 | static iApp app_; |
@@ -160,6 +163,7 @@ static iString *serializePrefs_App_(const iApp *d) { | |||
160 | appendFormat_String(str, "ostheme arg:%d\n", d->useSystemTheme); | 163 | appendFormat_String(str, "ostheme arg:%d\n", d->useSystemTheme); |
161 | appendFormat_String(str, "proxy.gopher address:%s\n", cstr_String(&d->gopherProxy)); | 164 | appendFormat_String(str, "proxy.gopher address:%s\n", cstr_String(&d->gopherProxy)); |
162 | appendFormat_String(str, "proxy.http address:%s\n", cstr_String(&d->httpProxy)); | 165 | appendFormat_String(str, "proxy.http address:%s\n", cstr_String(&d->httpProxy)); |
166 | appendFormat_String(str, "downloads path:%s\n", cstr_String(&d->downloadDir)); | ||
163 | return str; | 167 | return str; |
164 | } | 168 | } |
165 | 169 | ||
@@ -304,12 +308,14 @@ static void init_App_(iApp *d, int argc, char **argv) { | |||
304 | d->pendingRefresh = iFalse; | 308 | d->pendingRefresh = iFalse; |
305 | d->zoomPercent = 100; | 309 | d->zoomPercent = 100; |
306 | d->forceWrap = iFalse; | 310 | d->forceWrap = iFalse; |
311 | d->forceSoftwareRender = checkArgument_CommandLine(&d->args, "sw") != NULL; | ||
307 | d->certs = new_GmCerts(dataDir_App_); | 312 | d->certs = new_GmCerts(dataDir_App_); |
308 | d->visited = new_Visited(); | 313 | d->visited = new_Visited(); |
309 | d->bookmarks = new_Bookmarks(); | 314 | d->bookmarks = new_Bookmarks(); |
310 | d->tabEnum = 0; /* generates unique IDs for tab pages */ | 315 | d->tabEnum = 0; /* generates unique IDs for tab pages */ |
311 | init_String(&d->gopherProxy); | 316 | init_String(&d->gopherProxy); |
312 | init_String(&d->httpProxy); | 317 | init_String(&d->httpProxy); |
318 | initCStr_String(&d->downloadDir, downloadDir_App_); | ||
313 | setThemePalette_Color(d->theme); | 319 | setThemePalette_Color(d->theme); |
314 | #if defined (iPlatformApple) | 320 | #if defined (iPlatformApple) |
315 | setupApplication_MacOS(); | 321 | setupApplication_MacOS(); |
@@ -384,6 +390,7 @@ static void init_App_(iApp *d, int argc, char **argv) { | |||
384 | static void deinit_App(iApp *d) { | 390 | static void deinit_App(iApp *d) { |
385 | saveState_App_(d); | 391 | saveState_App_(d); |
386 | savePrefs_App_(d); | 392 | savePrefs_App_(d); |
393 | deinit_String(&d->downloadDir); | ||
387 | deinit_String(&d->httpProxy); | 394 | deinit_String(&d->httpProxy); |
388 | deinit_String(&d->gopherProxy); | 395 | deinit_String(&d->gopherProxy); |
389 | save_Bookmarks(d->bookmarks, dataDir_App_); | 396 | save_Bookmarks(d->bookmarks, dataDir_App_); |
@@ -406,6 +413,10 @@ const iString *dataDir_App(void) { | |||
406 | return collect_String(cleanedCStr_Path(dataDir_App_)); | 413 | return collect_String(cleanedCStr_Path(dataDir_App_)); |
407 | } | 414 | } |
408 | 415 | ||
416 | const iString *downloadDir_App(void) { | ||
417 | return collect_String(cleaned_Path(&app_.downloadDir)); | ||
418 | } | ||
419 | |||
409 | const iString *debugInfo_App(void) { | 420 | const iString *debugInfo_App(void) { |
410 | iApp *d = &app_; | 421 | iApp *d = &app_; |
411 | iString *msg = collectNew_String(); | 422 | iString *msg = collectNew_String(); |
@@ -531,8 +542,20 @@ int zoom_App(void) { | |||
531 | return app_.zoomPercent; | 542 | return app_.zoomPercent; |
532 | } | 543 | } |
533 | 544 | ||
534 | iBool isLineWrapForced_App(void) { | 545 | iBool forceLineWrap_App(void) { |
535 | return app_.forceWrap; | 546 | return app_.forceWrap; |
547 | iBool forceSoftwareRender_App(void) { | ||
548 | } | ||
549 | |||
550 | if (app_.forceSoftwareRender) { | ||
551 | return iTrue; | ||
552 | } | ||
553 | #if defined (LAGRANGE_ENABLE_X11_SWRENDER) | ||
554 | if (getenv("DISPLAY")) { | ||
555 | return iTrue; | ||
556 | } | ||
557 | #endif | ||
558 | return iFalse; | ||
536 | } | 559 | } |
537 | 560 | ||
538 | enum iColorTheme colorTheme_App(void) { | 561 | enum iColorTheme colorTheme_App(void) { |
@@ -644,6 +667,8 @@ static iBool handlePrefsCommands_(iWidget *d, const char *cmd) { | |||
644 | if (equal_Command(cmd, "prefs.dismiss") || equal_Command(cmd, "preferences")) { | 667 | if (equal_Command(cmd, "prefs.dismiss") || equal_Command(cmd, "preferences")) { |
645 | setUiScale_Window(get_Window(), | 668 | setUiScale_Window(get_Window(), |
646 | toFloat_String(text_InputWidget(findChild_Widget(d, "prefs.uiscale")))); | 669 | toFloat_String(text_InputWidget(findChild_Widget(d, "prefs.uiscale")))); |
670 | postCommandf_App("downloads path:%s", | ||
671 | cstr_String(text_InputWidget(findChild_Widget(d, "prefs.downloads")))); | ||
647 | postCommandf_App("window.retain arg:%d", | 672 | postCommandf_App("window.retain arg:%d", |
648 | isSelected_Widget(findChild_Widget(d, "prefs.retainwindow"))); | 673 | isSelected_Widget(findChild_Widget(d, "prefs.retainwindow"))); |
649 | postCommandf_App("ostheme arg:%d", | 674 | postCommandf_App("ostheme arg:%d", |
@@ -777,12 +802,17 @@ iBool handleCommand_App(const char *cmd) { | |||
777 | d->retainWindowSize = arg_Command(cmd); | 802 | d->retainWindowSize = arg_Command(cmd); |
778 | return iTrue; | 803 | return iTrue; |
779 | } | 804 | } |
805 | else if (equal_Command(cmd, "downloads")) { | ||
806 | setCStr_String(&d->downloadDir, suffixPtr_Command(cmd, "path")); | ||
807 | return iTrue; | ||
808 | } | ||
780 | else if (equal_Command(cmd, "open")) { | 809 | else if (equal_Command(cmd, "open")) { |
781 | const iString *url = collectNewCStr_String(suffixPtr_Command(cmd, "url")); | 810 | const iString *url = collectNewCStr_String(suffixPtr_Command(cmd, "url")); |
782 | iUrl parts; | 811 | iUrl parts; |
783 | init_Url(&parts, url); | 812 | init_Url(&parts, url); |
784 | if (isEmpty_String(&d->httpProxy) && | 813 | if (equalCase_Rangecc(parts.scheme, "mailto") || |
785 | (equalCase_Rangecc(parts.scheme, "http") || equalCase_Rangecc(parts.scheme, "https"))) { | 814 | (isEmpty_String(&d->httpProxy) && (equalCase_Rangecc(parts.scheme, "http") || |
815 | equalCase_Rangecc(parts.scheme, "https")))) { | ||
786 | openInDefaultBrowser_App(url); | 816 | openInDefaultBrowser_App(url); |
787 | return iTrue; | 817 | return iTrue; |
788 | } | 818 | } |
@@ -872,6 +902,7 @@ iBool handleCommand_App(const char *cmd) { | |||
872 | else if (equal_Command(cmd, "preferences")) { | 902 | else if (equal_Command(cmd, "preferences")) { |
873 | iWidget *dlg = makePreferences_Widget(); | 903 | iWidget *dlg = makePreferences_Widget(); |
874 | updatePrefsThemeButtons_(dlg); | 904 | updatePrefsThemeButtons_(dlg); |
905 | setText_InputWidget(findChild_Widget(dlg, "prefs.downloads"), &d->downloadDir); | ||
875 | setToggle_Widget(findChild_Widget(dlg, "prefs.ostheme"), d->useSystemTheme); | 906 | setToggle_Widget(findChild_Widget(dlg, "prefs.ostheme"), d->useSystemTheme); |
876 | setToggle_Widget(findChild_Widget(dlg, "prefs.retainwindow"), d->retainWindowSize); | 907 | setToggle_Widget(findChild_Widget(dlg, "prefs.retainwindow"), d->retainWindowSize); |
877 | setText_InputWidget(findChild_Widget(dlg, "prefs.uiscale"), | 908 | setText_InputWidget(findChild_Widget(dlg, "prefs.uiscale"), |
@@ -883,8 +914,28 @@ iBool handleCommand_App(const char *cmd) { | |||
883 | setCommandHandler_Widget(dlg, handlePrefsCommands_); | 914 | setCommandHandler_Widget(dlg, handlePrefsCommands_); |
884 | } | 915 | } |
885 | else if (equal_Command(cmd, "navigate.home")) { | 916 | else if (equal_Command(cmd, "navigate.home")) { |
886 | /* TODO: Look for bookmarks tagged homepage, or use the URL set in Preferences. */ | 917 | /* Look for bookmarks tagged "homepage". */ |
918 | iRegExp *pattern = iClob(new_RegExp("\\bhomepage\\b", caseInsensitive_RegExpOption)); | ||
919 | const iPtrArray *homepages = | ||
920 | list_Bookmarks(d->bookmarks, NULL, filterTagsRegExp_Bookmarks, pattern); | ||
921 | if (isEmpty_PtrArray(homepages)) { | ||
887 | postCommand_App("open url:about:lagrange"); | 922 | postCommand_App("open url:about:lagrange"); |
923 | } | ||
924 | else { | ||
925 | iStringSet *urls = iClob(new_StringSet()); | ||
926 | iConstForEach(PtrArray, i, homepages) { | ||
927 | const iBookmark *bm = i.ptr; | ||
928 | /* Try to switch to a different bookmark. */ | ||
929 | if (cmpStringCase_String(url_DocumentWidget(document_App()), &bm->url)) { | ||
930 | insert_StringSet(urls, &bm->url); | ||
931 | } | ||
932 | } | ||
933 | if (!isEmpty_StringSet(urls)) { | ||
934 | postCommandf_App( | ||
935 | "open url:%s", | ||
936 | cstr_String(constAt_StringSet(urls, iRandoms(0, size_StringSet(urls))))); | ||
937 | } | ||
938 | } | ||
888 | return iTrue; | 939 | return iTrue; |
889 | } | 940 | } |
890 | else if (equal_Command(cmd, "zoom.set")) { | 941 | else if (equal_Command(cmd, "zoom.set")) { |
@@ -993,9 +1044,10 @@ void openInDefaultBrowser_App(const iString *url) { | |||
993 | iClob(newStringsCStr_StringList("/usr/bin/x-www-browser", cstr_String(url), NULL)) | 1044 | iClob(newStringsCStr_StringList("/usr/bin/x-www-browser", cstr_String(url), NULL)) |
994 | #elif defined (iPlatformMsys) | 1045 | #elif defined (iPlatformMsys) |
995 | iClob(newStringsCStr_StringList( | 1046 | iClob(newStringsCStr_StringList( |
996 | "c:\\Windows\\System32\\cmd.exe", "/q", "/c", "start", cstr_String(url), NULL)) | 1047 | concatPath_CStr(cstr_String(execPath_App()), "../urlopen.bat"), |
997 | /* TODO: Should consult environment variables to find the | 1048 | cstr_String(url), |
998 | right cmd.exe. Also, the prompt window is shown momentarily... */ | 1049 | NULL)) |
1050 | /* TODO: The prompt window is shown momentarily... */ | ||
999 | #endif | 1051 | #endif |
1000 | ); | 1052 | ); |
1001 | start_Process(proc); | 1053 | start_Process(proc); |
@@ -48,6 +48,7 @@ enum iUserEventCode { | |||
48 | 48 | ||
49 | const iString *execPath_App (void); | 49 | const iString *execPath_App (void); |
50 | const iString *dataDir_App (void); | 50 | const iString *dataDir_App (void); |
51 | const iString *downloadDir_App (void); | ||
51 | const iString *debugInfo_App (void); | 52 | const iString *debugInfo_App (void); |
52 | 53 | ||
53 | int run_App (int argc, char **argv); | 54 | int run_App (int argc, char **argv); |
@@ -58,7 +59,8 @@ iBool isRefreshPending_App (void); | |||
58 | uint32_t elapsedSinceLastTicker_App (void); /* milliseconds */ | 59 | uint32_t elapsedSinceLastTicker_App (void); /* milliseconds */ |
59 | 60 | ||
60 | int zoom_App (void); | 61 | int zoom_App (void); |
61 | iBool isLineWrapForced_App(void); | 62 | iBool forceLineWrap_App (void); |
63 | iBool forceSoftwareRender_App(void); | ||
62 | enum iColorTheme colorTheme_App (void); | 64 | enum iColorTheme colorTheme_App (void); |
63 | const iString * schemeProxy_App (iRangecc scheme); | 65 | const iString * schemeProxy_App (iRangecc scheme); |
64 | 66 | ||
diff --git a/src/bookmarks.c b/src/bookmarks.c index 8fe7d109..7e98fb27 100644 --- a/src/bookmarks.c +++ b/src/bookmarks.c | |||
@@ -26,6 +26,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
26 | #include <the_Foundation/hash.h> | 26 | #include <the_Foundation/hash.h> |
27 | #include <the_Foundation/mutex.h> | 27 | #include <the_Foundation/mutex.h> |
28 | #include <the_Foundation/path.h> | 28 | #include <the_Foundation/path.h> |
29 | #include <the_Foundation/regexp.h> | ||
29 | 30 | ||
30 | void init_Bookmark(iBookmark *d) { | 31 | void init_Bookmark(iBookmark *d) { |
31 | init_String(&d->url); | 32 | init_String(&d->url); |
@@ -166,6 +167,12 @@ iBookmark *get_Bookmarks(iBookmarks *d, uint32_t id) { | |||
166 | return (iBookmark *) value_Hash(&d->bookmarks, id); | 167 | return (iBookmark *) value_Hash(&d->bookmarks, id); |
167 | } | 168 | } |
168 | 169 | ||
170 | iBool filterTagsRegExp_Bookmarks(void *regExp, const iBookmark *bm) { | ||
171 | iRegExpMatch m; | ||
172 | init_RegExpMatch(&m); | ||
173 | return matchString_RegExp(regExp, &bm->tags, &m); | ||
174 | } | ||
175 | |||
169 | const iPtrArray *list_Bookmarks(const iBookmarks *d, iBookmarksCompareFunc cmp, | 176 | const iPtrArray *list_Bookmarks(const iBookmarks *d, iBookmarksCompareFunc cmp, |
170 | iBookmarksFilterFunc filter, void *context) { | 177 | iBookmarksFilterFunc filter, void *context) { |
171 | lock_Mutex(d->mtx); | 178 | lock_Mutex(d->mtx); |
diff --git a/src/bookmarks.h b/src/bookmarks.h index aac83be1..4889e7b5 100644 --- a/src/bookmarks.h +++ b/src/bookmarks.h | |||
@@ -55,6 +55,8 @@ iBookmark *get_Bookmarks (iBookmarks *, uint32_t id); | |||
55 | typedef iBool (*iBookmarksFilterFunc) (void *context, const iBookmark *); | 55 | typedef iBool (*iBookmarksFilterFunc) (void *context, const iBookmark *); |
56 | typedef int (*iBookmarksCompareFunc)(const iBookmark **, const iBookmark **); | 56 | typedef int (*iBookmarksCompareFunc)(const iBookmark **, const iBookmark **); |
57 | 57 | ||
58 | iBool filterTagsRegExp_Bookmarks (void *regExp, const iBookmark *); | ||
59 | |||
58 | /** | 60 | /** |
59 | * Lists all or a subset of the bookmarks in a sorted array of Bookmark pointers. | 61 | * Lists all or a subset of the bookmarks in a sorted array of Bookmark pointers. |
60 | * | 62 | * |
diff --git a/src/gmdocument.c b/src/gmdocument.c index e2832ae2..4d4ba603 100644 --- a/src/gmdocument.c +++ b/src/gmdocument.c | |||
@@ -64,11 +64,13 @@ struct Impl_GmImage { | |||
64 | size_t numBytes; | 64 | size_t numBytes; |
65 | iString mime; | 65 | iString mime; |
66 | iGmLinkId linkId; | 66 | iGmLinkId linkId; |
67 | iBool isPermanent; | ||
67 | SDL_Texture *texture; | 68 | SDL_Texture *texture; |
68 | }; | 69 | }; |
69 | 70 | ||
70 | void init_GmImage(iGmImage *d, const iBlock *data) { | 71 | void init_GmImage(iGmImage *d, const iBlock *data) { |
71 | init_String(&d->mime); | 72 | init_String(&d->mime); |
73 | d->isPermanent = iFalse; | ||
72 | d->numBytes = size_Block(data); | 74 | d->numBytes = size_Block(data); |
73 | uint8_t *imgData = stbi_load_from_memory( | 75 | uint8_t *imgData = stbi_load_from_memory( |
74 | constData_Block(data), size_Block(data), &d->size.x, &d->size.y, NULL, 4); | 76 | constData_Block(data), size_Block(data), &d->size.x, &d->size.y, NULL, 4); |
@@ -224,6 +226,12 @@ static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *li | |||
224 | else if (equalCase_Rangecc(parts.scheme, "data")) { | 226 | else if (equalCase_Rangecc(parts.scheme, "data")) { |
225 | link->flags |= data_GmLinkFlag; | 227 | link->flags |= data_GmLinkFlag; |
226 | } | 228 | } |
229 | else if (equalCase_Rangecc(parts.scheme, "about")) { | ||
230 | link->flags |= about_GmLinkFlag; | ||
231 | } | ||
232 | else if (equalCase_Rangecc(parts.scheme, "mailto")) { | ||
233 | link->flags |= mailto_GmLinkFlag; | ||
234 | } | ||
227 | /* Check the file name extension, if present. */ | 235 | /* Check the file name extension, if present. */ |
228 | if (!isEmpty_Range(&parts.path)) { | 236 | if (!isEmpty_Range(&parts.path)) { |
229 | iString *path = newRange_String(parts.path); | 237 | iString *path = newRange_String(parts.path); |
@@ -253,7 +261,7 @@ static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *li | |||
253 | trim_Rangecc(&desc); | 261 | trim_Rangecc(&desc); |
254 | if (!isEmpty_Range(&desc)) { | 262 | if (!isEmpty_Range(&desc)) { |
255 | line = desc; /* Just show the description. */ | 263 | line = desc; /* Just show the description. */ |
256 | link->flags |= userFriendly_GmLinkFlag; | 264 | link->flags |= humanReadable_GmLinkFlag; |
257 | } | 265 | } |
258 | else { | 266 | else { |
259 | line = capturedRange_RegExpMatch(&m, 1); /* Show the URL. */ | 267 | line = capturedRange_RegExpMatch(&m, 1); /* Show the URL. */ |
@@ -316,10 +324,12 @@ static void doLayout_GmDocument_(iGmDocument *d) { | |||
316 | static const float bottomMargin[max_GmLineType] = { | 324 | static const float bottomMargin[max_GmLineType] = { |
317 | 0.0f, 0.5f, 1.0f, 0.5f, 0.5f, 0.5f, 0.5f, 1.0f | 325 | 0.0f, 0.5f, 1.0f, 0.5f, 0.5f, 0.5f, 0.5f, 1.0f |
318 | }; | 326 | }; |
319 | static const char *arrow = "\u27a4"; // "\u2192"; | 327 | static const char *arrow = "\u27a4"; |
320 | static const char *bullet = "\u2022"; | 328 | static const char *envelope = "\U0001f4e7"; |
321 | static const char *folder = "\U0001f4c1"; | 329 | static const char *bullet = "\u2022"; |
322 | static const char *globe = "\U0001f310"; | 330 | static const char *folder = "\U0001f4c1"; |
331 | static const char *globe = "\U0001f310"; | ||
332 | static const char *quote = "\u201c"; | ||
323 | const float midRunSkip = 0; /*0.120f;*/ /* extra space between wrapped text/quote lines */ | 333 | const float midRunSkip = 0; /*0.120f;*/ /* extra space between wrapped text/quote lines */ |
324 | clear_Array(&d->layout); | 334 | clear_Array(&d->layout); |
325 | clearLinks_GmDocument_(d); | 335 | clearLinks_GmDocument_(d); |
@@ -332,12 +342,13 @@ static void doLayout_GmDocument_(iGmDocument *d) { | |||
332 | iRangecc contentLine = iNullRange; | 342 | iRangecc contentLine = iNullRange; |
333 | iInt2 pos = zero_I2(); | 343 | iInt2 pos = zero_I2(); |
334 | iBool isFirstText = isGemini; | 344 | iBool isFirstText = isGemini; |
345 | iBool addQuoteIcon = iTrue; | ||
335 | iBool isPreformat = iFalse; | 346 | iBool isPreformat = iFalse; |
336 | iRangecc preAltText = iNullRange; | 347 | iRangecc preAltText = iNullRange; |
337 | int preFont = preformatted_FontId; | 348 | int preFont = preformatted_FontId; |
338 | iBool enableIndents = iFalse; | 349 | iBool enableIndents = iFalse; |
339 | iBool addSiteBanner = iTrue; | 350 | iBool addSiteBanner = iTrue; |
340 | enum iGmLineType prevType; | 351 | enum iGmLineType prevType = text_GmLineType; |
341 | if (d->format == plainText_GmDocumentFormat) { | 352 | if (d->format == plainText_GmDocumentFormat) { |
342 | isPreformat = iTrue; | 353 | isPreformat = iTrue; |
343 | isFirstText = iFalse; | 354 | isFirstText = iFalse; |
@@ -351,9 +362,10 @@ static void doLayout_GmDocument_(iGmDocument *d) { | |||
351 | run.imageId = 0; | 362 | run.imageId = 0; |
352 | enum iGmLineType type; | 363 | enum iGmLineType type; |
353 | int indent = 0; | 364 | int indent = 0; |
365 | /* Detect the type of the line. */ | ||
354 | if (!isPreformat) { | 366 | if (!isPreformat) { |
355 | type = lineType_GmDocument_(d, line); | 367 | type = lineType_GmDocument_(d, line); |
356 | if (line.start == content.start) { | 368 | if (contentLine.start == content.start) { |
357 | prevType = type; | 369 | prevType = type; |
358 | } | 370 | } |
359 | indent = indents[type]; | 371 | indent = indents[type]; |
@@ -389,6 +401,9 @@ static void doLayout_GmDocument_(iGmDocument *d) { | |||
389 | else { | 401 | else { |
390 | /* Preformatted line. */ | 402 | /* Preformatted line. */ |
391 | type = preformatted_GmLineType; | 403 | type = preformatted_GmLineType; |
404 | if (contentLine.start == content.start) { | ||
405 | prevType = type; | ||
406 | } | ||
392 | if (d->format == gemini_GmDocumentFormat && | 407 | if (d->format == gemini_GmDocumentFormat && |
393 | startsWithSc_Rangecc(line, "```", &iCaseSensitive)) { | 408 | startsWithSc_Rangecc(line, "```", &iCaseSensitive)) { |
394 | isPreformat = iFalse; | 409 | isPreformat = iFalse; |
@@ -458,10 +473,30 @@ static void doLayout_GmDocument_(iGmDocument *d) { | |||
458 | bulRun.visBounds.size = advance_Text(run.font, bullet); | 473 | bulRun.visBounds.size = advance_Text(run.font, bullet); |
459 | bulRun.visBounds.pos.x -= 4 * gap_Text - width_Rect(bulRun.visBounds) / 2; | 474 | bulRun.visBounds.pos.x -= 4 * gap_Text - width_Rect(bulRun.visBounds) / 2; |
460 | bulRun.bounds = zero_Rect(); /* just visual */ | 475 | bulRun.bounds = zero_Rect(); /* just visual */ |
461 | bulRun.text = range_CStr(bullet); | 476 | bulRun.text = range_CStr(bullet); |
462 | bulRun.flags |= decoration_GmRunFlag; | 477 | bulRun.flags |= decoration_GmRunFlag; |
463 | pushBack_Array(&d->layout, &bulRun); | 478 | pushBack_Array(&d->layout, &bulRun); |
464 | } | 479 | } |
480 | /* Quote icon. */ | ||
481 | if (type == quote_GmLineType && addQuoteIcon) { | ||
482 | addQuoteIcon = iFalse; | ||
483 | iGmRun quoteRun = run; | ||
484 | quoteRun.font = heading1_FontId; | ||
485 | quoteRun.text = range_CStr(quote); | ||
486 | quoteRun.color = tmQuoteIcon_ColorId; | ||
487 | iRect vis = visualBounds_Text(quoteRun.font, quoteRun.text); | ||
488 | quoteRun.visBounds.size = advance_Text(quoteRun.font, quote); | ||
489 | quoteRun.visBounds.pos = | ||
490 | add_I2(pos, | ||
491 | init_I2(indents[text_GmLineType] * gap_Text, | ||
492 | lineHeight_Text(quote_FontId) / 2 - bottom_Rect(vis))); | ||
493 | quoteRun.bounds = zero_Rect(); /* just visual */ | ||
494 | quoteRun.flags |= decoration_GmRunFlag; | ||
495 | pushBack_Array(&d->layout, "eRun); | ||
496 | } | ||
497 | else if (type != quote_GmLineType) { | ||
498 | addQuoteIcon = iTrue; | ||
499 | } | ||
465 | /* Link icon. */ | 500 | /* Link icon. */ |
466 | if (type == link_GmLineType) { | 501 | if (type == link_GmLineType) { |
467 | iGmRun icon = run; | 502 | iGmRun icon = run; |
@@ -471,7 +506,9 @@ static void doLayout_GmDocument_(iGmDocument *d) { | |||
471 | const iGmLink *link = constAt_PtrArray(&d->links, run.linkId - 1); | 506 | const iGmLink *link = constAt_PtrArray(&d->links, run.linkId - 1); |
472 | icon.text = range_CStr(link->flags & file_GmLinkFlag | 507 | icon.text = range_CStr(link->flags & file_GmLinkFlag |
473 | ? folder | 508 | ? folder |
474 | : link->flags & remote_GmLinkFlag ? globe : arrow); | 509 | : link->flags & mailto_GmLinkFlag |
510 | ? envelope | ||
511 | : link->flags & remote_GmLinkFlag ? globe : arrow); | ||
475 | if (link->flags & remote_GmLinkFlag) { | 512 | if (link->flags & remote_GmLinkFlag) { |
476 | icon.visBounds.pos.x -= gap_Text / 2; | 513 | icon.visBounds.pos.x -= gap_Text / 2; |
477 | } | 514 | } |
@@ -536,8 +573,14 @@ static void doLayout_GmDocument_(iGmDocument *d) { | |||
536 | if (type == link_GmLineType) { | 573 | if (type == link_GmLineType) { |
537 | const size_t imgIndex = findLinkImage_GmDocument_(d, run.linkId); | 574 | const size_t imgIndex = findLinkImage_GmDocument_(d, run.linkId); |
538 | if (imgIndex != iInvalidPos) { | 575 | if (imgIndex != iInvalidPos) { |
539 | ((iGmLink *) at_PtrArray(&d->links, run.linkId - 1))->flags |= content_GmLinkFlag; | ||
540 | const iGmImage *img = constAt_PtrArray(&d->images, imgIndex); | 576 | const iGmImage *img = constAt_PtrArray(&d->images, imgIndex); |
577 | /* Mark the link as having content. */ { | ||
578 | iGmLink *link = at_PtrArray(&d->links, run.linkId - 1); | ||
579 | link->flags |= content_GmLinkFlag; | ||
580 | if (img->isPermanent) { | ||
581 | link->flags |= permanent_GmLinkFlag; | ||
582 | } | ||
583 | } | ||
541 | const int margin = 0.5f * lineHeight_Text(paragraph_FontId); | 584 | const int margin = 0.5f * lineHeight_Text(paragraph_FontId); |
542 | pos.y += margin; | 585 | pos.y += margin; |
543 | run.bounds.pos = pos; | 586 | run.bounds.pos = pos; |
@@ -854,6 +897,8 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) { | |||
854 | setHsl_Color(i, color); | 897 | setHsl_Color(i, color); |
855 | } | 898 | } |
856 | } | 899 | } |
900 | set_Color(tmQuoteIcon_ColorId, | ||
901 | mix_Color(get_Color(tmQuote_ColorId), get_Color(tmBackground_ColorId), 0.55f)); | ||
857 | /* Special exceptions. */ | 902 | /* Special exceptions. */ |
858 | if (seed) { | 903 | if (seed) { |
859 | if (equal_CStr(cstr_Block(seed), "gemini.circumlunar.space")) { | 904 | if (equal_CStr(cstr_Block(seed), "gemini.circumlunar.space")) { |
@@ -946,7 +991,8 @@ void setSource_GmDocument(iGmDocument *d, const iString *source, int width, int | |||
946 | setWidth_GmDocument(d, width, forceBreakWidth); /* re-do layout */ | 991 | setWidth_GmDocument(d, width, forceBreakWidth); /* re-do layout */ |
947 | } | 992 | } |
948 | 993 | ||
949 | void setImage_GmDocument(iGmDocument *d, iGmLinkId linkId, const iString *mime, const iBlock *data) { | 994 | void setImage_GmDocument(iGmDocument *d, iGmLinkId linkId, const iString *mime, const iBlock *data, |
995 | iBool allowHide) { | ||
950 | if (!mime || !data) { | 996 | if (!mime || !data) { |
951 | iGmImage *img; | 997 | iGmImage *img; |
952 | if (take_PtrArray(&d->images, findLinkImage_GmDocument_(d, linkId), (void **) &img)) { | 998 | if (take_PtrArray(&d->images, findLinkImage_GmDocument_(d, linkId), (void **) &img)) { |
@@ -955,9 +1001,10 @@ void setImage_GmDocument(iGmDocument *d, iGmLinkId linkId, const iString *mime, | |||
955 | } | 1001 | } |
956 | else { | 1002 | else { |
957 | /* TODO: check if we know this MIME type */ | 1003 | /* TODO: check if we know this MIME type */ |
958 | /* Load the image. */ { | 1004 | /* Upload the image. */ { |
959 | iGmImage *img = new_GmImage(data); | 1005 | iGmImage *img = new_GmImage(data); |
960 | img->linkId = linkId; /* TODO: use a hash? */ | 1006 | img->linkId = linkId; /* TODO: use a hash? */ |
1007 | img->isPermanent = !allowHide; | ||
961 | set_String(&img->mime, mime); | 1008 | set_String(&img->mime, mime); |
962 | if (img->texture) { | 1009 | if (img->texture) { |
963 | pushBack_PtrArray(&d->images, img); | 1010 | pushBack_PtrArray(&d->images, img); |
@@ -1098,6 +1145,7 @@ uint16_t linkImage_GmDocument(const iGmDocument *d, iGmLinkId linkId) { | |||
1098 | 1145 | ||
1099 | enum iColorId linkColor_GmDocument(const iGmDocument *d, iGmLinkId linkId, enum iGmLinkPart part) { | 1146 | enum iColorId linkColor_GmDocument(const iGmDocument *d, iGmLinkId linkId, enum iGmLinkPart part) { |
1100 | const iGmLink *link = link_GmDocument_(d, linkId); | 1147 | const iGmLink *link = link_GmDocument_(d, linkId); |
1148 | const int www_GmLinkFlag = http_GmLinkFlag | mailto_GmLinkFlag; | ||
1101 | if (link) { | 1149 | if (link) { |
1102 | const iBool isBad = (link->flags & supportedProtocol_GmLinkFlag) == 0; | 1150 | const iBool isBad = (link->flags & supportedProtocol_GmLinkFlag) == 0; |
1103 | if (part == icon_GmLinkPart) { | 1151 | if (part == icon_GmLinkPart) { |
@@ -1105,24 +1153,24 @@ enum iColorId linkColor_GmDocument(const iGmDocument *d, iGmLinkId linkId, enum | |||
1105 | return tmBadLink_ColorId; | 1153 | return tmBadLink_ColorId; |
1106 | } | 1154 | } |
1107 | if (link->flags & visited_GmLinkFlag) { | 1155 | if (link->flags & visited_GmLinkFlag) { |
1108 | return link->flags & http_GmLinkFlag | 1156 | return link->flags & www_GmLinkFlag |
1109 | ? tmHypertextLinkIconVisited_ColorId | 1157 | ? tmHypertextLinkIconVisited_ColorId |
1110 | : link->flags & gopher_GmLinkFlag ? tmGopherLinkIconVisited_ColorId | 1158 | : link->flags & gopher_GmLinkFlag ? tmGopherLinkIconVisited_ColorId |
1111 | : tmLinkIconVisited_ColorId; | 1159 | : tmLinkIconVisited_ColorId; |
1112 | } | 1160 | } |
1113 | return link->flags & http_GmLinkFlag | 1161 | return link->flags & www_GmLinkFlag |
1114 | ? tmHypertextLinkIcon_ColorId | 1162 | ? tmHypertextLinkIcon_ColorId |
1115 | : link->flags & gopher_GmLinkFlag ? tmGopherLinkIcon_ColorId | 1163 | : link->flags & gopher_GmLinkFlag ? tmGopherLinkIcon_ColorId |
1116 | : tmLinkIcon_ColorId; | 1164 | : tmLinkIcon_ColorId; |
1117 | } | 1165 | } |
1118 | if (part == text_GmLinkPart) { | 1166 | if (part == text_GmLinkPart) { |
1119 | return link->flags & http_GmLinkFlag | 1167 | return link->flags & www_GmLinkFlag |
1120 | ? tmHypertextLinkText_ColorId | 1168 | ? tmHypertextLinkText_ColorId |
1121 | : link->flags & gopher_GmLinkFlag ? tmGopherLinkText_ColorId | 1169 | : link->flags & gopher_GmLinkFlag ? tmGopherLinkText_ColorId |
1122 | : tmLinkText_ColorId; | 1170 | : tmLinkText_ColorId; |
1123 | } | 1171 | } |
1124 | if (part == textHover_GmLinkPart) { | 1172 | if (part == textHover_GmLinkPart) { |
1125 | return link->flags & http_GmLinkFlag | 1173 | return link->flags & www_GmLinkFlag |
1126 | ? tmHypertextLinkTextHover_ColorId | 1174 | ? tmHypertextLinkTextHover_ColorId |
1127 | : link->flags & gopher_GmLinkFlag ? tmGopherLinkTextHover_ColorId | 1175 | : link->flags & gopher_GmLinkFlag ? tmGopherLinkTextHover_ColorId |
1128 | : tmLinkTextHover_ColorId; | 1176 | : tmLinkTextHover_ColorId; |
@@ -1131,13 +1179,13 @@ enum iColorId linkColor_GmDocument(const iGmDocument *d, iGmLinkId linkId, enum | |||
1131 | if (isBad) { | 1179 | if (isBad) { |
1132 | return tmBadLink_ColorId; | 1180 | return tmBadLink_ColorId; |
1133 | } | 1181 | } |
1134 | return link->flags & http_GmLinkFlag | 1182 | return link->flags & www_GmLinkFlag |
1135 | ? tmHypertextLinkDomain_ColorId | 1183 | ? tmHypertextLinkDomain_ColorId |
1136 | : link->flags & gopher_GmLinkFlag ? tmGopherLinkDomain_ColorId | 1184 | : link->flags & gopher_GmLinkFlag ? tmGopherLinkDomain_ColorId |
1137 | : tmLinkDomain_ColorId; | 1185 | : tmLinkDomain_ColorId; |
1138 | } | 1186 | } |
1139 | if (part == visited_GmLinkPart) { | 1187 | if (part == visited_GmLinkPart) { |
1140 | return link->flags & http_GmLinkFlag | 1188 | return link->flags & www_GmLinkFlag |
1141 | ? tmHypertextLinkLastVisitDate_ColorId | 1189 | ? tmHypertextLinkLastVisitDate_ColorId |
1142 | : link->flags & gopher_GmLinkFlag ? tmGopherLinkLastVisitDate_ColorId | 1190 | : link->flags & gopher_GmLinkFlag ? tmGopherLinkLastVisitDate_ColorId |
1143 | : tmLinkLastVisitDate_ColorId; | 1191 | : tmLinkLastVisitDate_ColorId; |
diff --git a/src/gmdocument.h b/src/gmdocument.h index e3c21097..b453fba9 100644 --- a/src/gmdocument.h +++ b/src/gmdocument.h | |||
@@ -43,13 +43,16 @@ enum iGmLinkFlags { | |||
43 | http_GmLinkFlag = iBit(3), | 43 | http_GmLinkFlag = iBit(3), |
44 | file_GmLinkFlag = iBit(4), | 44 | file_GmLinkFlag = iBit(4), |
45 | data_GmLinkFlag = iBit(5), | 45 | data_GmLinkFlag = iBit(5), |
46 | supportedProtocol_GmLinkFlag = 0x1f, | 46 | about_GmLinkFlag = iBit(6), |
47 | mailto_GmLinkFlag = iBit(7), | ||
48 | supportedProtocol_GmLinkFlag = 0xff, | ||
47 | remote_GmLinkFlag = iBit(9), | 49 | remote_GmLinkFlag = iBit(9), |
48 | userFriendly_GmLinkFlag = iBit(10), | 50 | humanReadable_GmLinkFlag = iBit(10), /* link has a human-readable description */ |
49 | imageFileExtension_GmLinkFlag = iBit(11), | 51 | imageFileExtension_GmLinkFlag = iBit(11), |
50 | audioFileExtension_GmLinkFlag = iBit(12), | 52 | audioFileExtension_GmLinkFlag = iBit(12), |
51 | content_GmLinkFlag = iBit(13), /* content visible below */ | 53 | content_GmLinkFlag = iBit(13), /* content visible below */ |
52 | visited_GmLinkFlag = iBit(14), /* in the history */ | 54 | visited_GmLinkFlag = iBit(14), /* in the history */ |
55 | permanent_GmLinkFlag = iBit(15), /* content cannot be dismissed; media link */ | ||
53 | }; | 56 | }; |
54 | 57 | ||
55 | struct Impl_GmImageInfo { | 58 | struct Impl_GmImageInfo { |
@@ -97,7 +100,8 @@ void setFormat_GmDocument (iGmDocument *, enum iGmDocumentFormat format); | |||
97 | void setWidth_GmDocument (iGmDocument *, int width, int forceBreakWidth); | 100 | void setWidth_GmDocument (iGmDocument *, int width, int forceBreakWidth); |
98 | void setUrl_GmDocument (iGmDocument *, const iString *url); | 101 | void setUrl_GmDocument (iGmDocument *, const iString *url); |
99 | void setSource_GmDocument (iGmDocument *, const iString *source, int width, int forceBreakWidth); | 102 | void setSource_GmDocument (iGmDocument *, const iString *source, int width, int forceBreakWidth); |
100 | void setImage_GmDocument (iGmDocument *, iGmLinkId linkId, const iString *mime, const iBlock *data); | 103 | void setImage_GmDocument (iGmDocument *, iGmLinkId linkId, const iString *mime, const iBlock *data, |
104 | iBool allowHide); | ||
101 | 105 | ||
102 | void reset_GmDocument (iGmDocument *); /* free images */ | 106 | void reset_GmDocument (iGmDocument *); /* free images */ |
103 | 107 | ||
diff --git a/src/gmrequest.c b/src/gmrequest.c index 843d2f46..43cc874a 100644 --- a/src/gmrequest.c +++ b/src/gmrequest.c | |||
@@ -228,7 +228,6 @@ static void readIncoming_GmRequest_(iAnyObject *obj) { | |||
228 | lock_Mutex(&d->mutex); | 228 | lock_Mutex(&d->mutex); |
229 | iAssert(d->state != finished_GmRequestState); /* notifications out of order? */ | 229 | iAssert(d->state != finished_GmRequestState); /* notifications out of order? */ |
230 | iBlock *data = readAll_TlsRequest(d->req); | 230 | iBlock *data = readAll_TlsRequest(d->req); |
231 | fflush(stdout); | ||
232 | if (d->state == receivingHeader_GmRequestState) { | 231 | if (d->state == receivingHeader_GmRequestState) { |
233 | appendCStrN_String(&d->resp.meta, constData_Block(data), size_Block(data)); | 232 | appendCStrN_String(&d->resp.meta, constData_Block(data), size_Block(data)); |
234 | /* Check if the header line is complete. */ | 233 | /* Check if the header line is complete. */ |
@@ -305,6 +304,9 @@ static const iBlock *aboutPageSource_(iRangecc path) { | |||
305 | if (equalCase_Rangecc(path, "help")) { | 304 | if (equalCase_Rangecc(path, "help")) { |
306 | return &blobHelp_Embedded; | 305 | return &blobHelp_Embedded; |
307 | } | 306 | } |
307 | if (equalCase_Rangecc(path, "license")) { | ||
308 | return &blobLicense_Embedded; | ||
309 | } | ||
308 | if (equalCase_Rangecc(path, "version")) { | 310 | if (equalCase_Rangecc(path, "version")) { |
309 | return &blobVersion_Embedded; | 311 | return &blobVersion_Embedded; |
310 | } | 312 | } |
diff --git a/src/gmutil.c b/src/gmutil.c index 131734b2..f55729d1 100644 --- a/src/gmutil.c +++ b/src/gmutil.c | |||
@@ -132,7 +132,8 @@ const iString *absoluteUrl_String(const iString *d, const iString *urlMaybeRelat | |||
132 | iUrl rel; | 132 | iUrl rel; |
133 | init_Url(&orig, d); | 133 | init_Url(&orig, d); |
134 | init_Url(&rel, urlMaybeRelative); | 134 | init_Url(&rel, urlMaybeRelative); |
135 | if (equalCase_Rangecc(rel.scheme, "data") || equalCase_Rangecc(rel.scheme, "about")) { | 135 | if (equalCase_Rangecc(rel.scheme, "data") || equalCase_Rangecc(rel.scheme, "about") || |
136 | equalCase_Rangecc(rel.scheme, "mailto")) { | ||
136 | /* Special case, the contents should be left unparsed. */ | 137 | /* Special case, the contents should be left unparsed. */ |
137 | return urlMaybeRelative; | 138 | return urlMaybeRelative; |
138 | } | 139 | } |
@@ -214,8 +215,7 @@ static const struct { | |||
214 | { unsupportedMimeType_GmStatusCode, | 215 | { unsupportedMimeType_GmStatusCode, |
215 | { 0x1f47d, /* alien */ | 216 | { 0x1f47d, /* alien */ |
216 | "Unsupported MIME Type", | 217 | "Unsupported MIME Type", |
217 | "The received content is in an unsupported format and cannot be viewed with " | 218 | "The received content cannot be viewed with this application." } }, |
218 | "this application." } }, | ||
219 | { invalidHeader_GmStatusCode, | 219 | { invalidHeader_GmStatusCode, |
220 | { 0x1f4a9, /* pile of poo */ | 220 | { 0x1f4a9, /* pile of poo */ |
221 | "Invalid Header", | 221 | "Invalid Header", |
diff --git a/src/macos.m b/src/macos.m index bbbaaa19..b000fed9 100644 --- a/src/macos.m +++ b/src/macos.m | |||
@@ -28,38 +28,19 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
28 | 28 | ||
29 | #import <AppKit/AppKit.h> | 29 | #import <AppKit/AppKit.h> |
30 | 30 | ||
31 | #if 0 | 31 | static NSTouchBarItemIdentifier goBack_TouchId_ = @"fi.skyjake.Lagrange.back"; |
32 | static NSTouchBarItemIdentifier play_TouchId_ = @"fi.skyjake.BitwiseHarmony.play"; | 32 | static NSTouchBarItemIdentifier goForward_TouchId_ = @"fi.skyjake.Lagrange.forward"; |
33 | static NSTouchBarItemIdentifier restart_TouchId_ = @"fi.skyjake.BitwiseHarmony.restart"; | 33 | static NSTouchBarItemIdentifier find_TouchId_ = @"fi.skyjake.Lagrange.find"; |
34 | 34 | static NSTouchBarItemIdentifier newTab_TouchId_ = @"fi.skyjake.Lagrange.tabs.new"; | |
35 | static NSTouchBarItemIdentifier seqMoveUp_TouchId_ = @"fi.skyjake.BitwiseHarmony.sequence.move.up"; | 35 | static NSTouchBarItemIdentifier sidebarMode_TouchId_ = @"fi.skyjake.Lagrange.sidebar.mode"; |
36 | static NSTouchBarItemIdentifier seqMoveDown_TouchId_ = @"fi.skyjake.BitwiseHarmony.sequence.move.down"; | ||
37 | |||
38 | static NSTouchBarItemIdentifier goto_TouchId_ = @"fi.skyjake.BitwiseHarmony.goto"; | ||
39 | static NSTouchBarItemIdentifier mute_TouchId_ = @"fi.skyjake.BitwiseHarmony.mute"; | ||
40 | static NSTouchBarItemIdentifier solo_TouchId_ = @"fi.skyjake.BitwiseHarmony.solo"; | ||
41 | static NSTouchBarItemIdentifier color_TouchId_ = @"fi.skyjake.BitwiseHarmony.color"; | ||
42 | static NSTouchBarItemIdentifier event_TouchId_ = @"fi.skyjake.BitwiseHarmony.event"; | ||
43 | |||
44 | static NSTouchBarItemIdentifier eventList_TouchId_ = @"fi.skyjake.BitwiseHarmony.eventlist"; | ||
45 | static NSTouchBarItemIdentifier masterGainEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.mastergain"; | ||
46 | static NSTouchBarItemIdentifier resetEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.reset"; | ||
47 | static NSTouchBarItemIdentifier voiceEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.voice"; | ||
48 | static NSTouchBarItemIdentifier panEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.pan"; | ||
49 | static NSTouchBarItemIdentifier gainEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.gain"; | ||
50 | static NSTouchBarItemIdentifier fadeEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.fade"; | ||
51 | static NSTouchBarItemIdentifier pitchSpeedEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.pitchspeed"; | ||
52 | static NSTouchBarItemIdentifier pitchBendUpEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.pitchbendup"; | ||
53 | static NSTouchBarItemIdentifier pitchBendDownEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.pitchbenddown"; | ||
54 | static NSTouchBarItemIdentifier tremoloEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.tremolo"; | ||
55 | #endif | ||
56 | 36 | ||
57 | enum iTouchBarVariant { | 37 | enum iTouchBarVariant { |
58 | default_TouchBarVariant, | 38 | default_TouchBarVariant, |
59 | }; | 39 | }; |
60 | 40 | ||
61 | #if 0 | 41 | /*----------------------------------------------------------------------------------------------*/ |
62 | @interface CommandButton : NSButtonTouchBarItem { | 42 | |
43 | @interface CommandButton : NSCustomTouchBarItem { | ||
63 | NSString *command; | 44 | NSString *command; |
64 | iWidget *widget; | 45 | iWidget *widget; |
65 | } | 46 | } |
@@ -70,6 +51,10 @@ enum iTouchBarVariant { | |||
70 | title:(NSString *)title | 51 | title:(NSString *)title |
71 | widget:(iWidget *)widget | 52 | widget:(iWidget *)widget |
72 | command:(NSString *)cmd; | 53 | command:(NSString *)cmd; |
54 | - (id)initWithIdentifier:(NSTouchBarItemIdentifier)identifier | ||
55 | image:(NSImage *)image | ||
56 | widget:(iWidget *)widget | ||
57 | command:(NSString *)cmd; | ||
73 | - (void)dealloc; | 58 | - (void)dealloc; |
74 | @end | 59 | @end |
75 | 60 | ||
@@ -79,9 +64,17 @@ enum iTouchBarVariant { | |||
79 | title:(NSString *)title | 64 | title:(NSString *)title |
80 | command:(NSString *)cmd { | 65 | command:(NSString *)cmd { |
81 | [super initWithIdentifier:identifier]; | 66 | [super initWithIdentifier:identifier]; |
82 | self.title = title; | 67 | self.view = [NSButton buttonWithTitle:title target:self action:@selector(buttonPressed)]; |
83 | self.target = self; | 68 | command = cmd; |
84 | self.action = @selector(buttonPressed); | 69 | return self; |
70 | } | ||
71 | |||
72 | - (id)initWithIdentifier:(NSTouchBarItemIdentifier)identifier | ||
73 | image:(NSImage *)image | ||
74 | widget:(iWidget *)widget | ||
75 | command:(NSString *)cmd { | ||
76 | [super initWithIdentifier:identifier]; | ||
77 | self.view = [NSButton buttonWithImage:image target:self action:@selector(buttonPressed)]; | ||
85 | command = cmd; | 78 | command = cmd; |
86 | return self; | 79 | return self; |
87 | } | 80 | } |
@@ -111,7 +104,8 @@ enum iTouchBarVariant { | |||
111 | } | 104 | } |
112 | 105 | ||
113 | @end | 106 | @end |
114 | #endif | 107 | |
108 | /*----------------------------------------------------------------------------------------------*/ | ||
115 | 109 | ||
116 | @interface MyDelegate : NSResponder<NSApplicationDelegate, NSTouchBarDelegate> { | 110 | @interface MyDelegate : NSResponder<NSApplicationDelegate, NSTouchBarDelegate> { |
117 | enum iTouchBarVariant touchBarVariant; | 111 | enum iTouchBarVariant touchBarVariant; |
@@ -120,7 +114,7 @@ enum iTouchBarVariant { | |||
120 | NSMutableDictionary<NSString *, NSString*> *menuCommands; | 114 | NSMutableDictionary<NSString *, NSString*> *menuCommands; |
121 | } | 115 | } |
122 | - (id)initWithSDLDelegate:(NSObject<NSApplicationDelegate> *)sdl; | 116 | - (id)initWithSDLDelegate:(NSObject<NSApplicationDelegate> *)sdl; |
123 | //- (NSTouchBar *)makeTouchBar; | 117 | - (NSTouchBar *)makeTouchBar; |
124 | - (BOOL)application:(NSApplication *)app openFile:(NSString *)filename; | 118 | - (BOOL)application:(NSApplication *)app openFile:(NSString *)filename; |
125 | - (void)application:(NSApplication *)app openFiles:(NSArray<NSString *> *)filenames; | 119 | - (void)application:(NSApplication *)app openFiles:(NSArray<NSString *> *)filenames; |
126 | - (void)application:(NSApplication *)app openURLs:(NSArray<NSURL *> *)urls; | 120 | - (void)application:(NSApplication *)app openURLs:(NSArray<NSURL *> *)urls; |
@@ -153,8 +147,6 @@ static void appearanceChanged_MacOS_(NSString *name) { | |||
153 | const iBool isDark = [name containsString:@"Dark"]; | 147 | const iBool isDark = [name containsString:@"Dark"]; |
154 | const iBool isHighContrast = [name containsString:@"HighContrast"]; | 148 | const iBool isHighContrast = [name containsString:@"HighContrast"]; |
155 | postCommandf_App("~os.theme.changed dark:%d contrast:%d", isDark ? 1 : 0, isHighContrast ? 1 : 0); | 149 | postCommandf_App("~os.theme.changed dark:%d contrast:%d", isDark ? 1 : 0, isHighContrast ? 1 : 0); |
156 | // printf("Effective appearance changed: %s\n", [name cStringUsingEncoding:NSUTF8StringEncoding]); | ||
157 | // fflush(stdout); | ||
158 | } | 150 | } |
159 | 151 | ||
160 | - (void)setAppearance:(NSString *)name { | 152 | - (void)setAppearance:(NSString *)name { |
@@ -204,42 +196,19 @@ static void appearanceChanged_MacOS_(NSString *name) { | |||
204 | } | 196 | } |
205 | } | 197 | } |
206 | 198 | ||
207 | #if 0 | 199 | #if 1 |
208 | - (NSTouchBar *)makeTouchBar { | 200 | - (NSTouchBar *)makeTouchBar { |
209 | NSTouchBar *bar = [[NSTouchBar alloc] init]; | 201 | NSTouchBar *bar = [[NSTouchBar alloc] init]; |
210 | bar.delegate = self; | 202 | bar.delegate = self; |
211 | switch (touchBarVariant) { | 203 | switch (touchBarVariant) { |
212 | case default_TouchBarVariant: | 204 | case default_TouchBarVariant: |
213 | bar.defaultItemIdentifiers = @[ play_TouchId_, restart_TouchId_, | 205 | bar.defaultItemIdentifiers = @[ goBack_TouchId_, goForward_TouchId_, |
214 | NSTouchBarItemIdentifierFixedSpaceSmall, | 206 | NSTouchBarItemIdentifierFixedSpaceSmall, |
215 | NSTouchBarItemIdentifierOtherItemsProxy ]; | 207 | find_TouchId_, |
216 | break; | ||
217 | case sequence_TouchBarVariant: | ||
218 | bar.defaultItemIdentifiers = @[ play_TouchId_, restart_TouchId_, | ||
219 | NSTouchBarItemIdentifierFlexibleSpace, | ||
220 | seqMoveUp_TouchId_, seqMoveDown_TouchId_, | ||
221 | NSTouchBarItemIdentifierFlexibleSpace, | ||
222 | NSTouchBarItemIdentifierOtherItemsProxy]; | ||
223 | break; | ||
224 | case tracker_TouchBarVariant: | ||
225 | bar.defaultItemIdentifiers = @[ play_TouchId_, restart_TouchId_, | ||
226 | NSTouchBarItemIdentifierFlexibleSpace, | 208 | NSTouchBarItemIdentifierFlexibleSpace, |
227 | goto_TouchId_, | 209 | sidebarMode_TouchId_, |
228 | event_TouchId_, | ||
229 | NSTouchBarItemIdentifierFlexibleSpace, | ||
230 | solo_TouchId_, mute_TouchId_, color_TouchId_, | ||
231 | NSTouchBarItemIdentifierFlexibleSpace, | ||
232 | NSTouchBarItemIdentifierOtherItemsProxy ]; | ||
233 | break; | ||
234 | case wide_TouchBarVariant: | ||
235 | bar.defaultItemIdentifiers = @[ play_TouchId_, restart_TouchId_, | ||
236 | NSTouchBarItemIdentifierFlexibleSpace, | ||
237 | event_TouchId_, | ||
238 | NSTouchBarItemIdentifierFlexibleSpace, | ||
239 | solo_TouchId_, mute_TouchId_, color_TouchId_, | ||
240 | NSTouchBarItemIdentifierFlexibleSpace, | ||
241 | seqMoveUp_TouchId_, seqMoveDown_TouchId_, | ||
242 | NSTouchBarItemIdentifierFlexibleSpace, | 210 | NSTouchBarItemIdentifierFlexibleSpace, |
211 | newTab_TouchId_, | ||
243 | NSTouchBarItemIdentifierOtherItemsProxy ]; | 212 | NSTouchBarItemIdentifierOtherItemsProxy ]; |
244 | break; | 213 | break; |
245 | } | 214 | } |
@@ -262,9 +231,48 @@ static void appearanceChanged_MacOS_(NSString *name) { | |||
262 | } | 231 | } |
263 | } | 232 | } |
264 | 233 | ||
234 | - (void)sidebarModePressed:(id)sender { | ||
235 | NSSegmentedControl *seg = sender; | ||
236 | postCommandf_App("sidebar.mode arg:%d toggle:1", (int) [seg selectedSegment]); | ||
237 | } | ||
238 | |||
265 | - (nullable NSTouchBarItem *)touchBar:(NSTouchBar *)touchBar | 239 | - (nullable NSTouchBarItem *)touchBar:(NSTouchBar *)touchBar |
266 | makeItemForIdentifier:(NSTouchBarItemIdentifier)identifier { | 240 | makeItemForIdentifier:(NSTouchBarItemIdentifier)identifier { |
267 | iUnused(touchBar); | 241 | iUnused(touchBar); |
242 | if ([identifier isEqualToString:goBack_TouchId_]) { | ||
243 | return [[CommandButton alloc] initWithIdentifier:identifier | ||
244 | image:[NSImage imageNamed:NSImageNameTouchBarGoBackTemplate] | ||
245 | widget:nil | ||
246 | command:@"navigate.back"]; | ||
247 | } | ||
248 | else if ([identifier isEqualToString:goForward_TouchId_]) { | ||
249 | return [[CommandButton alloc] initWithIdentifier:identifier | ||
250 | image:[NSImage imageNamed:NSImageNameTouchBarGoForwardTemplate] | ||
251 | widget:nil | ||
252 | command:@"navigate.forward"]; | ||
253 | } | ||
254 | else if ([identifier isEqualToString:find_TouchId_]) { | ||
255 | return [[CommandButton alloc] initWithIdentifier:identifier | ||
256 | image:[NSImage imageNamed:NSImageNameTouchBarSearchTemplate] | ||
257 | widget:nil | ||
258 | command:@"focus.set id:find.input"]; | ||
259 | } | ||
260 | else if ([identifier isEqualToString:sidebarMode_TouchId_]) { | ||
261 | NSCustomTouchBarItem *item = [[NSCustomTouchBarItem alloc] initWithIdentifier:sidebarMode_TouchId_]; | ||
262 | NSSegmentedControl *seg = | ||
263 | [NSSegmentedControl segmentedControlWithLabels:@[ @"Bookmarks", @"History", @"Identities", @"Outline"] | ||
264 | trackingMode:NSSegmentSwitchTrackingMomentary | ||
265 | target:[[NSApplication sharedApplication] delegate] | ||
266 | action:@selector(sidebarModePressed:)]; | ||
267 | item.view = seg; | ||
268 | return item; | ||
269 | } | ||
270 | else if ([identifier isEqualToString:newTab_TouchId_]) { | ||
271 | return [[CommandButton alloc] initWithIdentifier:identifier | ||
272 | image:[NSImage imageNamed:NSImageNameTouchBarAddTemplate] | ||
273 | widget:nil | ||
274 | command:@"tabs.new"]; | ||
275 | } | ||
268 | #if 0 | 276 | #if 0 |
269 | if ([identifier isEqualToString:play_TouchId_]) { | 277 | if ([identifier isEqualToString:play_TouchId_]) { |
270 | return [NSButtonTouchBarItem | 278 | return [NSButtonTouchBarItem |
@@ -524,18 +532,7 @@ void handleCommand_MacOS(const char *cmd) { | |||
524 | if (equal_Command(cmd, "tabs.changed")) { | 532 | if (equal_Command(cmd, "tabs.changed")) { |
525 | MyDelegate *myDel = (MyDelegate *) [[NSApplication sharedApplication] delegate]; | 533 | MyDelegate *myDel = (MyDelegate *) [[NSApplication sharedApplication] delegate]; |
526 | const char *tabId = suffixPtr_Command(cmd, "id"); | 534 | const char *tabId = suffixPtr_Command(cmd, "id"); |
527 | if (equal_CStr(tabId, "tracker")) { | 535 | [myDel setTouchBarVariant:default_TouchBarVariant]; |
528 | [myDel setTouchBarVariant:tracker_TouchBarVariant]; | ||
529 | } | ||
530 | else if (equal_CStr(tabId, "sequence")) { | ||
531 | [myDel setTouchBarVariant:sequence_TouchBarVariant]; | ||
532 | } | ||
533 | else if (equal_CStr(tabId, "trackertab")) { | ||
534 | [myDel setTouchBarVariant:wide_TouchBarVariant]; | ||
535 | } | ||
536 | else { | ||
537 | [myDel setTouchBarVariant:default_TouchBarVariant]; | ||
538 | } | ||
539 | } | 536 | } |
540 | #endif | 537 | #endif |
541 | } | 538 | } |
diff --git a/src/ui/color.h b/src/ui/color.h index 2c481d13..51d3370f 100644 --- a/src/ui/color.h +++ b/src/ui/color.h | |||
@@ -48,7 +48,7 @@ enum iColorId { | |||
48 | gray50_ColorId, | 48 | gray50_ColorId, |
49 | gray75_ColorId, | 49 | gray75_ColorId, |
50 | white_ColorId, | 50 | white_ColorId, |
51 | brown_ColorId, | 51 | brown_ColorId, |
52 | orange_ColorId, | 52 | orange_ColorId, |
53 | teal_ColorId, | 53 | teal_ColorId, |
54 | cyan_ColorId, | 54 | cyan_ColorId, |
@@ -109,6 +109,7 @@ enum iColorId { | |||
109 | tmParagraph_ColorId, | 109 | tmParagraph_ColorId, |
110 | tmFirstParagraph_ColorId, | 110 | tmFirstParagraph_ColorId, |
111 | tmQuote_ColorId, | 111 | tmQuote_ColorId, |
112 | tmQuoteIcon_ColorId, | ||
112 | tmPreformatted_ColorId, | 113 | tmPreformatted_ColorId, |
113 | tmHeading1_ColorId, | 114 | tmHeading1_ColorId, |
114 | tmHeading2_ColorId, | 115 | tmHeading2_ColorId, |
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index fce548b4..70e66180 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c | |||
@@ -36,6 +36,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
36 | #include "../gmutil.h" | 36 | #include "../gmutil.h" |
37 | 37 | ||
38 | #include <the_Foundation/file.h> | 38 | #include <the_Foundation/file.h> |
39 | #include <the_Foundation/fileinfo.h> | ||
39 | #include <the_Foundation/objectlist.h> | 40 | #include <the_Foundation/objectlist.h> |
40 | #include <the_Foundation/path.h> | 41 | #include <the_Foundation/path.h> |
41 | #include <the_Foundation/ptrarray.h> | 42 | #include <the_Foundation/ptrarray.h> |
@@ -46,6 +47,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
46 | #include <SDL_timer.h> | 47 | #include <SDL_timer.h> |
47 | #include <SDL_render.h> | 48 | #include <SDL_render.h> |
48 | #include <ctype.h> | 49 | #include <ctype.h> |
50 | #include <errno.h> | ||
49 | 51 | ||
50 | iDeclareClass(MediaRequest) | 52 | iDeclareClass(MediaRequest) |
51 | 53 | ||
@@ -54,16 +56,12 @@ struct Impl_MediaRequest { | |||
54 | iDocumentWidget *doc; | 56 | iDocumentWidget *doc; |
55 | iGmLinkId linkId; | 57 | iGmLinkId linkId; |
56 | iGmRequest * req; | 58 | iGmRequest * req; |
57 | iAtomicInt isUpdated; | ||
58 | }; | 59 | }; |
59 | 60 | ||
60 | static void updated_MediaRequest_(iAnyObject *obj) { | 61 | static void updated_MediaRequest_(iAnyObject *obj) { |
61 | iMediaRequest *d = obj; | 62 | iMediaRequest *d = obj; |
62 | int wasUpdated = exchange_Atomic(&d->isUpdated, iTrue); | ||
63 | if (!wasUpdated) { | ||
64 | postCommandf_App("media.updated link:%u request:%p", d->linkId, d); | 63 | postCommandf_App("media.updated link:%u request:%p", d->linkId, d); |
65 | } | 64 | } |
66 | } | ||
67 | 65 | ||
68 | static void finished_MediaRequest_(iAnyObject *obj) { | 66 | static void finished_MediaRequest_(iAnyObject *obj) { |
69 | iMediaRequest *d = obj; | 67 | iMediaRequest *d = obj; |
@@ -77,7 +75,6 @@ void init_MediaRequest(iMediaRequest *d, iDocumentWidget *doc, iGmLinkId linkId, | |||
77 | setUrl_GmRequest(d->req, url); | 75 | setUrl_GmRequest(d->req, url); |
78 | iConnect(GmRequest, d->req, updated, d, updated_MediaRequest_); | 76 | iConnect(GmRequest, d->req, updated, d, updated_MediaRequest_); |
79 | iConnect(GmRequest, d->req, finished, d, finished_MediaRequest_); | 77 | iConnect(GmRequest, d->req, finished, d, finished_MediaRequest_); |
80 | set_Atomic(&d->isUpdated, iFalse); | ||
81 | submit_GmRequest(d->req); | 78 | submit_GmRequest(d->req); |
82 | } | 79 | } |
83 | 80 | ||
@@ -105,8 +102,8 @@ struct Impl_Model { | |||
105 | }; | 102 | }; |
106 | 103 | ||
107 | void init_Model(iModel *d) { | 104 | void init_Model(iModel *d) { |
108 | d->history = new_History(); | 105 | d->history = new_History(); |
109 | d->url = new_String(); | 106 | d->url = new_String(); |
110 | } | 107 | } |
111 | 108 | ||
112 | void deinit_Model(iModel *d) { | 109 | void deinit_Model(iModel *d) { |
@@ -147,6 +144,8 @@ struct Impl_DocumentWidget { | |||
147 | iGmRequest * request; | 144 | iGmRequest * request; |
148 | iAtomicInt isRequestUpdated; /* request has new content, need to parse it */ | 145 | iAtomicInt isRequestUpdated; /* request has new content, need to parse it */ |
149 | iObjectList * media; | 146 | iObjectList * media; |
147 | iString sourceMime; | ||
148 | iBlock sourceContent; /* original content as received, for saving */ | ||
150 | iGmDocument * doc; | 149 | iGmDocument * doc; |
151 | int certFlags; | 150 | int certFlags; |
152 | iDate certExpiry; | 151 | iDate certExpiry; |
@@ -208,6 +207,8 @@ void init_DocumentWidget(iDocumentWidget *d) { | |||
208 | d->showLinkNumbers = iFalse; | 207 | d->showLinkNumbers = iFalse; |
209 | d->visBuf = new_VisBuf(); | 208 | d->visBuf = new_VisBuf(); |
210 | d->invalidRuns = new_PtrSet(); | 209 | d->invalidRuns = new_PtrSet(); |
210 | init_String(&d->sourceMime); | ||
211 | init_Block(&d->sourceContent, 0); | ||
211 | init_PtrArray(&d->visibleLinks); | 212 | init_PtrArray(&d->visibleLinks); |
212 | init_Click(&d->click, d, SDL_BUTTON_LEFT); | 213 | init_Click(&d->click, d, SDL_BUTTON_LEFT); |
213 | addChild_Widget(w, iClob(d->scroll = new_ScrollWidget())); | 214 | addChild_Widget(w, iClob(d->scroll = new_ScrollWidget())); |
@@ -225,6 +226,8 @@ void deinit_DocumentWidget(iDocumentWidget *d) { | |||
225 | delete_PtrSet(d->invalidRuns); | 226 | delete_PtrSet(d->invalidRuns); |
226 | iRelease(d->media); | 227 | iRelease(d->media); |
227 | iRelease(d->request); | 228 | iRelease(d->request); |
229 | deinit_Block(&d->sourceContent); | ||
230 | deinit_String(&d->sourceMime); | ||
228 | iRelease(d->doc); | 231 | iRelease(d->doc); |
229 | deinit_PtrArray(&d->visibleLinks); | 232 | deinit_PtrArray(&d->visibleLinks); |
230 | delete_String(d->certSubject); | 233 | delete_String(d->certSubject); |
@@ -269,7 +272,7 @@ static iRect documentBounds_DocumentWidget_(const iDocumentWidget *d) { | |||
269 | } | 272 | } |
270 | 273 | ||
271 | static int forceBreakWidth_DocumentWidget_(const iDocumentWidget *d) { | 274 | static int forceBreakWidth_DocumentWidget_(const iDocumentWidget *d) { |
272 | if (isLineWrapForced_App()) { | 275 | if (forceLineWrap_App()) { |
273 | const iRect bounds = bounds_Widget(constAs_Widget(d)); | 276 | const iRect bounds = bounds_Widget(constAs_Widget(d)); |
274 | const iRect docBounds = documentBounds_DocumentWidget_(d); | 277 | const iRect docBounds = documentBounds_DocumentWidget_(d); |
275 | return right_Rect(bounds) - left_Rect(docBounds) - gap_UI * d->pageMargin; | 278 | return right_Rect(bounds) - left_Rect(docBounds) - gap_UI * d->pageMargin; |
@@ -363,8 +366,12 @@ static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) { | |||
363 | if (isHover_Widget(w) && !contains_Widget(constAs_Widget(d->scroll), mouse)) { | 366 | if (isHover_Widget(w) && !contains_Widget(constAs_Widget(d->scroll), mouse)) { |
364 | setCursor_Window(get_Window(), | 367 | setCursor_Window(get_Window(), |
365 | d->hoverLink ? SDL_SYSTEM_CURSOR_HAND : SDL_SYSTEM_CURSOR_IBEAM); | 368 | d->hoverLink ? SDL_SYSTEM_CURSOR_HAND : SDL_SYSTEM_CURSOR_IBEAM); |
369 | if (d->hoverLink && | ||
370 | linkFlags_GmDocument(d->doc, d->hoverLink->linkId) & permanent_GmLinkFlag) { | ||
371 | setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW); /* not dismissable */ | ||
366 | } | 372 | } |
367 | } | 373 | } |
374 | } | ||
368 | 375 | ||
369 | static void updateVisible_DocumentWidget_(iDocumentWidget *d) { | 376 | static void updateVisible_DocumentWidget_(iDocumentWidget *d) { |
370 | const iRangei visRange = visibleRange_DocumentWidget_(d); | 377 | const iRangei visRange = visibleRange_DocumentWidget_(d); |
@@ -507,9 +514,17 @@ static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode | |||
507 | case certificateNotValid_GmStatusCode: | 514 | case certificateNotValid_GmStatusCode: |
508 | appendFormat_String(src, "\n\n%s", cstr_String(meta)); | 515 | appendFormat_String(src, "\n\n%s", cstr_String(meta)); |
509 | break; | 516 | break; |
510 | case unsupportedMimeType_GmStatusCode: | 517 | case unsupportedMimeType_GmStatusCode: { |
511 | appendFormat_String(src, "\n```\n%s\n```\n", cstr_String(meta)); | 518 | iString *key = collectNew_String(); |
519 | toString_Sym(SDLK_s, KMOD_PRIMARY, key); | ||
520 | appendFormat_String(src, | ||
521 | "\n```\n%s\n```\n" | ||
522 | "You can save it as a file to your Downloads folder, though. " | ||
523 | "Press %s or select Save Page from the menu.", | ||
524 | cstr_String(meta), | ||
525 | cstr_String(key)); | ||
512 | break; | 526 | break; |
527 | } | ||
513 | case slowDown_GmStatusCode: | 528 | case slowDown_GmStatusCode: |
514 | appendFormat_String(src, "\n\nWait %s seconds before your next request.", | 529 | appendFormat_String(src, "\n\nWait %s seconds before your next request.", |
515 | cstr_String(meta)); | 530 | cstr_String(meta)); |
@@ -534,6 +549,20 @@ static void updateTheme_DocumentWidget_(iDocumentWidget *d) { | |||
534 | } | 549 | } |
535 | } | 550 | } |
536 | 551 | ||
552 | static void updateFetchProgress_DocumentWidget_(iDocumentWidget *d) { | ||
553 | iLabelWidget *prog = findWidget_App("document.progress"); | ||
554 | const size_t dlSize = d->request ? size_Block(body_GmRequest(d->request)) : 0; | ||
555 | setFlags_Widget(as_Widget(prog), hidden_WidgetFlag, dlSize < 250000); | ||
556 | if (isVisible_Widget(prog)) { | ||
557 | updateText_LabelWidget(prog, | ||
558 | collectNewFormat_String("%s%.3f MB", | ||
559 | isFinished_GmRequest(d->request) | ||
560 | ? uiHeading_ColorEscape | ||
561 | : uiTextCaution_ColorEscape, | ||
562 | dlSize / 1.0e6f)); | ||
563 | } | ||
564 | } | ||
565 | |||
537 | static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse *response) { | 566 | static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse *response) { |
538 | if (d->state == ready_RequestState) { | 567 | if (d->state == ready_RequestState) { |
539 | return; | 568 | return; |
@@ -545,12 +574,15 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse | |||
545 | iString str; | 574 | iString str; |
546 | invalidate_DocumentWidget_(d); | 575 | invalidate_DocumentWidget_(d); |
547 | updateTheme_DocumentWidget_(d); | 576 | updateTheme_DocumentWidget_(d); |
577 | clear_String(&d->sourceMime); | ||
578 | // set_Block(&d->sourceContent, &response->body); | ||
548 | initBlock_String(&str, &response->body); | 579 | initBlock_String(&str, &response->body); |
549 | if (category_GmStatusCode(statusCode) == categorySuccess_GmStatusCode) { | 580 | if (category_GmStatusCode(statusCode) == categorySuccess_GmStatusCode) { |
550 | /* Check the MIME type. */ | 581 | /* Check the MIME type. */ |
551 | iRangecc charset = range_CStr("utf-8"); | 582 | iRangecc charset = range_CStr("utf-8"); |
552 | enum iGmDocumentFormat docFormat = undefined_GmDocumentFormat; | 583 | enum iGmDocumentFormat docFormat = undefined_GmDocumentFormat; |
553 | const iString *mimeStr = collect_String(lower_String(&response->meta)); /* for convenience */ | 584 | const iString *mimeStr = collect_String(lower_String(&response->meta)); /* for convenience */ |
585 | set_String(&d->sourceMime, mimeStr); | ||
554 | iRangecc mime = range_String(mimeStr); | 586 | iRangecc mime = range_String(mimeStr); |
555 | iRangecc seg = iNullRange; | 587 | iRangecc seg = iNullRange; |
556 | while (nextSplit_Rangecc(mime, ";", &seg)) { | 588 | while (nextSplit_Rangecc(mime, ";", &seg)) { |
@@ -558,12 +590,15 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse | |||
558 | trim_Rangecc(¶m); | 590 | trim_Rangecc(¶m); |
559 | if (equal_Rangecc(param, "text/plain")) { | 591 | if (equal_Rangecc(param, "text/plain")) { |
560 | docFormat = plainText_GmDocumentFormat; | 592 | docFormat = plainText_GmDocumentFormat; |
593 | setRange_String(&d->sourceMime, param); | ||
561 | } | 594 | } |
562 | else if (equal_Rangecc(param, "text/gemini")) { | 595 | else if (equal_Rangecc(param, "text/gemini")) { |
563 | docFormat = gemini_GmDocumentFormat; | 596 | docFormat = gemini_GmDocumentFormat; |
597 | setRange_String(&d->sourceMime, param); | ||
564 | } | 598 | } |
565 | else if (startsWith_Rangecc(param, "image/")) { | 599 | else if (startsWith_Rangecc(param, "image/")) { |
566 | docFormat = gemini_GmDocumentFormat; | 600 | docFormat = gemini_GmDocumentFormat; |
601 | setRange_String(&d->sourceMime, param); | ||
567 | if (!d->request || isFinished_GmRequest(d->request)) { | 602 | if (!d->request || isFinished_GmRequest(d->request)) { |
568 | /* Make a simple document with an image. */ | 603 | /* Make a simple document with an image. */ |
569 | const char *imageTitle = "Image"; | 604 | const char *imageTitle = "Image"; |
@@ -573,9 +608,9 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse | |||
573 | imageTitle = | 608 | imageTitle = |
574 | baseName_Path(collect_String(newRange_String(parts.path))).start; | 609 | baseName_Path(collect_String(newRange_String(parts.path))).start; |
575 | } | 610 | } |
576 | format_String( | 611 | format_String(&str, "=> %s %s\n", cstr_String(d->mod.url), imageTitle); |
577 | &str, "=> %s %s\n", cstr_String(d->mod.url), imageTitle); | 612 | setImage_GmDocument( |
578 | setImage_GmDocument(d->doc, 1, mimeStr, &response->body); | 613 | d->doc, 1, mimeStr, &response->body, iFalse /* it's fixed */); |
579 | } | 614 | } |
580 | else { | 615 | else { |
581 | clear_String(&str); | 616 | clear_String(&str); |
@@ -747,7 +782,9 @@ void setRedirectCount_DocumentWidget(iDocumentWidget *d, int count) { | |||
747 | } | 782 | } |
748 | 783 | ||
749 | iBool isRequestOngoing_DocumentWidget(const iDocumentWidget *d) { | 784 | iBool isRequestOngoing_DocumentWidget(const iDocumentWidget *d) { |
750 | return d->state == fetching_RequestState || d->state == receivedPartialResponse_RequestState; | 785 | /*return d->state == fetching_RequestState || |
786 | d->state == receivedPartialResponse_RequestState;*/ | ||
787 | return d->request != NULL; | ||
751 | } | 788 | } |
752 | 789 | ||
753 | static void scroll_DocumentWidget_(iDocumentWidget *d, int offset) { | 790 | static void scroll_DocumentWidget_(iDocumentWidget *d, int offset) { |
@@ -961,7 +998,9 @@ static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char * | |||
961 | return iFalse; /* not our request */ | 998 | return iFalse; /* not our request */ |
962 | } | 999 | } |
963 | if (equal_Command(cmd, "media.updated")) { | 1000 | if (equal_Command(cmd, "media.updated")) { |
964 | /* TODO: Show a progress indicator */ | 1001 | /* Update the link's progress. */ |
1002 | invalidateLink_DocumentWidget_(d, req->linkId); | ||
1003 | refresh_Widget(d); | ||
965 | return iTrue; | 1004 | return iTrue; |
966 | } | 1005 | } |
967 | else if (equal_Command(cmd, "media.finished")) { | 1006 | else if (equal_Command(cmd, "media.finished")) { |
@@ -974,7 +1013,7 @@ static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char * | |||
974 | // cstr_String(meta_GmRequest(req->req))); | 1013 | // cstr_String(meta_GmRequest(req->req))); |
975 | if (startsWith_String(meta_GmRequest(req->req), "image/")) { | 1014 | if (startsWith_String(meta_GmRequest(req->req), "image/")) { |
976 | setImage_GmDocument(d->doc, req->linkId, meta_GmRequest(req->req), | 1015 | setImage_GmDocument(d->doc, req->linkId, meta_GmRequest(req->req), |
977 | body_GmRequest(req->req)); | 1016 | body_GmRequest(req->req), iTrue); |
978 | updateVisible_DocumentWidget_(d); | 1017 | updateVisible_DocumentWidget_(d); |
979 | invalidate_DocumentWidget_(d); | 1018 | invalidate_DocumentWidget_(d); |
980 | refresh_Widget(as_Widget(d)); | 1019 | refresh_Widget(as_Widget(d)); |
@@ -999,20 +1038,6 @@ static void allocVisBuffer_DocumentWidget_(const iDocumentWidget *d) { | |||
999 | } | 1038 | } |
1000 | else { | 1039 | else { |
1001 | dealloc_VisBuf(d->visBuf); | 1040 | dealloc_VisBuf(d->visBuf); |
1002 | #if 0 | ||
1003 | iZap(d->visBuffer->validRange); | ||
1004 | d->visBuffer->size = size; | ||
1005 | iAssert(size.x > 0); | ||
1006 | iForIndices(i, d->visBuffer->texture) { | ||
1007 | d->visBuffer->texture[i] = | ||
1008 | SDL_CreateTexture(renderer_Window(get_Window()), | ||
1009 | SDL_PIXELFORMAT_RGBA8888, | ||
1010 | SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET, | ||
1011 | size.x, | ||
1012 | size.y); | ||
1013 | SDL_SetTextureBlendMode(d->visBuffer->texture[i], SDL_BLENDMODE_NONE); | ||
1014 | } | ||
1015 | #endif | ||
1016 | } | 1041 | } |
1017 | } | 1042 | } |
1018 | 1043 | ||
@@ -1054,6 +1079,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
1054 | updateTheme_DocumentWidget_(d); | 1079 | updateTheme_DocumentWidget_(d); |
1055 | updateTrust_DocumentWidget_(d, NULL); | 1080 | updateTrust_DocumentWidget_(d, NULL); |
1056 | updateSize_DocumentWidget(d); | 1081 | updateSize_DocumentWidget(d); |
1082 | updateFetchProgress_DocumentWidget_(d); | ||
1057 | } | 1083 | } |
1058 | updateWindowTitle_DocumentWidget_(d); | 1084 | updateWindowTitle_DocumentWidget_(d); |
1059 | allocVisBuffer_DocumentWidget_(d); | 1085 | allocVisBuffer_DocumentWidget_(d); |
@@ -1135,17 +1161,25 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
1135 | } | 1161 | } |
1136 | else if (equalWidget_Command(cmd, w, "document.request.updated") && | 1162 | else if (equalWidget_Command(cmd, w, "document.request.updated") && |
1137 | pointerLabel_Command(cmd, "request") == d->request) { | 1163 | pointerLabel_Command(cmd, "request") == d->request) { |
1164 | set_Block(&d->sourceContent, body_GmRequest(d->request)); | ||
1165 | if (document_App() == d) { | ||
1166 | updateFetchProgress_DocumentWidget_(d); | ||
1167 | } | ||
1138 | checkResponse_DocumentWidget_(d); | 1168 | checkResponse_DocumentWidget_(d); |
1169 | set_Atomic(&d->isRequestUpdated, iFalse); /* ready to be notified again */ | ||
1139 | return iFalse; | 1170 | return iFalse; |
1140 | } | 1171 | } |
1141 | else if (equalWidget_Command(cmd, w, "document.request.finished") && | 1172 | else if (equalWidget_Command(cmd, w, "document.request.finished") && |
1142 | pointerLabel_Command(cmd, "request") == d->request) { | 1173 | pointerLabel_Command(cmd, "request") == d->request) { |
1174 | set_Block(&d->sourceContent, body_GmRequest(d->request)); | ||
1175 | updateFetchProgress_DocumentWidget_(d); | ||
1143 | checkResponse_DocumentWidget_(d); | 1176 | checkResponse_DocumentWidget_(d); |
1144 | resetSmoothScroll_DocumentWidget_(d); | 1177 | resetSmoothScroll_DocumentWidget_(d); |
1145 | d->scrollY = d->initNormScrollY * size_GmDocument(d->doc).y; | 1178 | d->scrollY = d->initNormScrollY * size_GmDocument(d->doc).y; |
1146 | d->state = ready_RequestState; | 1179 | d->state = ready_RequestState; |
1147 | /* The response may be cached. */ { | 1180 | /* The response may be cached. */ { |
1148 | if (!equal_Rangecc(urlScheme_String(d->mod.url), "about")) { | 1181 | if (!equal_Rangecc(urlScheme_String(d->mod.url), "about") && |
1182 | startsWithCase_String(meta_GmRequest(d->request), "text/")) { | ||
1149 | setCachedResponse_History(d->mod.history, response_GmRequest(d->request)); | 1183 | setCachedResponse_History(d->mod.history, response_GmRequest(d->request)); |
1150 | } | 1184 | } |
1151 | } | 1185 | } |
@@ -1159,20 +1193,102 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
1159 | cancel_GmRequest(d->request); | 1193 | cancel_GmRequest(d->request); |
1160 | return iFalse; | 1194 | return iFalse; |
1161 | } | 1195 | } |
1196 | /* | ||
1162 | else if (equal_Command(cmd, "document.request.cancelled") && document_Command(cmd) == d) { | 1197 | else if (equal_Command(cmd, "document.request.cancelled") && document_Command(cmd) == d) { |
1163 | postCommand_App("navigate.back"); | 1198 | postCommand_App("navigate.back"); |
1164 | return iFalse; | 1199 | return iFalse; |
1165 | } | 1200 | } |
1201 | */ | ||
1202 | else if (equal_Command(cmd, "media.updated") || equal_Command(cmd, "media.finished")) { | ||
1203 | return handleMediaCommand_DocumentWidget_(d, cmd); | ||
1204 | } | ||
1166 | else if (equal_Command(cmd, "document.stop") && document_App() == d) { | 1205 | else if (equal_Command(cmd, "document.stop") && document_App() == d) { |
1167 | if (d->request) { | 1206 | if (d->request) { |
1168 | postCommandf_App("document.request.cancelled doc:%p url:%s", d, cstr_String(d->mod.url)); | 1207 | postCommandf_App( |
1208 | "document.request.cancelled doc:%p url:%s", d, cstr_String(d->mod.url)); | ||
1169 | iReleasePtr(&d->request); | 1209 | iReleasePtr(&d->request); |
1210 | if (d->state != ready_RequestState) { | ||
1170 | d->state = ready_RequestState; | 1211 | d->state = ready_RequestState; |
1212 | postCommand_App("navigate.back"); | ||
1213 | } | ||
1214 | updateFetchProgress_DocumentWidget_(d); | ||
1171 | return iTrue; | 1215 | return iTrue; |
1172 | } | 1216 | } |
1173 | } | 1217 | } |
1174 | else if (equal_Command(cmd, "media.updated") || equal_Command(cmd, "media.finished")) { | 1218 | else if (equal_Command(cmd, "document.save") && document_App() == d) { |
1175 | return handleMediaCommand_DocumentWidget_(d, cmd); | 1219 | if (d->request) { |
1220 | makeMessage_Widget(uiTextCaution_ColorEscape "PAGE INCOMPLETE", | ||
1221 | "The page contents are still being downloaded."); | ||
1222 | } | ||
1223 | else if (!isEmpty_Block(&d->sourceContent)) { | ||
1224 | /* Figure out a file name from the URL. */ | ||
1225 | /* TODO: Make this a utility function. */ | ||
1226 | iUrl parts; | ||
1227 | init_Url(&parts, d->mod.url); | ||
1228 | while (startsWith_Rangecc(parts.path, "/")) { | ||
1229 | parts.path.start++; | ||
1230 | } | ||
1231 | while (endsWith_Rangecc(parts.path, "/")) { | ||
1232 | parts.path.end--; | ||
1233 | } | ||
1234 | iString *name = collectNewCStr_String("pagecontent"); | ||
1235 | if (isEmpty_Range(&parts.path)) { | ||
1236 | if (!isEmpty_Range(&parts.host)) { | ||
1237 | setRange_String(name, parts.host); | ||
1238 | replace_Block(&name->chars, '.', '_'); | ||
1239 | } | ||
1240 | } | ||
1241 | else { | ||
1242 | iRangecc fn = { parts.path.start + lastIndexOfCStr_Rangecc(parts.path, "/") + 1, | ||
1243 | parts.path.end }; | ||
1244 | if (!isEmpty_Range(&fn)) { | ||
1245 | setRange_String(name, fn); | ||
1246 | } | ||
1247 | } | ||
1248 | iString *savePath = concat_Path(downloadDir_App(), name); | ||
1249 | if (lastIndexOfCStr_String(savePath, ".") == iInvalidPos) { | ||
1250 | /* No extension specified in URL. */ | ||
1251 | if (startsWith_String(&d->sourceMime, "text/gemini")) { | ||
1252 | appendCStr_String(savePath, ".gmi"); | ||
1253 | } | ||
1254 | else if (startsWith_String(&d->sourceMime, "text/")) { | ||
1255 | appendCStr_String(savePath, ".txt"); | ||
1256 | } | ||
1257 | else if (startsWith_String(&d->sourceMime, "image/")) { | ||
1258 | appendCStr_String(savePath, cstr_String(&d->sourceMime) + 6); | ||
1259 | if (fileExists_FileInfo(savePath)) { | ||
1260 | } | ||
1261 | } | ||
1262 | /* Make it unique. */ | ||
1263 | iDate now; | ||
1264 | initCurrent_Date(&now); | ||
1265 | size_t insPos = lastIndexOfCStr_String(savePath, "."); | ||
1266 | if (insPos == iInvalidPos) { | ||
1267 | insPos = size_String(savePath); | ||
1268 | } | ||
1269 | const iString *date = collect_String(format_Date(&now, "_%Y-%m-%d_%H%M%S")); | ||
1270 | insertData_Block(&savePath->chars, insPos, cstr_String(date), size_String(date)); | ||
1271 | } | ||
1272 | /* Write the file. */ { | ||
1273 | iFile *f = new_File(savePath); | ||
1274 | if (open_File(f, writeOnly_FileMode)) { | ||
1275 | write_File(f, &d->sourceContent); | ||
1276 | const size_t size = size_Block(&d->sourceContent); | ||
1277 | const iBool isMega = size >= 1000000; | ||
1278 | makeMessage_Widget(uiHeading_ColorEscape "PAGE SAVED", | ||
1279 | format_CStr("%s\nSize: %.3f %s", cstr_String(path_File(f)), | ||
1280 | isMega ? size / 1.0e6f : (size / 1.0e3f), | ||
1281 | isMega ? "MB" : "KB")); | ||
1282 | } | ||
1283 | else { | ||
1284 | makeMessage_Widget(uiTextCaution_ColorEscape "ERROR SAVING PAGE", | ||
1285 | strerror(errno)); | ||
1286 | } | ||
1287 | iRelease(f); | ||
1288 | } | ||
1289 | delete_String(savePath); | ||
1290 | } | ||
1291 | return iTrue; | ||
1176 | } | 1292 | } |
1177 | else if (equal_Command(cmd, "document.reload") && document_App() == d) { | 1293 | else if (equal_Command(cmd, "document.reload") && document_App() == d) { |
1178 | d->initNormScrollY = normScrollPos_DocumentWidget_(d); | 1294 | d->initNormScrollY = normScrollPos_DocumentWidget_(d); |
@@ -1368,7 +1484,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
1368 | case '`': { | 1484 | case '`': { |
1369 | iBlock *seed = new_Block(64); | 1485 | iBlock *seed = new_Block(64); |
1370 | for (size_t i = 0; i < 64; ++i) { | 1486 | for (size_t i = 0; i < 64; ++i) { |
1371 | setByte_Block(seed, i, iRandom(0, 255)); | 1487 | setByte_Block(seed, i, iRandom(0, 256)); |
1372 | } | 1488 | } |
1373 | setThemeSeed_GmDocument(d->doc, seed); | 1489 | setThemeSeed_GmDocument(d->doc, seed); |
1374 | delete_Block(seed); | 1490 | delete_Block(seed); |
@@ -1400,8 +1516,8 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
1400 | } | 1516 | } |
1401 | smoothScroll_DocumentWidget_( | 1517 | smoothScroll_DocumentWidget_( |
1402 | d, | 1518 | d, |
1403 | -3 * ev->wheel.y * lineHeight_Text(default_FontId), | 1519 | -3 * ev->wheel.y * lineHeight_Text(paragraph_FontId), |
1404 | gap_UI * smoothSpeed_DocumentWidget_ + | 1520 | gap_Text * smoothSpeed_DocumentWidget_ + |
1405 | (isSmoothScrolling_DocumentWidget_(d) ? d->smoothSpeed : 0)); | 1521 | (isSmoothScrolling_DocumentWidget_(d) ? d->smoothSpeed : 0)); |
1406 | #endif | 1522 | #endif |
1407 | d->noHoverWhileScrolling = iTrue; | 1523 | d->noHoverWhileScrolling = iTrue; |
@@ -1433,23 +1549,48 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
1433 | } | 1549 | } |
1434 | iArray items; | 1550 | iArray items; |
1435 | init_Array(&items, sizeof(iMenuItem)); | 1551 | init_Array(&items, sizeof(iMenuItem)); |
1552 | if (d->contextLink) { | ||
1553 | pushBackN_Array( | ||
1554 | &items, | ||
1555 | (iMenuItem[]){ { "Open Link in New Tab", | ||
1556 | 0, | ||
1557 | 0, | ||
1558 | format_CStr("!open newtab:1 url:%s", | ||
1559 | cstr_String(linkUrl_GmDocument( | ||
1560 | d->doc, d->contextLink->linkId))) }, | ||
1561 | { "---", 0, 0, NULL }, | ||
1562 | { "Copy Link", | ||
1563 | 0, | ||
1564 | 0, | ||
1565 | "document.copylink" }}, | ||
1566 | 3); | ||
1567 | } | ||
1568 | else { | ||
1569 | if (!isEmpty_Range(&d->selectMark)) { | ||
1436 | pushBackN_Array( | 1570 | pushBackN_Array( |
1437 | &items, | 1571 | &items, |
1572 | (iMenuItem[]){ { "Copy", 0, 0, "copy" }, { "---", 0, 0, NULL } }, | ||
1573 | 2); | ||
1574 | } | ||
1575 | pushBackN_Array( | ||
1576 | &items, | ||
1438 | (iMenuItem[]){ | 1577 | (iMenuItem[]){ |
1439 | { "Go Back", navigateBack_KeyShortcut, "navigate.back" }, | 1578 | { "Go Back", navigateBack_KeyShortcut, "navigate.back" }, |
1440 | { "Go Forward", navigateForward_KeyShortcut, "navigate.forward" }, | 1579 | { "Go Forward", navigateForward_KeyShortcut, "navigate.forward" }, |
1441 | { "Reload Page", reload_KeyShortcut, "navigate.reload" }, | 1580 | { "Reload Page", reload_KeyShortcut, "navigate.reload" }, |
1442 | { "---", 0, 0, NULL }, | 1581 | { "---", 0, 0, NULL }, |
1443 | { d->contextLink ? "Copy Link URL" : "Copy Page URL", | 1582 | { "Copy Page URL", 0, 0, "document.copylink" }, |
1444 | 0, | 1583 | { "---", 0, 0, NULL } }, |
1445 | 0, | ||
1446 | "document.copylink" }, | ||
1447 | { isEmpty_Range(&d->selectMark) ? "Copy Full Source" : "Copy Selected", | ||
1448 | 'c', | ||
1449 | KMOD_PRIMARY, | ||
1450 | "copy" }, | ||
1451 | }, | ||
1452 | 6); | 1584 | 6); |
1585 | if (isEmpty_Range(&d->selectMark)) { | ||
1586 | pushBackN_Array( | ||
1587 | &items, | ||
1588 | (iMenuItem[]){ | ||
1589 | { "Copy Page Source", 'c', KMOD_PRIMARY, "copy" }, | ||
1590 | { "Save to Downloads", SDLK_s, KMOD_PRIMARY, "document.save" } }, | ||
1591 | 2); | ||
1592 | } | ||
1593 | } | ||
1453 | d->menu = makeMenu_Widget(w, data_Array(&items), size_Array(&items)); | 1594 | d->menu = makeMenu_Widget(w, data_Array(&items), size_Array(&items)); |
1454 | deinit_Array(&items); | 1595 | deinit_Array(&items); |
1455 | } | 1596 | } |
@@ -1489,10 +1630,16 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
1489 | iAssert(linkId); | 1630 | iAssert(linkId); |
1490 | /* Media links are opened inline by default. */ | 1631 | /* Media links are opened inline by default. */ |
1491 | if (isMediaLink_GmDocument(d->doc, linkId)) { | 1632 | if (isMediaLink_GmDocument(d->doc, linkId)) { |
1633 | const int linkFlags = linkFlags_GmDocument(d->doc, linkId); | ||
1634 | if (linkFlags & content_GmLinkFlag && linkFlags & permanent_GmLinkFlag) { | ||
1635 | /* We have the image and it cannot be dismissed, so nothing | ||
1636 | further to do. */ | ||
1637 | return iTrue; | ||
1638 | } | ||
1492 | if (!requestMedia_DocumentWidget_(d, linkId)) { | 1639 | if (!requestMedia_DocumentWidget_(d, linkId)) { |
1493 | if (linkFlags_GmDocument(d->doc, linkId) & content_GmLinkFlag) { | 1640 | if (linkFlags & content_GmLinkFlag) { |
1494 | /* Dismiss shown content on click. */ | 1641 | /* Dismiss shown content on click. */ |
1495 | setImage_GmDocument(d->doc, linkId, NULL, NULL); | 1642 | setImage_GmDocument(d->doc, linkId, NULL, NULL, iTrue); |
1496 | d->hoverLink = NULL; | 1643 | d->hoverLink = NULL; |
1497 | scroll_DocumentWidget_(d, 0); | 1644 | scroll_DocumentWidget_(d, 0); |
1498 | updateVisible_DocumentWidget_(d); | 1645 | updateVisible_DocumentWidget_(d); |
@@ -1505,7 +1652,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
1505 | iMediaRequest *req = findMediaRequest_DocumentWidget_(d, linkId); | 1652 | iMediaRequest *req = findMediaRequest_DocumentWidget_(d, linkId); |
1506 | if (req) { | 1653 | if (req) { |
1507 | setImage_GmDocument(d->doc, linkId, meta_GmRequest(req->req), | 1654 | setImage_GmDocument(d->doc, linkId, meta_GmRequest(req->req), |
1508 | body_GmRequest(req->req)); | 1655 | body_GmRequest(req->req), iTrue); |
1509 | updateVisible_DocumentWidget_(d); | 1656 | updateVisible_DocumentWidget_(d); |
1510 | invalidate_DocumentWidget_(d); | 1657 | invalidate_DocumentWidget_(d); |
1511 | refresh_Widget(w); | 1658 | refresh_Widget(w); |
@@ -1671,8 +1818,8 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { | |||
1671 | const int flags = linkFlags_GmDocument(doc, run->linkId); | 1818 | const int flags = linkFlags_GmDocument(doc, run->linkId); |
1672 | const iRect linkRect = moved_Rect(run->visBounds, origin); | 1819 | const iRect linkRect = moved_Rect(run->visBounds, origin); |
1673 | iMediaRequest *mr = NULL; | 1820 | iMediaRequest *mr = NULL; |
1674 | /* Show inline content. */ | 1821 | /* Show metadata about inline content. */ |
1675 | if (flags & content_GmLinkFlag) { | 1822 | if (flags & content_GmLinkFlag && run->flags & endOfLine_GmRunFlag) { |
1676 | fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart); | 1823 | fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart); |
1677 | iAssert(!isEmpty_Rect(run->bounds)); | 1824 | iAssert(!isEmpty_Rect(run->bounds)); |
1678 | iGmImageInfo info; | 1825 | iGmImageInfo info; |
@@ -1704,7 +1851,8 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { | |||
1704 | draw_Text(metaFont, | 1851 | draw_Text(metaFont, |
1705 | topRight_Rect(linkRect), | 1852 | topRight_Rect(linkRect), |
1706 | tmInlineContentMetadata_ColorId, | 1853 | tmInlineContentMetadata_ColorId, |
1707 | " \u2014 Fetching\u2026"); | 1854 | " \u2014 Fetching\u2026 (%.1f MB)", |
1855 | (float) size_Block(body_GmRequest(mr->req)) / 1.0e6f); | ||
1708 | } | 1856 | } |
1709 | } | 1857 | } |
1710 | else if (isHover) { | 1858 | else if (isHover) { |
@@ -1713,20 +1861,23 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { | |||
1713 | const int flags = linkFlags_GmDocument(doc, linkId); | 1861 | const int flags = linkFlags_GmDocument(doc, linkId); |
1714 | iUrl parts; | 1862 | iUrl parts; |
1715 | init_Url(&parts, url); | 1863 | init_Url(&parts, url); |
1716 | fg = linkColor_GmDocument(doc, linkId, textHover_GmLinkPart); | 1864 | fg = linkColor_GmDocument(doc, linkId, textHover_GmLinkPart); |
1717 | const iBool showHost = (!isEmpty_Range(&parts.host) && flags & userFriendly_GmLinkFlag); | 1865 | const iBool showHost = (flags & humanReadable_GmLinkFlag && |
1866 | (!isEmpty_Range(&parts.host) || flags & mailto_GmLinkFlag)); | ||
1718 | const iBool showImage = (flags & imageFileExtension_GmLinkFlag) != 0; | 1867 | const iBool showImage = (flags & imageFileExtension_GmLinkFlag) != 0; |
1719 | const iBool showAudio = (flags & audioFileExtension_GmLinkFlag) != 0; | 1868 | const iBool showAudio = (flags & audioFileExtension_GmLinkFlag) != 0; |
1720 | iString str; | 1869 | iString str; |
1721 | init_String(&str); | 1870 | init_String(&str); |
1871 | /* Show scheme and host. */ | ||
1722 | if (run->flags & endOfLine_GmRunFlag && | 1872 | if (run->flags & endOfLine_GmRunFlag && |
1723 | (flags & (imageFileExtension_GmLinkFlag | audioFileExtension_GmLinkFlag) || | 1873 | (flags & (imageFileExtension_GmLinkFlag | audioFileExtension_GmLinkFlag) || |
1724 | showHost)) { | 1874 | showHost)) { |
1725 | format_String( | 1875 | format_String(&str, |
1726 | &str, | ||
1727 | " \u2014%s%s%s\r%c%s", | 1876 | " \u2014%s%s%s\r%c%s", |
1728 | showHost ? " " : "", | 1877 | showHost ? " " : "", |
1729 | showHost ? (!equalCase_Rangecc(parts.scheme, "gemini") | 1878 | showHost ? (flags & mailto_GmLinkFlag |
1879 | ? cstr_String(url) | ||
1880 | : ~flags & gemini_GmLinkFlag | ||
1730 | ? format_CStr("%s://%s", | 1881 | ? format_CStr("%s://%s", |
1731 | cstr_Rangecc(parts.scheme), | 1882 | cstr_Rangecc(parts.scheme), |
1732 | cstr_Rangecc(parts.host)) | 1883 | cstr_Rangecc(parts.host)) |
@@ -1735,7 +1886,8 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { | |||
1735 | showHost && (showImage || showAudio) ? " \u2014" : "", | 1886 | showHost && (showImage || showAudio) ? " \u2014" : "", |
1736 | showImage || showAudio | 1887 | showImage || showAudio |
1737 | ? asciiBase_ColorEscape + fg | 1888 | ? asciiBase_ColorEscape + fg |
1738 | : (asciiBase_ColorEscape + linkColor_GmDocument(doc, run->linkId, domain_GmLinkPart)), | 1889 | : (asciiBase_ColorEscape + |
1890 | linkColor_GmDocument(doc, run->linkId, domain_GmLinkPart)), | ||
1739 | showImage ? " View Image \U0001f5bc" | 1891 | showImage ? " View Image \U0001f5bc" |
1740 | : showAudio ? " Play Audio \U0001f3b5" : ""); | 1892 | : showAudio ? " Play Audio \U0001f3b5" : ""); |
1741 | } | 1893 | } |
diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index 11098c80..2d6d84dd 100644 --- a/src/ui/inputwidget.c +++ b/src/ui/inputwidget.c | |||
@@ -182,6 +182,7 @@ void setText_InputWidget(iInputWidget *d, const iString *text) { | |||
182 | pushBack_Array(&d->text, &i.value); | 182 | pushBack_Array(&d->text, &i.value); |
183 | } | 183 | } |
184 | iZap(d->mark); | 184 | iZap(d->mark); |
185 | d->cursor = iMin(d->cursor, size_Array(&d->text)); | ||
185 | refresh_Widget(as_Widget(d)); | 186 | refresh_Widget(as_Widget(d)); |
186 | } | 187 | } |
187 | 188 | ||
@@ -733,6 +734,7 @@ static void draw_InputWidget_(const iInputWidget *d) { | |||
733 | deinit_String(&cur); | 734 | deinit_String(&cur); |
734 | } | 735 | } |
735 | delete_String(text); | 736 | delete_String(text); |
737 | drawChildren_Widget(w); | ||
736 | } | 738 | } |
737 | 739 | ||
738 | iBeginDefineSubclass(InputWidget, Widget) | 740 | iBeginDefineSubclass(InputWidget, Widget) |
diff --git a/src/ui/labelwidget.c b/src/ui/labelwidget.c index 8b2506e7..28f43173 100644 --- a/src/ui/labelwidget.c +++ b/src/ui/labelwidget.c | |||
@@ -94,54 +94,7 @@ static iBool processEvent_LabelWidget_(iLabelWidget *d, const SDL_Event *ev) { | |||
94 | } | 94 | } |
95 | 95 | ||
96 | static void keyStr_LabelWidget_(const iLabelWidget *d, iString *str) { | 96 | static void keyStr_LabelWidget_(const iLabelWidget *d, iString *str) { |
97 | #if defined (iPlatformApple) | 97 | toString_Sym(d->key, d->kmods, str); |
98 | if (d->kmods & KMOD_CTRL) { | ||
99 | appendChar_String(str, 0x2303); | ||
100 | } | ||
101 | if (d->kmods & KMOD_ALT) { | ||
102 | appendChar_String(str, 0x2325); | ||
103 | } | ||
104 | if (d->kmods & KMOD_SHIFT) { | ||
105 | appendChar_String(str, 0x21e7); | ||
106 | } | ||
107 | if (d->kmods & KMOD_GUI) { | ||
108 | appendChar_String(str, 0x2318); | ||
109 | } | ||
110 | #else | ||
111 | if (d->kmods & KMOD_CTRL) { | ||
112 | appendCStr_String(str, "Ctrl+"); | ||
113 | } | ||
114 | if (d->kmods & KMOD_ALT) { | ||
115 | appendCStr_String(str, "Alt+"); | ||
116 | } | ||
117 | if (d->kmods & KMOD_SHIFT) { | ||
118 | appendCStr_String(str, "Shift+"); | ||
119 | } | ||
120 | if (d->kmods & KMOD_GUI) { | ||
121 | appendCStr_String(str, "Meta+"); | ||
122 | } | ||
123 | #endif | ||
124 | if (d->key == 0x20) { | ||
125 | appendCStr_String(str, "Space"); | ||
126 | } | ||
127 | else if (d->key == SDLK_LEFT) { | ||
128 | appendChar_String(str, 0x2190); | ||
129 | } | ||
130 | else if (d->key == SDLK_RIGHT) { | ||
131 | appendChar_String(str, 0x2192); | ||
132 | } | ||
133 | else if (d->key < 128 && (isalnum(d->key) || ispunct(d->key))) { | ||
134 | appendChar_String(str, upper_Char(d->key)); | ||
135 | } | ||
136 | else if (d->key == SDLK_BACKSPACE) { | ||
137 | appendChar_String(str, 0x232b); /* Erase to the Left */ | ||
138 | } | ||
139 | else if (d->key == SDLK_DELETE) { | ||
140 | appendChar_String(str, 0x2326); /* Erase to the Right */ | ||
141 | } | ||
142 | else { | ||
143 | appendCStr_String(str, SDL_GetKeyName(d->key)); | ||
144 | } | ||
145 | } | 98 | } |
146 | 99 | ||
147 | static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int *frame1, int *frame2) { | 100 | static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int *frame1, int *frame2) { |
diff --git a/src/ui/lookupwidget.c b/src/ui/lookupwidget.c index b7de5872..dcde7d79 100644 --- a/src/ui/lookupwidget.c +++ b/src/ui/lookupwidget.c | |||
@@ -265,7 +265,7 @@ static void searchIdentities_LookupJob_(iLookupJob *d) { | |||
265 | 265 | ||
266 | static iThreadResult worker_LookupWidget_(iThread *thread) { | 266 | static iThreadResult worker_LookupWidget_(iThread *thread) { |
267 | iLookupWidget *d = userData_Thread(thread); | 267 | iLookupWidget *d = userData_Thread(thread); |
268 | printf("[LookupWidget] worker is running\n"); fflush(stdout); | 268 | // printf("[LookupWidget] worker is running\n"); fflush(stdout); |
269 | lock_Mutex(d->mtx); | 269 | lock_Mutex(d->mtx); |
270 | for (;;) { | 270 | for (;;) { |
271 | wait_Condition(&d->jobAvailable, d->mtx); | 271 | wait_Condition(&d->jobAvailable, d->mtx); |
@@ -312,13 +312,13 @@ static iThreadResult worker_LookupWidget_(iThread *thread) { | |||
312 | /* Previous results haven't been taken yet. */ | 312 | /* Previous results haven't been taken yet. */ |
313 | delete_LookupJob(d->finishedJob); | 313 | delete_LookupJob(d->finishedJob); |
314 | } | 314 | } |
315 | printf("[LookupWidget] worker has %zu results\n", size_PtrArray(&job->results)); | 315 | // printf("[LookupWidget] worker has %zu results\n", size_PtrArray(&job->results)); |
316 | fflush(stdout); | 316 | fflush(stdout); |
317 | d->finishedJob = job; | 317 | d->finishedJob = job; |
318 | postCommand_Widget(as_Widget(d), "lookup.ready"); | 318 | postCommand_Widget(as_Widget(d), "lookup.ready"); |
319 | } | 319 | } |
320 | unlock_Mutex(d->mtx); | 320 | unlock_Mutex(d->mtx); |
321 | printf("[LookupWidget] worker has quit\n"); fflush(stdout); | 321 | // printf("[LookupWidget] worker has quit\n"); fflush(stdout); |
322 | return 0; | 322 | return 0; |
323 | } | 323 | } |
324 | 324 | ||
diff --git a/src/ui/util.c b/src/ui/util.c index 0af33138..ff6f8822 100644 --- a/src/ui/util.c +++ b/src/ui/util.c | |||
@@ -52,6 +52,57 @@ const char *command_UserEvent(const SDL_Event *d) { | |||
52 | return ""; | 52 | return ""; |
53 | } | 53 | } |
54 | 54 | ||
55 | void toString_Sym(int key, int kmods, iString *str) { | ||
56 | #if defined (iPlatformApple) | ||
57 | if (kmods & KMOD_CTRL) { | ||
58 | appendChar_String(str, 0x2303); | ||
59 | } | ||
60 | if (kmods & KMOD_ALT) { | ||
61 | appendChar_String(str, 0x2325); | ||
62 | } | ||
63 | if (kmods & KMOD_SHIFT) { | ||
64 | appendChar_String(str, 0x21e7); | ||
65 | } | ||
66 | if (kmods & KMOD_GUI) { | ||
67 | appendChar_String(str, 0x2318); | ||
68 | } | ||
69 | #else | ||
70 | if (kmods & KMOD_CTRL) { | ||
71 | appendCStr_String(str, "Ctrl+"); | ||
72 | } | ||
73 | if (kmods & KMOD_ALT) { | ||
74 | appendCStr_String(str, "Alt+"); | ||
75 | } | ||
76 | if (kmods & KMOD_SHIFT) { | ||
77 | appendCStr_String(str, "Shift+"); | ||
78 | } | ||
79 | if (kmods & KMOD_GUI) { | ||
80 | appendCStr_String(str, "Meta+"); | ||
81 | } | ||
82 | #endif | ||
83 | if (key == 0x20) { | ||
84 | appendCStr_String(str, "Space"); | ||
85 | } | ||
86 | else if (key == SDLK_LEFT) { | ||
87 | appendChar_String(str, 0x2190); | ||
88 | } | ||
89 | else if (key == SDLK_RIGHT) { | ||
90 | appendChar_String(str, 0x2192); | ||
91 | } | ||
92 | else if (key < 128 && (isalnum(key) || ispunct(key))) { | ||
93 | appendChar_String(str, upper_Char(key)); | ||
94 | } | ||
95 | else if (key == SDLK_BACKSPACE) { | ||
96 | appendChar_String(str, 0x232b); /* Erase to the Left */ | ||
97 | } | ||
98 | else if (key == SDLK_DELETE) { | ||
99 | appendChar_String(str, 0x2326); /* Erase to the Right */ | ||
100 | } | ||
101 | else { | ||
102 | appendCStr_String(str, SDL_GetKeyName(key)); | ||
103 | } | ||
104 | } | ||
105 | |||
55 | int keyMods_Sym(int kmods) { | 106 | int keyMods_Sym(int kmods) { |
56 | kmods &= (KMOD_SHIFT | KMOD_ALT | KMOD_CTRL | KMOD_GUI); | 107 | kmods &= (KMOD_SHIFT | KMOD_ALT | KMOD_CTRL | KMOD_GUI); |
57 | /* Don't treat left/right modifiers differently. */ | 108 | /* Don't treat left/right modifiers differently. */ |
@@ -192,6 +243,12 @@ iWidget *addAction_Widget(iWidget *parent, int key, int kmods, const char *comma | |||
192 | 243 | ||
193 | /*-----------------------------------------------------------------------------------------------*/ | 244 | /*-----------------------------------------------------------------------------------------------*/ |
194 | 245 | ||
246 | static iBool isCommandIgnoredByMenus_(const char *cmd) { | ||
247 | return equal_Command(cmd, "media.updated") || equal_Command(cmd, "document.request.updated") || | ||
248 | equal_Command(cmd, "window.resized") || | ||
249 | (equal_Command(cmd, "mouse.clicked") && !arg_Command(cmd)); /* button released */ | ||
250 | } | ||
251 | |||
195 | static iBool menuHandler_(iWidget *menu, const char *cmd) { | 252 | static iBool menuHandler_(iWidget *menu, const char *cmd) { |
196 | if (isVisible_Widget(menu)) { | 253 | if (isVisible_Widget(menu)) { |
197 | if (equalWidget_Command(cmd, menu, "menu.opened")) { | 254 | if (equalWidget_Command(cmd, menu, "menu.opened")) { |
@@ -201,13 +258,13 @@ static iBool menuHandler_(iWidget *menu, const char *cmd) { | |||
201 | /* Don't reopen self; instead, root will close the menu. */ | 258 | /* Don't reopen self; instead, root will close the menu. */ |
202 | return iFalse; | 259 | return iFalse; |
203 | } | 260 | } |
204 | if (equal_Command(cmd, "mouse.clicked") && arg_Command(cmd)) { | 261 | if ((equal_Command(cmd, "mouse.clicked") || equal_Command(cmd, "mouse.missed")) && |
262 | arg_Command(cmd)) { | ||
205 | /* Dismiss open menus when clicking outside them. */ | 263 | /* Dismiss open menus when clicking outside them. */ |
206 | closeMenu_Widget(menu); | 264 | closeMenu_Widget(menu); |
207 | return iTrue; | 265 | return iTrue; |
208 | } | 266 | } |
209 | if (!equal_Command(cmd, "window.resized") && | 267 | if (!isCommandIgnoredByMenus_(cmd)) { |
210 | !(equal_Command(cmd, "mouse.clicked") && !arg_Command(cmd)) /* ignore button release */) { | ||
211 | closeMenu_Widget(menu); | 268 | closeMenu_Widget(menu); |
212 | } | 269 | } |
213 | } | 270 | } |
@@ -252,6 +309,7 @@ void openMenu_Widget(iWidget *d, iInt2 coord) { | |||
252 | postCommand_App("cancel"); /* dismiss any other menus */ | 309 | postCommand_App("cancel"); /* dismiss any other menus */ |
253 | processEvents_App(postedEventsOnly_AppEventMode); | 310 | processEvents_App(postedEventsOnly_AppEventMode); |
254 | setFlags_Widget(d, hidden_WidgetFlag, iFalse); | 311 | setFlags_Widget(d, hidden_WidgetFlag, iFalse); |
312 | setFlags_Widget(d, commandOnMouseMiss_WidgetFlag, iTrue); | ||
255 | setFlags_Widget(findChild_Widget(d, "menu.cancel"), disabled_WidgetFlag, iFalse); | 313 | setFlags_Widget(findChild_Widget(d, "menu.cancel"), disabled_WidgetFlag, iFalse); |
256 | arrange_Widget(d); | 314 | arrange_Widget(d); |
257 | d->rect.pos = coord; | 315 | d->rect.pos = coord; |
@@ -681,10 +739,9 @@ void updateValueInput_Widget(iWidget *d, const char *title, const char *prompt) | |||
681 | 739 | ||
682 | static iBool messageHandler_(iWidget *msg, const char *cmd) { | 740 | static iBool messageHandler_(iWidget *msg, const char *cmd) { |
683 | /* Almost any command dismisses the sheet. */ | 741 | /* Almost any command dismisses the sheet. */ |
684 | // if (equal_Command(cmd, "menu.closed")) { | 742 | if (!(equal_Command(cmd, "media.updated") || equal_Command(cmd, "document.request.updated"))) { |
685 | // return iFalse; | 743 | destroy_Widget(msg); |
686 | // } | 744 | } |
687 | destroy_Widget(msg); | ||
688 | return iFalse; | 745 | return iFalse; |
689 | } | 746 | } |
690 | 747 | ||
@@ -755,14 +812,16 @@ iWidget *makePreferences_Widget(void) { | |||
755 | addChild_Widget(dlg, iClob(page)); | 812 | addChild_Widget(dlg, iClob(page)); |
756 | setFlags_Widget(page, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue); | 813 | setFlags_Widget(page, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue); |
757 | iWidget *headings = addChildFlags_Widget( | 814 | iWidget *headings = addChildFlags_Widget( |
758 | page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); | 815 | page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); |
759 | iWidget *values = addChildFlags_Widget( | 816 | iWidget *values = addChildFlags_Widget( |
760 | page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); | 817 | page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); |
818 | addChild_Widget(headings, iClob(makeHeading_Widget("Downloads folder:"))); | ||
819 | setId_Widget(addChild_Widget(values, iClob(new_InputWidget(0))), "prefs.downloads"); | ||
761 | #if defined (iPlatformApple) || defined (iPlatformMSys) | 820 | #if defined (iPlatformApple) || defined (iPlatformMSys) |
762 | addChild_Widget(headings, iClob(makeHeading_Widget("Use system theme:"))); | 821 | addChild_Widget(headings, iClob(makeHeading_Widget("Use system theme:"))); |
763 | addChild_Widget(values, iClob(makeToggle_Widget("prefs.ostheme"))); | 822 | addChild_Widget(values, iClob(makeToggle_Widget("prefs.ostheme"))); |
764 | #endif | 823 | #endif |
765 | addChild_Widget(headings, iClob(makeHeading_Widget("Theme:"))); | 824 | addChild_Widget(headings, iClob(makeHeading_Widget("Theme:"))); |
766 | iWidget *themes = new_Widget(); | 825 | iWidget *themes = new_Widget(); |
767 | /* Themes. */ { | 826 | /* Themes. */ { |
768 | setId_Widget(addChild_Widget(themes, iClob(new_LabelWidget("Pure Black", 0, 0, "theme.set arg:0"))), "prefs.theme.0"); | 827 | setId_Widget(addChild_Widget(themes, iClob(new_LabelWidget("Pure Black", 0, 0, "theme.set arg:0"))), "prefs.theme.0"); |
@@ -782,8 +841,9 @@ iWidget *makePreferences_Widget(void) { | |||
782 | addChild_Widget(headings, iClob(makeHeading_Widget("HTTP proxy:"))); | 841 | addChild_Widget(headings, iClob(makeHeading_Widget("HTTP proxy:"))); |
783 | setId_Widget(addChild_Widget(values, iClob(new_InputWidget(0))), "prefs.proxy.http"); | 842 | setId_Widget(addChild_Widget(values, iClob(new_InputWidget(0))), "prefs.proxy.http"); |
784 | arrange_Widget(dlg); | 843 | arrange_Widget(dlg); |
785 | /* Text input widths. */ { | 844 | /* Set text input widths. */ { |
786 | const int inputWidth = width_Rect(page->rect) - width_Rect(headings->rect); | 845 | const int inputWidth = width_Rect(page->rect) - width_Rect(headings->rect); |
846 | as_Widget(findChild_Widget(values, "prefs.downloads"))->rect.size.x = inputWidth; | ||
787 | as_Widget(findChild_Widget(values, "prefs.proxy.http"))->rect.size.x = inputWidth; | 847 | as_Widget(findChild_Widget(values, "prefs.proxy.http"))->rect.size.x = inputWidth; |
788 | as_Widget(findChild_Widget(values, "prefs.proxy.gopher"))->rect.size.x = inputWidth; | 848 | as_Widget(findChild_Widget(values, "prefs.proxy.gopher"))->rect.size.x = inputWidth; |
789 | } | 849 | } |
diff --git a/src/ui/util.h b/src/ui/util.h index 8ca9dd53..5590d008 100644 --- a/src/ui/util.h +++ b/src/ui/util.h | |||
@@ -49,6 +49,7 @@ iLocalDef iBool isResize_UserEvent(const SDL_Event *d) { | |||
49 | #endif | 49 | #endif |
50 | 50 | ||
51 | int keyMods_Sym (int kmods); /* shift, alt, control, or gui */ | 51 | int keyMods_Sym (int kmods); /* shift, alt, control, or gui */ |
52 | void toString_Sym (int key, int kmods, iString *str); | ||
52 | 53 | ||
53 | iRangei intersect_Rangei (iRangei a, iRangei b); | 54 | iRangei intersect_Rangei (iRangei a, iRangei b); |
54 | iRangei union_Rangei (iRangei a, iRangei b); | 55 | iRangei union_Rangei (iRangei a, iRangei b); |
diff --git a/src/ui/visbuf.c b/src/ui/visbuf.c index 64d861c6..8a66c300 100644 --- a/src/ui/visbuf.c +++ b/src/ui/visbuf.c | |||
@@ -37,6 +37,7 @@ void deinit_VisBuf(iVisBuf *d) { | |||
37 | 37 | ||
38 | void invalidate_VisBuf(iVisBuf *d) { | 38 | void invalidate_VisBuf(iVisBuf *d) { |
39 | iForIndices(i, d->buffers) { | 39 | iForIndices(i, d->buffers) { |
40 | d->buffers[i].origin = i * d->texSize.y; | ||
40 | iZap(d->buffers[i].validRange); | 41 | iZap(d->buffers[i].validRange); |
41 | } | 42 | } |
42 | } | 43 | } |
diff --git a/src/ui/widget.c b/src/ui/widget.c index d3f28b08..05bb62cc 100644 --- a/src/ui/widget.c +++ b/src/ui/widget.c | |||
@@ -542,6 +542,15 @@ iBool processEvent_Widget(iWidget *d, const SDL_Event *ev) { | |||
542 | break; | 542 | break; |
543 | } | 543 | } |
544 | } | 544 | } |
545 | if (d->flags & commandOnMouseMiss_WidgetFlag && ev->type == SDL_MOUSEBUTTONDOWN && | ||
546 | !contains_Widget(d, init_I2(ev->button.x, ev->button.y))) { | ||
547 | postCommand_Widget(d, | ||
548 | "mouse.missed arg:%d button:%d coord:%d %d", | ||
549 | ev->type == SDL_MOUSEBUTTONDOWN ? 1 : 0, | ||
550 | ev->button.button, | ||
551 | ev->button.x, | ||
552 | ev->button.y); | ||
553 | } | ||
545 | if (d->flags & mouseModal_WidgetFlag && isMouseEvent_(ev)) { | 554 | if (d->flags & mouseModal_WidgetFlag && isMouseEvent_(ev)) { |
546 | setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW); | 555 | setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW); |
547 | return iTrue; | 556 | return iTrue; |
diff --git a/src/ui/widget.h b/src/ui/widget.h index 25208c30..fa4fbe0f 100644 --- a/src/ui/widget.h +++ b/src/ui/widget.h | |||
@@ -58,6 +58,7 @@ enum iWidgetFlag { | |||
58 | tight_WidgetFlag = iBit(12), /* smaller padding */ | 58 | tight_WidgetFlag = iBit(12), /* smaller padding */ |
59 | keepOnTop_WidgetFlag = iBit(13), /* gets events first; drawn last */ | 59 | keepOnTop_WidgetFlag = iBit(13), /* gets events first; drawn last */ |
60 | mouseModal_WidgetFlag = iBit(14), /* eats all unprocessed mouse events */ | 60 | mouseModal_WidgetFlag = iBit(14), /* eats all unprocessed mouse events */ |
61 | commandOnMouseMiss_WidgetFlag = iBit(15), | ||
61 | /* arrange behavior */ | 62 | /* arrange behavior */ |
62 | fixedPosition_WidgetFlag = iBit(16), | 63 | fixedPosition_WidgetFlag = iBit(16), |
63 | arrangeHorizontal_WidgetFlag = iBit(17), /* arrange children horizontally */ | 64 | arrangeHorizontal_WidgetFlag = iBit(17), /* arrange children horizontally */ |
diff --git a/src/ui/window.c b/src/ui/window.c index 0a63a941..8ebb67a8 100644 --- a/src/ui/window.c +++ b/src/ui/window.c | |||
@@ -91,10 +91,13 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) { | |||
91 | #endif | 91 | #endif |
92 | 92 | ||
93 | #if !defined (iHaveNativeMenus) | 93 | #if !defined (iHaveNativeMenus) |
94 | /* TODO: Submenus wouldn't hurt here. */ | ||
94 | static const iMenuItem navMenuItems[] = { | 95 | static const iMenuItem navMenuItems[] = { |
95 | { "New Tab", 't', KMOD_PRIMARY, "tabs.new" }, | 96 | { "New Tab", 't', KMOD_PRIMARY, "tabs.new" }, |
96 | { "Open Location...", SDLK_l, KMOD_PRIMARY, "focus.set id:url" }, | 97 | { "Open Location...", SDLK_l, KMOD_PRIMARY, "focus.set id:url" }, |
97 | { "---", 0, 0, NULL }, | 98 | { "---", 0, 0, NULL }, |
99 | { "Save to Downloads", SDLK_s, KMOD_PRIMARY, "document.save" }, | ||
100 | { "---", 0, 0, NULL }, | ||
98 | { "Copy Source Text", SDLK_c, KMOD_PRIMARY, "copy" }, | 101 | { "Copy Source Text", SDLK_c, KMOD_PRIMARY, "copy" }, |
99 | { "Bookmark This Page", SDLK_d, KMOD_PRIMARY, "bookmark.add" }, | 102 | { "Bookmark This Page", SDLK_d, KMOD_PRIMARY, "bookmark.add" }, |
100 | { "---", 0, 0, NULL }, | 103 | { "---", 0, 0, NULL }, |
@@ -104,7 +107,7 @@ static const iMenuItem navMenuItems[] = { | |||
104 | { "Reset Zoom", SDLK_0, KMOD_PRIMARY, "zoom.set arg:100" }, | 107 | { "Reset Zoom", SDLK_0, KMOD_PRIMARY, "zoom.set arg:100" }, |
105 | { "---", 0, 0, NULL }, | 108 | { "---", 0, 0, NULL }, |
106 | { "Preferences...", SDLK_COMMA, KMOD_PRIMARY, "preferences" }, | 109 | { "Preferences...", SDLK_COMMA, KMOD_PRIMARY, "preferences" }, |
107 | { "Help", 0, 0, "!open url:about:help" }, | 110 | { "Help", SDLK_F1, 0, "!open url:about:help" }, |
108 | { "Release Notes", 0, 0, "!open url:about:version" }, | 111 | { "Release Notes", 0, 0, "!open url:about:version" }, |
109 | { "---", 0, 0, NULL }, | 112 | { "---", 0, 0, NULL }, |
110 | { "Quit Lagrange", 'q', KMOD_PRIMARY, "quit" } | 113 | { "Quit Lagrange", 'q', KMOD_PRIMARY, "quit" } |
@@ -116,6 +119,8 @@ static const iMenuItem navMenuItems[] = { | |||
116 | static const iMenuItem fileMenuItems[] = { | 119 | static const iMenuItem fileMenuItems[] = { |
117 | { "New Tab", SDLK_t, KMOD_PRIMARY, "tabs.new" }, | 120 | { "New Tab", SDLK_t, KMOD_PRIMARY, "tabs.new" }, |
118 | { "Open Location...", SDLK_l, KMOD_PRIMARY, "focus.set id:url" }, | 121 | { "Open Location...", SDLK_l, KMOD_PRIMARY, "focus.set id:url" }, |
122 | { "---", 0, 0, NULL }, | ||
123 | { "Save to Downloads", SDLK_s, KMOD_PRIMARY, "document.save" }, | ||
119 | }; | 124 | }; |
120 | 125 | ||
121 | static const iMenuItem editMenuItems[] = { | 126 | static const iMenuItem editMenuItems[] = { |
@@ -362,12 +367,24 @@ static void setupUserInterface_Window(iWindow *d) { | |||
362 | setId_Widget(as_Widget(lock), "navbar.lock"); | 367 | setId_Widget(as_Widget(lock), "navbar.lock"); |
363 | setFont_LabelWidget(lock, defaultSymbols_FontId); | 368 | setFont_LabelWidget(lock, defaultSymbols_FontId); |
364 | updateTextCStr_LabelWidget(lock, "\U0001f512"); | 369 | updateTextCStr_LabelWidget(lock, "\U0001f512"); |
365 | iInputWidget *url = new_InputWidget(0); | 370 | /* URL input field. */ { |
366 | setSelectAllOnFocus_InputWidget(url, iTrue); | 371 | iInputWidget *url = new_InputWidget(0); |
367 | setId_Widget(as_Widget(url), "url"); | 372 | setSelectAllOnFocus_InputWidget(url, iTrue); |
368 | setNotifyEdits_InputWidget(url, iTrue); | 373 | setId_Widget(as_Widget(url), "url"); |
369 | setTextCStr_InputWidget(url, "gemini://"); | 374 | setNotifyEdits_InputWidget(url, iTrue); |
370 | addChildFlags_Widget(navBar, iClob(url), expand_WidgetFlag); | 375 | setTextCStr_InputWidget(url, "gemini://"); |
376 | addChildFlags_Widget(navBar, iClob(url), expand_WidgetFlag); | ||
377 | /* Download progress indicator is inside the input field, but hidden normally. */ | ||
378 | setPadding_Widget(as_Widget(url),0, 0, gap_UI * 1, 0); | ||
379 | iLabelWidget *progress = new_LabelWidget(uiTextCaution_ColorEscape "00.000 MB", 0, 0, NULL); | ||
380 | setId_Widget(as_Widget(progress), "document.progress"); | ||
381 | setAlignVisually_LabelWidget(progress, iTrue); | ||
382 | shrink_Rect(&as_Widget(progress)->rect, init_I2(0, gap_UI)); | ||
383 | addChildFlags_Widget(as_Widget(url), | ||
384 | iClob(progress), | ||
385 | moveToParentRightEdge_WidgetFlag); | ||
386 | setBackgroundColor_Widget(as_Widget(progress), uiBackground_ColorId); | ||
387 | } | ||
371 | setId_Widget(addChild_Widget( | 388 | setId_Widget(addChild_Widget( |
372 | navBar, iClob(newIcon_LabelWidget(reloadCStr_, 0, 0, "navigate.reload"))), | 389 | navBar, iClob(newIcon_LabelWidget(reloadCStr_, 0, 0, "navigate.reload"))), |
373 | "reload"); | 390 | "reload"); |
@@ -477,26 +494,36 @@ static void drawBlank_Window_(iWindow *d) { | |||
477 | SDL_RenderPresent(d->render); | 494 | SDL_RenderPresent(d->render); |
478 | } | 495 | } |
479 | 496 | ||
480 | // #define ENABLE_SWRENDER | 497 | iBool create_Window_(iWindow *d, iRect rect, uint32_t flags) { |
498 | flags |= SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI; | ||
499 | if (SDL_CreateWindowAndRenderer( | ||
500 | width_Rect(rect), height_Rect(rect), flags, &d->win, &d->render)) { | ||
501 | return iFalse; | ||
502 | } | ||
503 | return iTrue; | ||
504 | } | ||
481 | 505 | ||
482 | void init_Window(iWindow *d, iRect rect) { | 506 | void init_Window(iWindow *d, iRect rect) { |
483 | theWindow_ = d; | 507 | theWindow_ = d; |
484 | iZap(d->cursors); | 508 | iZap(d->cursors); |
509 | d->initialPos = rect.pos; | ||
485 | d->pendingCursor = NULL; | 510 | d->pendingCursor = NULL; |
486 | d->isDrawFrozen = iTrue; | 511 | d->isDrawFrozen = iTrue; |
487 | uint32_t flags = SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI; | 512 | uint32_t flags = 0; |
488 | #if defined (ENABLE_SWRENDER) | 513 | #if defined (iPlatformApple) |
489 | SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software"); | ||
490 | #elif defined (iPlatformApple) | ||
491 | SDL_SetHint(SDL_HINT_RENDER_DRIVER, "metal"); | 514 | SDL_SetHint(SDL_HINT_RENDER_DRIVER, "metal"); |
492 | #else | 515 | #else |
493 | flags |= SDL_WINDOW_OPENGL; | 516 | flags |= SDL_WINDOW_OPENGL; |
494 | #endif | 517 | #endif |
495 | SDL_SetHint(SDL_HINT_RENDER_VSYNC, "1"); | 518 | SDL_SetHint(SDL_HINT_RENDER_VSYNC, "1"); |
496 | if (SDL_CreateWindowAndRenderer( | 519 | /* First try SDL's default renderer that should be the best option. */ |
497 | width_Rect(rect), height_Rect(rect), flags, &d->win, &d->render)) { | 520 | if (forceSoftwareRender_App() || !create_Window_(d, rect, flags)) { |
498 | fprintf(stderr, "Error when creating window: %s\n", SDL_GetError()); | 521 | /* No luck, maybe software only? This should always work as long as there is a display. */ |
499 | exit(-2); | 522 | SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software"); |
523 | if (!create_Window_(d, rect, 0)) { | ||
524 | fprintf(stderr, "Error when creating window: %s\n", SDL_GetError()); | ||
525 | exit(-2); | ||
526 | } | ||
500 | } | 527 | } |
501 | if (left_Rect(rect) >= 0) { | 528 | if (left_Rect(rect) >= 0) { |
502 | SDL_SetWindowPosition(d->win, left_Rect(rect), top_Rect(rect)); | 529 | SDL_SetWindowPosition(d->win, left_Rect(rect), top_Rect(rect)); |
@@ -571,6 +598,16 @@ SDL_Renderer *renderer_Window(const iWindow *d) { | |||
571 | 598 | ||
572 | static iBool handleWindowEvent_Window_(iWindow *d, const SDL_WindowEvent *ev) { | 599 | static iBool handleWindowEvent_Window_(iWindow *d, const SDL_WindowEvent *ev) { |
573 | switch (ev->event) { | 600 | switch (ev->event) { |
601 | #if defined (LAGRANGE_ENABLE_WINDOWPOS_FIX) | ||
602 | case SDL_WINDOWEVENT_EXPOSED: | ||
603 | if (d->initialPos.x >= 0) { | ||
604 | int bx, by; | ||
605 | SDL_GetWindowBordersSize(d->win, &by, &bx, NULL, NULL); | ||
606 | SDL_SetWindowPosition(d->win, d->initialPos.x + bx, d->initialPos.y + by); | ||
607 | d->initialPos = init1_I2(-1); | ||
608 | } | ||
609 | return iFalse; | ||
610 | #endif | ||
574 | case SDL_WINDOWEVENT_MOVED: | 611 | case SDL_WINDOWEVENT_MOVED: |
575 | /* No need to do anything. */ | 612 | /* No need to do anything. */ |
576 | return iTrue; | 613 | return iTrue; |
diff --git a/src/ui/window.h b/src/ui/window.h index 4aec2fa7..b067d30e 100644 --- a/src/ui/window.h +++ b/src/ui/window.h | |||
@@ -34,6 +34,7 @@ iDeclareTypeConstructionArgs(Window, iRect rect) | |||
34 | 34 | ||
35 | struct Impl_Window { | 35 | struct Impl_Window { |
36 | SDL_Window * win; | 36 | SDL_Window * win; |
37 | iInt2 initialPos; | ||
37 | iBool isDrawFrozen; /* avoids premature draws while restoring window state */ | 38 | iBool isDrawFrozen; /* avoids premature draws while restoring window state */ |
38 | SDL_Renderer *render; | 39 | SDL_Renderer *render; |
39 | iWidget * root; | 40 | iWidget * root; |