diff --git a/.appveyor.yml b/.appveyor.yml index 837ebb1d4..f2424fc94 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -7,7 +7,7 @@ environment: PYTHONUNBUFFERED: 1 PYTHON: C:\Python36\python.exe matrix: - - TESTENV: py36-pyqt59 + - TESTENV: py36-pyqt510 - TESTENV: pylint install: diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e29d2678d..d7da20300 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,3 +8,5 @@ tests/unit/completion/* @rcorre tests/unit/misc/test_sql.py @rcorre qutebrowser/config/configdata.yml @mschilli87 + +qutebrowser/javascript/caret.js @artur-shaik diff --git a/.gitignore b/.gitignore index cb244557b..85233aa2f 100644 --- a/.gitignore +++ b/.gitignore @@ -25,18 +25,19 @@ __pycache__ /.tox /testresults.html /.cache +/.pytest_cache /.testmondata /.hypothesis /.mypy_cache /prof /venv TODO -/scripts/testbrowser_cpp/webkit/Makefile -/scripts/testbrowser_cpp/webkit/main.o -/scripts/testbrowser_cpp/webkit/testbrowser -/scripts/testbrowser_cpp/webkit/.qmake.stash -/scripts/testbrowser_cpp/webengine/Makefile -/scripts/testbrowser_cpp/webengine/main.o -/scripts/testbrowser_cpp/webengine/testbrowser -/scripts/testbrowser_cpp/webengine/.qmake.stash +/scripts/testbrowser/cpp/webkit/Makefile +/scripts/testbrowser/cpp/webkit/main.o +/scripts/testbrowser/cpp/webkit/testbrowser +/scripts/testbrowser/cpp/webkit/.qmake.stash +/scripts/testbrowser/cpp/webengine/Makefile +/scripts/testbrowser/cpp/webengine/main.o +/scripts/testbrowser/cpp/webengine/testbrowser +/scripts/testbrowser/cpp/webengine/.qmake.stash /scripts/dev/pylint_checkers/qute_pylint.egg-info diff --git a/.travis.yml b/.travis.yml index 251842d06..d5ebb1dec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,6 +21,17 @@ matrix: env: TESTENV=py35-pyqt59 - os: linux env: TESTENV=py36-pyqt59-cov + - os: linux + env: TESTENV=py36-pyqt510 + # We need a newer Xvfb as a WORKAROUND for: + # https://bugreports.qt.io/browse/QTBUG-64928 + sudo: required + addons: + apt: + sources: + - sourceline: "deb http://us.archive.ubuntu.com/ubuntu/ xenial main universe" + packages: + - xvfb - os: osx env: TESTENV=py36 OSX=sierra osx_image: xcode9.2 diff --git a/MANIFEST.in b/MANIFEST.in index 54bb613f3..9dace6f98 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,7 +8,7 @@ graft icons graft doc/img graft misc/apparmor graft misc/userscripts -recursive-include scripts *.py *.sh +recursive-include scripts *.py *.sh *.js include qutebrowser/utils/testfile include qutebrowser/git-commit-id include LICENSE doc/* README.asciidoc @@ -23,7 +23,7 @@ include qutebrowser/config/configdata.yml prune www prune scripts/dev -prune scripts/testbrowser_cpp +prune scripts/testbrowser/cpp prune .github exclude scripts/asciidoc2html.py exclude doc/notes diff --git a/README.asciidoc b/README.asciidoc index a625f317c..c141aa0c3 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -44,8 +44,8 @@ Documentation In addition to the topics mentioned in this README, the following documents are available: -* https://qutebrowser.org/img/cheatsheet-big.png[Key binding cheatsheet]: + -image:https://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding cheatsheet",link="https://qutebrowser.org/img/cheatsheet-big.png"] +* https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/cheatsheet-big.png[Key binding cheatsheet]: + +image:https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/cheatsheet-small.png["qutebrowser key binding cheatsheet",link="https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/cheatsheet-big.png"] * link:doc/quickstart.asciidoc[Quick start guide] * https://www.shortcutfoo.com/app/dojos/qutebrowser[Free training course] to remember those key bindings * link:doc/faq.asciidoc[Frequently asked questions] @@ -114,7 +114,7 @@ The following software and libraries are required to run qutebrowser: * http://fdik.org/pyPEG/[pyPEG2] * http://jinja.pocoo.org/[jinja2] * http://pygments.org/[pygments] -* http://pyyaml.org/wiki/PyYAML[PyYAML] +* https://github.com/yaml/pyyaml[PyYAML] * http://www.attrs.org/[attrs] The following libraries are optional: diff --git a/doc/backers.asciidoc b/doc/backers.asciidoc index 2dd6f52b3..80f46fd6e 100644 --- a/doc/backers.asciidoc +++ b/doc/backers.asciidoc @@ -13,47 +13,75 @@ Thanks a lot to the following people who contributed to it: Gold sponsors ~~~~~~~~~~~~~ -TODO +- Iggy +- zwitschi +- 2x Anonymous Silver sponsors ~~~~~~~~~~~~~~~ -TODO +- https://benary.org[benaryorg] +- https://scratchbook.ch[Claude] +- Martin Tournoij +- http://supported.elsensohn.ch[Thomas Elsensohn] +- Christian Helbling +- Gavin Troy +- Chris King-Parra +- Tim Das Mool Wegener Other sponsors ~~~~~~~~~~~~~~ -TODO: people with t-shirts or higher pledge levels - - 7scan +- AMD1212 +- Alex - Alex Suykov - Alexey Zhikhartsev - Allan Nordhøy - Anirudh Sanjeev - Anssi Puustinen +- Anton Grensjö +- Aristaeus +- Armin Fisslthaler +- Ashley Hauck - Benedikt Steindorf - Bernardo Kuri - Blaise Duszynski - Bostan - Bruno Oliveira +- BunnyApocalypse +- Christian Kellermann - Colin Jacobs - Daniel Andersson +- Daniel Nelson +- Daniel P. Schmidt +- Daniel Salby - Danilo - David Beley - David Hollings +- David Keijser - David Parrish - Derin Yarsuvat - Dmytro Kostiuchenko +- Eero Kari +- Epictek +- Eric +- Faure Hu +- Ferus - Frederik Thorøe - G4v4g4i +- Granitosaurus - Gyula Teleki - H +- Heinz Bruhin - Hosaka +- Ihor Radchenko - Iordanis Grigoriou - Isaac Sandaljian - Jakub Podeszwik - Jamie Anderson - Jasper Woudenberg +- Jay Kamat - Jens Højgaard - Johannes - John Baber-Lucero @@ -61,9 +89,11 @@ TODO: people with t-shirts or higher pledge levels - Kenichiro Ito - Kenny Low - Lars Ivar Igesund +- Leulas - Lucas Aride Moulin - Ludovic Chabant - Lukas Gierth +- Magnus Lindström - Marulkan - Matthew Chun-Lum - Matthew Cronen @@ -80,7 +110,10 @@ TODO: people with t-shirts or higher pledge levels - Peter Rice - Philipp Middendorf - Pkill9 +- PluMGMK - Prescott +- ProXicT +- Ram-Z - Robotichead - Roshless - Ryan Ellis @@ -90,35 +123,53 @@ TODO: people with t-shirts or higher pledge levels - Sean Herman - Sebastian Frysztak - Shelby Cruver +- Simon Désaulniers - SirCmpwn - Soham Pal +- Stephan Jauernick - Stewart Webb - Sven Reinecke +- Timothée Floure - Tom Bass +- Tom Kirchner - Tomas Slusny - Tomasz Kramkowski - Tommy Thomas +- Tuscan +- Ulrich Pötter - Vasilij Schneidermann - Vlaaaaaaad +- XTaran +- Z2h-A6n +- ayekat - beanieuptop +- cee +- craftyguy - demure +- dlangevi +- epon - evenorbert - fishss - gsnewmark - guillermohs9 +- hernani - hubcaps +- jnphilipp - lobachevsky - neodarz - nihlaeth - notbenh +- nyctea +- ongy - patrick suwanvithaya - pyratebeard +- p≡p foundation - randm_dave - sabreman - toml - vimja - wiz -- 44 Anonymous +- 48 Anonymous 2016 ---- diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 4e51f1c5b..e85d9a188 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -15,54 +15,168 @@ breaking changes (such as renamed commands) can happen in minor releases. // `Fixed` for any bug fixes. // `Security` to invite users to upgrade in case of vulnerabilities. -v1.1.0 (unreleased) +v1.2.0 (unreleased) ------------------- Added ~~~~~ +- Initial implementation of per-domain settings: + * `:set` and `:config-cycle` now have a `-u`/`--pattern` argument taking a + https://developer.chrome.com/extensions/match_patterns[URL match pattern] + for supported settings. + * `config.set` in `config.py` now takes a third argument which is the pattern. + * New `with config.pattern('...') as p:` context manager for `config.py` to + use the shorthand syntax with a pattern. + * New `tsh` keybinding to toggle scripts for the current host. With a capital + `S`, the toggle is saved. With a capital `H`, subdomains are included. + * New `tsu` keybinding to toggle scripts for the current URL. With a capital + `S`, the toggle is saved. +- QtWebEngine: Caret/visual mode is now supported. +- QtWebEngine: Authentication via ~/.netrc is now supported. +- A new `qute://bindings` page, opened by `:bind`, shows all keybindings. +- `:session-load` has a new `--delete` flag which deletes the + session after loading it. +- QtWebEngine: Retrying downloads is now supported with Qt 5.10 or newer. +- QtWebEngine: Hinting and other features inside same-origin frames is now + supported. +- New `cycle-inputs.js` script in `scripts/` which can be used with `:jseval -f` + to cycle through inputs. +- New `--no-last` flag for `:tab-focus` to not focus the last tab when focusing + the currently focused one. +- New `--edit` flag for `:view-source` to open the source in an external editor. +- New `statusbar.widgets` setting to configure which widgets should be shown in + which order in the statusbar. +- New `:prompt-yank` command (bound to `Alt-y` by default) to yank URLs + referenced in prompts. +- The `hostblock_blame` script which was removed in v1.0 was updated for the new + config and re-added. +- New `qute://tabs` page (opened via `:buffer`) which lists all tabs. + +Changed +~~~~~~~ + +- The `hist_importer.py` script now only imports URL schemes qutebrowser can + handle. +- Deleting a prefix (`:`, `/` or `?`) via backspace now leaves command mode. +- Angular 1 elements now get hints assigned. +- `:tab-only` with pinned tabs now still closes unpinned tabs. +- GreaseMonkey `@include` and `@exclude` now support + regex matches. With QtWebEngine and Qt 5.8 and newer, Qt handles the matching, + but similar functionality was added in Qt 5.11. +- The sqlite history now uses write-ahead logging which should be + a performance and stability improvement. +- The `url.incdec_segments` option now also can take `port` as possible segment. +- QtWebEngine: `:view-source` now uses Chromium's `view-source:` scheme. +- Tabs now show their full title as tooltip. +- When an editor is spawned with `:open-editor` and `:config-edit`, the changes + are now applied as soon as the file is saved in the editor. +- When there are multiple unknown keys in a autoconfig.yml, they now all get + reported in one error. +- New `tabs.mode_on_change` setting which replaces + `tabs.persist_mode_on_change`. It can now be set to `restore` which remembers + input modes (input/passthrough) per tab. +- More performance improvements when opening/closing many tabs. +- The `:version` page now has a button to pastebin the information. +- Replacements like `{url}` can now be replaced as `{{url}}`. + +Fixed +~~~~~ + +- QtWebEngine: Improved fullscreen handling with Qt 5.10. +- QtWebEngine: Hinting and scrolling now works properly on special + `view-source:` pages. +- QtWebEngine: Scroll positions are now restored correctly from sessions. +- QtWebEngine: Crash with Qt 5.10.1 when using :undo on some tabs. +- QtWebEngine: `:follow-selected` should now work in more cases with Qt > 5.10. +- QtWebKit: `:view-source` now displays a valid URL. +- URLs containing ampersands and other special chars are now shown + correctly when filtering them in the completion. +- `:bookmark-add "" foo` can now be used to save the current URL with a custom + title. +- `:spawn -o` now waits until the process has finished before trying to show the + output. Previously, it incorrectly showed the previous output immediately. +- QtWebEngine: Qt download objects are now cleaned up properly when a download + is removed. +- Suspended pages now should always load the correct page when being un-suspended. +- Compatibility with Python 3.7 +- Exception types are now shown properly with `:config-source` and `:config-edit`. +- When using `:bookmark-add --toggle`, bookmarks are now saved properly. + +Removed +~~~~~~~ + +- `QUTE_SELECTED_HTML` is now not set for userscripts anymore except when called + via hints. +- The `qutebrowser_viewsource` userscript has been removed as `:view-source + --edit` can now be used. +- The `tabs.persist_mode_on_change` setting has been removed and replaced by + `tabs.mode_on_change`. + +v1.1.1 +------ + +Fixed +~~~~~ + +- The Makefile now actually works. +- Fixed crashes with Qt 5.10 when closing a tab before it finished loading. + +v1.1.0 +------ + +Added +~~~~~ + - Initial support for Greasemonkey scripts. There are still some rough edges, but many scripts should already work. - There's now a `misc/Makefile` file in releases, which should help distributions which package qutebrowser, as they can run something like `make -f misc/Makefile DESTDIR="$pkgdir" install` now. -- New `{current_url}` field for `window.title_format` and `tabs.title.format`. -- New `colors.statusbar.passthrough.fg`/`.bg` settings. -- New `completion.delay` and `completion.min_chars` settings to update the - completion less often. -- New `completion.use_best_match` setting to automatically use the best-matching - command in the completion. -- New `:tab-give` and `:tab-take` commands, to give tabs to another window, or - take them from another window. +- New fields for `window.title_format` and `tabs.title.format`: + * `{current_url}` + * `{protocol}` +- New settings: + * `colors.statusbar.passthrough.fg`/`.bg` + * `completion.delay` and `completion.min_chars` to update the completion less + often. + * `completion.use_best_match` to automatically use the best-matching + command in the completion. + * `keyhint.radius` to configure the edge rounding for the key hint widget. + * `qt.highdpi` to turn on Qt's High-DPI scaling. + * `tabs.pinned.shrink` (`true` by default) to make it possible + for pinned tabs and normal tabs to have the same size. + * `content.windowed_fullscreen` to show e.g. a fullscreened video in the + window without fullscreening that window. + * `tabs.persist_mode_on_change` to keep the current mode when + switching tabs. + * `session.lazy_restore` which allows to not load pages immediately + when restoring a session. +- New commands: + * `:tab-give` and `:tab-take`, to give tabs to another window, or take them + from another window. + * `:completion-item-yank` (bound to ``) to yank the current + completion item text. + * `:edit-command` to edit the commandline in an editor. + * `search.incremental` for incremental text search. +- New flags for existing commands: + * `-o` flag for `:spawn` to show stdout/stderr in a new tab. + * `--rapid` flag for `:command-accept` (bound to `Ctrl-Enter` by default), + which allows executing a command in the completion without closing it. + * `--private` and `--related` flags for `:edit-url`, which have the + same effect they have with `:open`. + * `--history` for `:completion-item-focus` which causes it to go + through the command history when no text was entered. The default bindings for + cursor keys in the completion changed to use that, so that they can be used + again to navigate through completion items when a text was entered. + * `--file` for `:debug-pyeval` which makes it take a filename instead of a + line of code. - New `config.source(...)` method for `config.py` to source another file. -- New `keyhint.radius` option to configure the edge rounding for the key hint - widget. -- `:edit-url` now handles the `--private` and `--related` flags, which have the - same effect they have with `:open`. - New `{line}` and `{column}` replacements for `editor.command` to position the cursor correctly. - New `qute-pass` userscript as alternative to `password_fill` which allows selecting accounts via rofi or any other dmenu-compatile application. -- New `qt.highdpi` setting to turn on Qt's High-DPI scaling. -- New `:completion-item-yank` command (bound to ``) to yank the current - completion item text. -- New `tabs.pinned.shrink` setting to (`true` by default) to make it possible - for pinned tabs and normal tabs to have the same size. -- New `content.windowed_fullscreen` setting to show e.g. a fullscreened video in - the window without fullscreening that window. -- New `:edit-command` command to edit the commandline in an editor. -- New `tabs.persist_mode_on_change` setting to keep the current mode when - switching tabs. -- New `session.lazy_restore` setting which allows to not load pages immediately - when restoring a session. - New `hist_importer.py` script to import history from Firefox/Chromium. -- New `{protocol}` replacement for `tabs.title.format` and friends. -- New `-o` flag for `:spawn` to show stdout/stderr in a new tab. -- Support for incremental search, with a new `search.incremental` setting. -- New `--rapid` flag for `:command-accept` (bound to `Ctrl-Enter` by default), - which allows executing a command in the completion without closing it. -- The `colors.completion.fg` setting can now be a list, allowing to specify - different colors for the three completion columns. Changed ~~~~~~~ @@ -73,13 +187,15 @@ Changed * `tabs.indicator_padding` -> `tabs.indicator.padding` * `session_default_name` -> `session.default_name` * `ignore_case` -> `search.ignore_case` +- Much improved user stylesheet handling for QtWebEngine which reduces + flickering and updates immediately after setting a stylesheet. - High-DPI favicons are now used when available. - The `asciidoc2html.py` script now uses Pygments (which is already a dependency of qutebrowser) instead of `source-highlight` for syntax highlighting. - The `:buffer` command now doesn't require quoting anymore, similar to `:open`. - The `importer.py` script was largely rewritten and now also supports importing from Firefox' `places.sqlite` file and Chrome/Chromium profiles. -- Various internal refactorings to use Python 3.5 and ECMAscript 6 features +- Various internal refactorings to use Python 3.5 and ECMAscript 6 features. - If the `window.hide_wayland_decoration` setting is False, but `QT_WAYLAND_DISABLE_WINDOWDECORATION` is set in the environment, the decorations are still hidden. @@ -91,17 +207,9 @@ Changed - The `qute://version` page now also shows the uptime of qutebrowser. - qutebrowser now prompts to create a non-existing directory when starting a download. -- Much improved user stylesheet handling which reduces flickering - and updates immediately after setting a stylesheet. -- `:completion-item-focus` now has a `--history` flag which causes it to go - through the command history when no text was entered. The default bindings for - cursor keys in the completion changed to use that, so that they can be used - again to navigate through completion items when a text was entered. -- `:debug-pyeval` now has a `--file` argument so it takes a filename instead of - a line of code. -- `:jseval --file` now searches relative paths in a js/ subdir in qutebrowser's - data dir, e.g. `~/.local/share/qutebrowser/js`. -- The current/default bindings are now shown in the :bind completion. +- `:jseval --file` now searches relative paths in a `js/` subdir in + qutebrowser's data dir, e.g. `~/.local/share/qutebrowser/js`. +- The current/default bindings are now shown in the ``:bind` completion. - Empty categories are now hidden in the `:open` completion. - Search terms for URLs and titles can now be mixed when filtering the completion. @@ -110,30 +218,35 @@ Changed order, and mixed matches on URL/tite are possible. - The system's default encoding (rather than UTF-8) is now used to decode subprocess output. +- qutebrowser now ensures it's focused again after an external editor is closed. +- The `colors.completion.fg` setting can now be a list, allowing to specify + different colors for the three completion columns. Fixed ~~~~~ - More consistent sizing for favicons with vertical tabs. - Using `:home` on pinned tabs is now prevented. -- Fix crash with unknown file types loaded via qute://help. +- Fix crash with unknown file types loaded via `qute://help`. - Scrolling performance improvements. - Sites like `qute://help` now redirect to `qute://help/` to make sure links work properly. - Fixes for the size calculation of pinned tabs in the tab bar. - Worked around a crash with PyQt 5.9.1 compiled against Qt < 5.9.1 when using - :yank or qute:// URLs. -- Fixed crash when opening `qute://help/img` + `:yank` or `qute://` URLs. +- Fixed crash when opening `qute://help/img`. - Fixed `gU` (`:navigate up`) on `qute://help` and webservers not handling `..` in a URL. - Using e.g. `-s backend webkit` to set the backend now works correctly. - Fixed crash when closing the tab an external editor was opened in. - When using `:search-next` before a search is finished, no warning about no results being found is shown anymore. -- Fix :click-element with an ID containing non-alphanumeric characters. +- Fix `:click-element` with an ID containing non-alphanumeric characters. - Fix crash when a subprocess outputs data which is not decodable as UTF-8. - Fix crash when closing a tab immediately after hinting. - Worked around issues in Qt 5.10 with loading progress never being finished. +- Fixed a crash when writing a flag before a command (e.g. `:-w open `). +- Fixed a crash when clicking certain form elements with QtWebEngine. Deprecated ~~~~~~~~~~ @@ -1088,7 +1201,7 @@ Added - New `:fake-key` command to send a fake keypress to a website or to qutebrowser. - New `--mhtml` argument for `:download` to download a page including all - ressources as MHTML file. + resources as MHTML file. - New option `tabs -> title-alignment` to change the alignment of tab titles. Changed @@ -1288,7 +1401,7 @@ Added - New argument `--no-err-windows` to suppress all error windows. - New arguments `--top-navigate` and `--bottom-navigate` (`-t`/`-b`) for `:scroll-page` to specify a navigation action (e.g. automatically go to the next page when arriving at the bottom). - New flag `-d`/`--detach` for `:spawn` to detach the spawned process so it's not closed when qutebrowser is. -- New flag `-v`/`--verbose` for `:spawn` to print informations when the process started/exited successfully. +- New flag `-v`/`--verbose` for `:spawn` to print information when the process started/exited successfully. - Many new color settings (foreground setting for every background setting). - New setting `ui -> modal-js-dialog` to use the standard modal dialogs for javascript questions instead of using the statusbar. - New setting `colors -> webpage.bg` to set the background color to use for websites which don't set one. diff --git a/doc/contributing.asciidoc b/doc/contributing.asciidoc index afbb752c5..9937434b9 100644 --- a/doc/contributing.asciidoc +++ b/doc/contributing.asciidoc @@ -44,8 +44,8 @@ be easy to solve] If you prefer C++ or Javascript to Python, see the relevant issues which involve work in those languages: -* https://github.com/qutebrowser/qutebrowser/issues?utf8=%E2%9C%93&q=is%3Aopen%20is%3Aissue%20label%3Ac%2B%2B[C++] (mostly work on Qt, the library behind qutebrowser) -* https://github.com/qutebrowser/qutebrowser/issues?q=is%3Aopen+is%3Aissue+label%3Ajavascript[JavaScript] +* https://github.com/qutebrowser/qutebrowser/issues?q=is%3Aopen+is%3Aissue+label%3A%22language%3A+c%2B%2B%22[C++] (mostly work on Qt, the library behind qutebrowser) +* https://github.com/qutebrowser/qutebrowser/issues?q=is%3Aopen+is%3Aissue+label%3A%22language%3A+javascript%22[JavaScript] There are also some things to do if you don't want to write code: @@ -375,7 +375,7 @@ The following logging levels are available for every logger: |error |There was an issue and some kind of operation was abandoned. |warning |There was an issue but the operation can continue running. |info |General informational messages. -|debug |Verbose debugging informations. +|debug |Verbose debugging information. |======================================================================= [[commands]] diff --git a/doc/faq.asciidoc b/doc/faq.asciidoc index 0d94796a4..8bbc1e5d0 100644 --- a/doc/faq.asciidoc +++ b/doc/faq.asciidoc @@ -216,6 +216,29 @@ And then re-emerging qtwebengine with: + emerge -1 qtwebengine +Unable to view DRM content (Netflix, Spotify, etc.).:: + You will need to install `widevine` and set `qt.args` to point to it. + Qt 5.9 currently only supports widevine up to Chrome version 61. ++ +On Arch, simply install `qt5-webengine-widevine` from the AUR and run: ++ +---- +:set qt.args '["ppapi-widevine-path=/usr/lib/qt/plugins/ppapi/libwidevinecdmadapter.so"]' +:restart +---- ++ +For other distributions, download the chromium tarball and widevine-cdm zip from +https://aur.archlinux.org/packages/qt5-webengine-widevine/[the AUR page], +extract `libwidevinecdmadapter.so` and `libwidevinecdm.so` files, respectively, +and move them to the `ppapi` plugin directory in your Qt library directory (create it if it does not exist). ++ +Lastly, set your `qt.args` to point to that directory and restart qutebrowser: ++ +---- +:set qt.args '["ppapi-widevine-path=/usr/lib64/qt5/plugins/ppapi/libwidevinecdmadapter.so"]' +:restart +---- + My issue is not listed.:: If you experience any segfaults or crashes, you can report the issue in https://github.com/qutebrowser/qutebrowser/issues[the issue tracker] or diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 098ada8af..58dfaaa16 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -1,6 +1,7 @@ // DO NOT EDIT THIS FILE DIRECTLY! // It is autogenerated by running: // $ python3 scripts/dev/src2asciidoc.py +// vim: readonly: = Commands @@ -145,14 +146,15 @@ How many pages to go back. [[bind]] === bind -Syntax: +:bind [*--mode* 'mode'] [*--default*] 'key' ['command']+ +Syntax: +:bind [*--mode* 'mode'] [*--default*] ['key'] ['command']+ Bind a key to a command. +If no command is given, show the current binding for the given key. Using :bind without any arguments opens a page showing all keybindings. + ==== positional arguments * +'key'+: The keychain or special key (inside `<...>`) to bind. -* +'command'+: The command to execute, with optional args, or not given to print the current binding. - +* +'command'+: The command to execute, with optional args. ==== optional arguments * +*-m*+, +*--mode*+: A comma-separated list of modes to bind the key in (default: `normal`). See `:help bindings.commands` for the @@ -174,7 +176,8 @@ Save the current page as a bookmark, or a specific url. If no url and title are provided, then save the current page as a bookmark. If a url and title have been provided, then save the given url as a bookmark with the provided title. You can view all saved bookmarks on the link:qute://bookmarks[bookmarks page]. ==== positional arguments -* +'url'+: url to save as a bookmark. If None, use url of current page. +* +'url'+: url to save as a bookmark. If not given, use url of current page. + * +'title'+: title of the new bookmark. ==== optional arguments @@ -218,7 +221,7 @@ Syntax: +:buffer ['index']+ Select tab by index or url/title best match. -Focuses window if necessary when index is given. If both index and count are given, use count. +Focuses window if necessary when index is given. If both index and count are given, use count. With neither index nor count given, open the qute://tabs page. ==== positional arguments * +'index'+: The [win_id/]index of the tab to focus. Or a substring in which case the closest match will be focused. @@ -271,7 +274,8 @@ Set all settings back to their default. [[config-cycle]] === config-cycle -Syntax: +:config-cycle [*--temp*] [*--print*] 'option' ['values' ['values' ...]]+ +Syntax: +:config-cycle [*--pattern* 'pattern'] [*--temp*] [*--print*] + 'option' ['values' ['values' ...]]+ Cycle an option between multiple values. @@ -280,6 +284,7 @@ Cycle an option between multiple values. * +'values'+: The values to cycle through. ==== optional arguments +* +*-u*+, +*--pattern*+: The URL pattern to use. * +*-t*+, +*--temp*+: Set value temporarily until qutebrowser is closed. * +*-p*+, +*--print*+: Print the value after setting. @@ -741,7 +746,13 @@ This tries to automatically click on typical _Previous Page_ or _Next Page_ link - `next`: Open a _next_ link. - `up`: Go up a level in the current URL. - `increment`: Increment the last number in the URL. + Uses the + link:settings.html#url.incdec_segments[url.incdec_segments] + config option. - `decrement`: Decrement the last number in the URL. + Uses the + link:settings.html#url.incdec_segments[url.incdec_segments] + config option. @@ -1066,7 +1077,7 @@ Delete a session. [[session-load]] === session-load -Syntax: +:session-load [*--clear*] [*--temp*] [*--force*] 'name'+ +Syntax: +:session-load [*--clear*] [*--temp*] [*--force*] [*--delete*] 'name'+ Load a session. @@ -1078,6 +1089,7 @@ Load a session. * +*-t*+, +*--temp*+: Don't set the current session for :session-save. * +*-f*+, +*--force*+: Force loading internal sessions (starting with an underline). +* +*-d*+, +*--delete*+: Delete the saved session once it has loaded. [[session-save]] === session-save @@ -1100,11 +1112,11 @@ Save a session. [[set]] === set -Syntax: +:set [*--temp*] [*--print*] ['option'] ['value']+ +Syntax: +:set [*--temp*] [*--print*] [*--pattern* 'pattern'] ['option'] ['value']+ Set an option. -If the option name ends with '?', the value of the option is shown instead. +If the option name ends with '?', the value of the option is shown instead. Using :set without any arguments opens a page where settings can be changed interactively. ==== positional arguments * +'option'+: The name of the option. @@ -1113,6 +1125,7 @@ If the option name ends with '?', the value of the option is shown instead. ==== optional arguments * +*-t*+, +*--temp*+: Set value temporarily until qutebrowser is closed. * +*-p*+, +*--print*+: Print the value after setting. +* +*-u*+, +*--pattern*+: The URL pattern to use. [[set-cmd-text]] === set-cmd-text @@ -1202,7 +1215,7 @@ The tab index to close [[tab-focus]] === tab-focus -Syntax: +:tab-focus ['index']+ +Syntax: +:tab-focus [*--no-last*] ['index']+ Select the tab given as argument/[count]. @@ -1214,6 +1227,9 @@ If neither count nor index are given, it behaves like tab-next. If both are give last tab. +==== optional arguments +* +*-n*+, +*--no-last*+: Whether to avoid focusing last tab if already focused. + ==== count The tab index to focus, starting with 1. @@ -1228,6 +1244,9 @@ If no win_id is given, the tab will get detached into a new window. ==== positional arguments * +'win-id'+: The window ID of the window to give the current tab to. +==== count +Overrides win_id (index starts at 1 for win_id=0). + [[tab-move]] === tab-move Syntax: +:tab-move ['index']+ @@ -1309,12 +1328,22 @@ Re-open the last closed tab or tabs. [[version]] === version +Syntax: +:version [*--paste*]+ + Show version information. +==== optional arguments +* +*-p*+, +*--paste*+: Paste to pastebin. + [[view-source]] === view-source +Syntax: +:view-source [*--edit*]+ + Show the source of the current page in a new tab. +==== optional arguments +* +*-e*+, +*--edit*+: Edit the source in the editor instead of opening a tab. + [[window-only]] === window-only Close all windows except for the current one. @@ -1402,6 +1431,7 @@ How many steps to zoom out. |<>|Accept the current prompt. |<>|Shift the focus of the prompt file completion menu to another item. |<>|Immediately open a download. +|<>|Yank URL to clipboard or primary selection. |<>|Move back a character. |<>|Delete the character before the cursor. |<>|Remove chars from the cursor to the beginning of the word. @@ -1607,6 +1637,15 @@ If no specific command is given, this will use the system's default application ==== note * This command does not split arguments after the last argument and handles quotes literally. +[[prompt-yank]] +=== prompt-yank +Syntax: +:prompt-yank [*--sel*]+ + +Yank URL to clipboard or primary selection. + +==== optional arguments +* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard. + [[rl-backward-char]] === rl-backward-char Move back a character. diff --git a/doc/help/configuring.asciidoc b/doc/help/configuring.asciidoc index a9b7b6ddf..266315d56 100644 --- a/doc/help/configuring.asciidoc +++ b/doc/help/configuring.asciidoc @@ -63,6 +63,10 @@ customizable. Using the link:commands.html#set[`:set`] command and command completion, you can quickly set settings interactively, for example `:set tabs.position left`. +Some settings are also customizable for a given +https://developer.chrome.com/apps/match_patterns[URL pattern] by doing e.g. +`:set --pattern=*://example.com/ content.images false`. + To get more help about a setting, use e.g. `:help tabs.position`. To bind and unbind keys, you can use the link:commands.html#bind[`:bind`] and @@ -147,7 +151,6 @@ prefix to preserve backslashes) or a Python regex object: If you want to read a setting, you can use the `c` object to do so as well: `c.colors.tabs.even.bg = c.colors.tabs.odd.bg`. - Using strings for setting names ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -171,6 +174,26 @@ To read a setting, use the `config.get` method: color = config.get('colors.completion.fg') ---- +Per-domain settings +~~~~~~~~~~~~~~~~~~~ + +Using `config.set`, some settings are also customizable for a given +https://developer.chrome.com/apps/match_patterns[URL pattern]: + +[source,python] +---- +config.set('content.images', False, '*://example.com/') +---- + +Alternatively, you can use `with config.pattern(...) as p:` to get a shortcut +similar to `c.` which is scoped to the given domain: + +[source,python] +---- +with config.pattern('*://example.com/') as p: + p.content.images = False +---- + Binding keys ~~~~~~~~~~~~ @@ -254,7 +277,7 @@ Getting the config directory ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you need to get the qutebrowser config directory, you can do so by reading -`config.configdir`. Similarily, you can get the qutebrowser data directory via +`config.configdir`. Similarly, you can get the qutebrowser data directory via `config.datadir`. This gives you a https://docs.python.org/3/library/pathlib.html[`pathlib.Path` @@ -366,6 +389,8 @@ You can use something like this to read colors from an `~/.Xresources` file: [source,python] ---- +import subprocess + def read_xresources(prefix): props = {} x = subprocess.run(['xrdb', '-query'], stdout=subprocess.PIPE) @@ -376,7 +401,7 @@ def read_xresources(prefix): return props xresources = read_xresources('*') -c.colors.statusbar.normal.bg = xresources['*background'] +c.colors.statusbar.normal.bg = xresources['*.background'] ---- Avoiding flake8 errors diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 53af8399d..a9e7becd2 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -1,6 +1,7 @@ // DO NOT EDIT THIS FILE DIRECTLY! // It is autogenerated by running: // $ python3 scripts/dev/src2asciidoc.py +// vim: readonly: = Setting reference @@ -229,6 +230,7 @@ |<>|Hide the statusbar unless a message is shown. |<>|Padding (in pixels) for the statusbar. |<>|Position of the status bar. +|<>|List of widgets displayed in the statusbar. |<>|Open new tabs (middleclick/ctrl+click) in the background. |<>|Mouse button with which to close tabs. |<>|How to behave when the close mouse button is pressed on the tab bar. @@ -237,11 +239,11 @@ |<>|Padding (in pixels) for tab indicators. |<>|Width (in pixels) of the progress indicator (0 to disable). |<>|How to behave when the last tab is closed. +|<>|When switching tabs, what input mode is applied. |<>|Switch between tabs using the mouse wheel. |<>|Position of new tabs opened from another tab. |<>|Position of new tabs which aren't opened from another tab. |<>|Padding (in pixels) around text for tabs. -|<>|Stay in insert/passthrough mode when switching tabs. |<>|Shrink pinned tabs down to their contents. |<>|Position of the tab bar. |<>|Which tab to select when the focused tab is removed. @@ -580,8 +582,14 @@ Default: * +pass:[sk]+: +pass:[set-cmd-text -s :bind]+ * +pass:[sl]+: +pass:[set-cmd-text -s :set -t]+ * +pass:[ss]+: +pass:[set-cmd-text -s :set]+ +* +pass:[tSH]+: +pass:[config-cycle -p -u *://*.{url:host}/* content.javascript.enabled ;; reload]+ +* +pass:[tSh]+: +pass:[config-cycle -p -u *://{url:host}/* content.javascript.enabled ;; reload]+ +* +pass:[tSu]+: +pass:[config-cycle -p -u {url} content.javascript.enabled ;; reload]+ * +pass:[th]+: +pass:[back -t]+ * +pass:[tl]+: +pass:[forward -t]+ +* +pass:[tsH]+: +pass:[config-cycle -p -t -u *://*.{url:host}/* content.javascript.enabled ;; reload]+ +* +pass:[tsh]+: +pass:[config-cycle -p -t -u *://{url:host}/* content.javascript.enabled ;; reload]+ +* +pass:[tsu]+: +pass:[config-cycle -p -t -u {url} content.javascript.enabled ;; reload]+ * +pass:[u]+: +pass:[undo]+ * +pass:[v]+: +pass:[enter-mode caret]+ * +pass:[wB]+: +pass:[set-cmd-text -s :bookmark-load -w]+ @@ -615,6 +623,8 @@ Default: * +pass:[<Alt-Backspace>]+: +pass:[rl-backward-kill-word]+ * +pass:[<Alt-D>]+: +pass:[rl-kill-word]+ * +pass:[<Alt-F>]+: +pass:[rl-forward-word]+ +* +pass:[<Alt-Shift-Y>]+: +pass:[prompt-yank --sel]+ +* +pass:[<Alt-Y>]+: +pass:[prompt-yank]+ * +pass:[<Ctrl-?>]+: +pass:[rl-delete-char]+ * +pass:[<Ctrl-A>]+: +pass:[rl-beginning-of-line]+ * +pass:[<Ctrl-B>]+: +pass:[rl-backward-char]+ @@ -1443,6 +1453,8 @@ Default: Enable support for the HTML 5 web application cache feature. An application cache acts like an HTTP cache in some sense. For documents that use the application cache via JavaScript, the loader engine will first ask the application cache for the contents, before hitting the network. +This setting supports URL patterns. + Type: <> Default: +pass:[true]+ @@ -1520,6 +1532,8 @@ This setting is only available with the QtWebKit backend. === content.dns_prefetch Try to pre-fetch DNS entries to speed up browsing. +This setting supports URL patterns. + Type: <> Default: +pass:[true]+ @@ -1531,6 +1545,8 @@ This setting is only available with the QtWebKit backend. Expand each subframe to its contents. This will flatten all the frames to become one scrollable page. +This setting supports URL patterns. + Type: <> Default: +pass:[false]+ @@ -1579,7 +1595,7 @@ Default: +pass:[true]+ [[content.headers.referer]] === content.headers.referer When to send the Referer header. -The Referer header tells websites from which website you were coming from when visting them. +The Referer header tells websites from which website you were coming from when visiting them. Type: <> @@ -1647,6 +1663,8 @@ Default: === content.hyperlink_auditing Enable hyperlink auditing (``). +This setting supports URL patterns. + Type: <> Default: +pass:[false]+ @@ -1655,6 +1673,8 @@ Default: +pass:[false]+ === content.images Load images automatically in web pages. +This setting supports URL patterns. + Type: <> Default: +pass:[true]+ @@ -1672,6 +1692,8 @@ Default: +pass:[true]+ Allow JavaScript to read from or write to the clipboard. With QtWebEngine, writing the clipboard as response to a user interaction is always allowed. +This setting supports URL patterns. + Type: <> Default: +pass:[false]+ @@ -1680,6 +1702,8 @@ Default: +pass:[false]+ === content.javascript.can_close_tabs Allow JavaScript to close tabs. +This setting supports URL patterns. + Type: <> Default: +pass:[false]+ @@ -1690,6 +1714,8 @@ This setting is only available with the QtWebKit backend. === content.javascript.can_open_tabs_automatically Allow JavaScript to open new tabs without user interaction. +This setting supports URL patterns. + Type: <> Default: +pass:[false]+ @@ -1698,6 +1724,8 @@ Default: +pass:[false]+ === content.javascript.enabled Enable JavaScript. +This setting supports URL patterns. + Type: <> Default: +pass:[true]+ @@ -1737,6 +1765,8 @@ Default: +pass:[true]+ === content.local_content_can_access_file_urls Allow locally loaded documents to access other local URLs. +This setting supports URL patterns. + Type: <> Default: +pass:[true]+ @@ -1745,6 +1775,8 @@ Default: +pass:[true]+ === content.local_content_can_access_remote_urls Allow locally loaded documents to access remote URLs. +This setting supports URL patterns. + Type: <> Default: +pass:[false]+ @@ -1753,6 +1785,8 @@ Default: +pass:[false]+ === content.local_storage Enable support for HTML 5 local storage and Web SQL. +This setting supports URL patterns. + Type: <> Default: +pass:[true]+ @@ -1813,6 +1847,8 @@ This setting is only available with the QtWebKit backend. === content.plugins Enable plugins in Web pages. +This setting supports URL patterns. + Type: <> Default: +pass:[false]+ @@ -1821,6 +1857,8 @@ Default: +pass:[false]+ === content.print_element_backgrounds Draw the background color and images also when the page is printed. +This setting supports URL patterns. + Type: <> Default: +pass:[true]+ @@ -1885,6 +1923,8 @@ Default: empty === content.webgl Enable WebGL. +This setting supports URL patterns. + Type: <> Default: +pass:[true]+ @@ -1902,6 +1942,8 @@ Default: +pass:[false]+ Monitor load requests for cross-site scripting attempts. Suspicious scripts will be blocked and reported in the inspector's JavaScript console. Enabling this feature might have an impact on performance. +This setting supports URL patterns. + Type: <> Default: +pass:[false]+ @@ -2375,6 +2417,8 @@ Default: +pass:[false]+ === input.links_included_in_focus_chain Include hyperlinks in the keyboard focus chain when tabbing. +This setting supports URL patterns. + Type: <> Default: +pass:[true]+ @@ -2402,6 +2446,8 @@ Default: +pass:[false]+ Enable spatial navigation. Spatial navigation consists in the ability to navigate between focusable elements in a Web page, such as hyperlinks and form controls, by using Left, Right, Up and Down arrow keys. For example, if the user presses the Right key, heuristics determine whether there is an element he might be trying to reach towards the right and which element he probably wants. +This setting supports URL patterns. + Type: <> Default: +pass:[false]+ @@ -2546,6 +2592,8 @@ Default: +pass:[false]+ Enable smooth scrolling for web pages. Note smooth scrolling does not work with the `:scroll-px` command. +This setting supports URL patterns. + Type: <> Default: +pass:[false]+ @@ -2682,6 +2730,31 @@ Valid values: Default: +pass:[bottom]+ +[[statusbar.widgets]] +=== statusbar.widgets +List of widgets displayed in the statusbar. + +Type: <> + +Valid values: + + * +url+: Current page URL. + * +scroll+: Percentage of the current page position like `10%`. + * +scroll_raw+: Raw percentage of the current page position like `10`. + * +history+: Display an arrow when possible to go back/forward in history. + * +tabs+: Current active tab, e.g. `2`. + * +keypress+: Display pressed keys when composing a vi command. + * +progress+: Progress bar for the current page loading. + +Default: + +- +pass:[keypress]+ +- +pass:[url]+ +- +pass:[scroll]+ +- +pass:[history]+ +- +pass:[tabs]+ +- +pass:[progress]+ + [[tabs.background]] === tabs.background Open new tabs (middleclick/ctrl+click) in the background. @@ -2773,6 +2846,20 @@ Valid values: Default: +pass:[ignore]+ +[[tabs.mode_on_change]] +=== tabs.mode_on_change +When switching tabs, what input mode is applied. + +Type: <> + +Valid values: + + * +persist+: Retain the current mode. + * +restore+: Restore previously saved mode. + * +normal+: Always revert to normal mode. + +Default: +pass:[normal]+ + [[tabs.mousewheel_switching]] === tabs.mousewheel_switching Switch between tabs using the mouse wheel. @@ -2824,14 +2911,6 @@ Default: - +pass:[right]+: +pass:[5]+ - +pass:[top]+: +pass:[0]+ -[[tabs.persist_mode_on_change]] -=== tabs.persist_mode_on_change -Stay in insert/passthrough mode when switching tabs. - -Type: <> - -Default: +pass:[false]+ - [[tabs.pinned.shrink]] === tabs.pinned.shrink Shrink pinned tabs down to their contents. @@ -2993,6 +3072,7 @@ Type: <> Valid values: * +host+ + * +port+ * +path+ * +query+ * +anchor+ @@ -3101,6 +3181,8 @@ Default: +pass:[512]+ === zoom.text_only Apply the zoom factor on a frame only to the text or to all content. +This setting supports URL patterns. + Type: <> Default: +pass:[false]+ diff --git a/doc/install.asciidoc b/doc/install.asciidoc index 4b1fd3f26..8916c1fdd 100644 --- a/doc/install.asciidoc +++ b/doc/install.asciidoc @@ -277,6 +277,11 @@ PS C:\> Install-Package qutebrowser ---- C:\> choco install qutebrowser ---- +* Scoop's client +---- +C:\> scoop bucket add extras +C:\> scoop install qutebrowser +---- Manual install ~~~~~~~~~~~~~~ diff --git a/doc/stacktrace.asciidoc b/doc/stacktrace.asciidoc index 5505eca93..00c89e884 100644 --- a/doc/stacktrace.asciidoc +++ b/doc/stacktrace.asciidoc @@ -37,7 +37,7 @@ is available in the repositories: Archlinux ^^^^^^^^^ -For Archlinux, no debug informations are provided. You can either compile Qt +For Archlinux, no debug information is provided. You can either compile Qt yourself (which will take a few hours even on a modern machine) or use debugging symbols compiled/packaged by me (x86_64 only). diff --git a/doc/userscripts.asciidoc b/doc/userscripts.asciidoc index 7f43e969b..c2f35b026 100644 --- a/doc/userscripts.asciidoc +++ b/doc/userscripts.asciidoc @@ -45,8 +45,6 @@ In `command` mode: - `QUTE_URL`: The current URL. - `QUTE_TITLE`: The title of the current page. - `QUTE_SELECTED_TEXT`: The text currently selected on the page. -- `QUTE_SELECTED_HTML` The HTML currently selected on the page (not supported - with QtWebEngine). In `hints` mode: diff --git a/misc/Makefile b/misc/Makefile index fe97eb6bf..714223d10 100644 --- a/misc/Makefile +++ b/misc/Makefile @@ -21,5 +21,5 @@ install: doc/qutebrowser.1.html $(wildcard misc/userscripts/*) install -Dm755 -t "$(DESTDIR)/usr/share/qutebrowser/scripts/" \ $(filter-out scripts/__init__.py scripts/__pycache__ scripts/dev \ - scripts/testbrowser_cpp scripts/asciidoc2html.py scripts/setupcommon.py \ + scripts/testbrowser scripts/asciidoc2html.py scripts/setupcommon.py \ scripts/link_pyqt.py,$(wildcard scripts/*)) diff --git a/misc/requirements/requirements-codecov.txt b/misc/requirements/requirements-codecov.txt index 6601cfb12..aa5de8b34 100644 --- a/misc/requirements/requirements-codecov.txt +++ b/misc/requirements/requirements-codecov.txt @@ -1,9 +1,9 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -certifi==2017.11.5 +certifi==2018.1.18 chardet==3.0.4 -codecov==2.0.10 -coverage==4.4.2 +codecov==2.0.15 +coverage==4.5.1 idna==2.6 requests==2.18.4 urllib3==1.22 diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt index bf7a02389..eb81fc37b 100644 --- a/misc/requirements/requirements-flake8.txt +++ b/misc/requirements/requirements-flake8.txt @@ -1,23 +1,23 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -attrs==17.3.0 +attrs==17.4.0 flake8==3.5.0 -flake8-bugbear==17.12.0 +flake8-bugbear==18.2.0 flake8-builtins==1.0.post0 flake8-comprehensions==1.4.1 flake8-copyright==0.2.0 -flake8-debugger==3.0.0 +flake8-debugger==3.1.0 flake8-deprecated==1.3 -flake8-docstrings==1.1.0 -flake8-future-import==0.4.3 +flake8-docstrings==1.3.0 +flake8-future-import==0.4.4 flake8-mock==0.3 flake8-per-file-ignores==0.4 -flake8-polyfill==1.0.1 +flake8-polyfill==1.0.2 flake8-string-format==0.2.3 flake8-tidy-imports==1.1.0 flake8-tuple==0.2.13 mccabe==0.6.1 -pep8-naming==0.4.1 +pep8-naming==0.5.0 pycodestyle==2.3.1 pydocstyle==2.1.1 pyflakes==1.6.0 diff --git a/misc/requirements/requirements-pip.txt b/misc/requirements/requirements-pip.txt index 42fae6bc9..b5e76ce0f 100644 --- a/misc/requirements/requirements-pip.txt +++ b/misc/requirements/requirements-pip.txt @@ -3,6 +3,6 @@ appdirs==1.4.3 packaging==16.8 pyparsing==2.2.0 -setuptools==38.2.5 +setuptools==38.5.1 six==1.11.0 wheel==0.30.0 diff --git a/misc/requirements/requirements-pylint-master.txt b/misc/requirements/requirements-pylint-master.txt index ce588c136..6364f0fcf 100644 --- a/misc/requirements/requirements-pylint-master.txt +++ b/misc/requirements/requirements-pylint-master.txt @@ -1,11 +1,11 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -e git+https://github.com/PyCQA/astroid.git#egg=astroid -certifi==2017.11.5 +certifi==2018.1.18 chardet==3.0.4 github3.py==0.9.6 idna==2.6 -isort==4.2.15 +isort==4.3.4 lazy-object-proxy==1.3.1 mccabe==0.6.1 -e git+https://github.com/PyCQA/pylint.git#egg=pylint diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index 6267fc2b0..de27257e1 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -1,14 +1,14 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -astroid==1.6.0 -certifi==2017.11.5 +astroid==1.6.1 +certifi==2018.1.18 chardet==3.0.4 github3.py==0.9.6 idna==2.6 -isort==4.2.15 +isort==4.3.4 lazy-object-proxy==1.3.1 mccabe==0.6.1 -pylint==1.8.1 +pylint==1.8.2 ./scripts/dev/pylint_checkers requests==2.18.4 six==1.11.0 diff --git a/misc/requirements/requirements-pyqt.txt b/misc/requirements/requirements-pyqt.txt index 5a08f2f73..99ba1b7cc 100644 --- a/misc/requirements/requirements-pyqt.txt +++ b/misc/requirements/requirements-pyqt.txt @@ -1,4 +1,4 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -PyQt5==5.9.2 -sip==4.19.6 +PyQt5==5.10 +sip==4.19.7 diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 90b60df47..796ed8085 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -1,39 +1,40 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -attrs==17.3.0 +attrs==17.4.0 beautifulsoup4==4.6.0 cheroot==6.0.0 click==6.7 # colorama==0.3.9 -coverage==4.4.2 +coverage==4.5.1 EasyProcess==0.2.3 fields==5.0.0 Flask==0.12.2 glob2==0.6 hunter==2.0.2 -hypothesis==3.44.4 +hypothesis==3.45.2 itsdangerous==0.24 # Jinja2==2.10 Mako==1.0.7 # MarkupSafe==1.0 +more-itertools==4.1.0 parse==1.8.2 parse-type==0.4.2 pluggy==0.6.0 py==1.5.2 py-cpuinfo==3.3.0 -pytest==3.3.1 -pytest-bdd==2.19.0 +pytest==3.4.0 +pytest-bdd==2.20.0 pytest-benchmark==3.1.1 pytest-cov==2.5.1 -pytest-faulthandler==1.3.1 +pytest-faulthandler==1.4.1 pytest-instafail==0.3.0 -pytest-mock==1.6.3 -pytest-qt==2.3.0 +pytest-mock==1.7.0 +pytest-qt==2.3.1 pytest-repeat==0.4.1 pytest-rerunfailures==4.0 pytest-travis-fold==1.3.0 -pytest-xvfb==1.0.0 +pytest-xvfb==1.1.0 PyVirtualDisplay==0.2.1 six==1.11.0 vulture==0.26 -Werkzeug==0.13 +Werkzeug==0.14.1 diff --git a/misc/userscripts/qutebrowser_viewsource b/misc/userscripts/qutebrowser_viewsource deleted file mode 100755 index a8ad71de3..000000000 --- a/misc/userscripts/qutebrowser_viewsource +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash - -# Copyright 2015 Zach-Button -# Copyright 2016-2017 Florian Bruhin (The Compiler) -# -# This file is part of qutebrowser. -# -# qutebrowser is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# qutebrowser is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with qutebrowser. If not, see . - -# -# This script fetches the unprocessed HTML source for a page and opens it in vim. -# :bind gf spawn --userscript qutebrowser_viewsource -# -# Caveat: Does not use authentication of any kind. Add it in if you want it to. -# - -path=$(mktemp --tmpdir qutebrowser_XXXXXXXX.html) - -curl "$QUTE_URL" > "$path" -urxvt -e vim "$path" - -rm "$path" diff --git a/misc/userscripts/readability b/misc/userscripts/readability index a5425dbac..d0ef43795 100755 --- a/misc/userscripts/readability +++ b/misc/userscripts/readability @@ -13,7 +13,11 @@ from __future__ import absolute_import import codecs, os -tmpfile=os.path.expanduser('~/.local/share/qutebrowser/userscripts/readability.html') +tmpfile = os.path.join( + os.environ.get('QUTE_DATA_DIR', + os.path.expanduser('~/.local/share/qutebrowser')), + 'userscripts/readability.html') + if not os.path.exists(os.path.dirname(tmpfile)): os.makedirs(os.path.dirname(tmpfile)) diff --git a/misc/userscripts/tor_identity b/misc/userscripts/tor_identity new file mode 100755 index 000000000..93b6d4136 --- /dev/null +++ b/misc/userscripts/tor_identity @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Copyright 2018 jnphilipp +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see . + +# Change your tor identity. +# +# Set a hotkey to launch this script, then: +# :bind ti spawn --userscript tor_identity PASSWORD +# +# Use the hotkey to change your tor identity, press 'ti' to change it. +# https://stem.torproject.org/faq.html#how-do-i-request-a-new-identity-from-tor +# + +import os +import sys + +try: + from stem import Signal + from stem.control import Controller +except ImportError: + if os.getenv('QUTE_FIFO'): + with open(os.environ['QUTE_FIFO'], 'w') as f: + f.write('message-error "Failed to import stem."') + else: + print('Failed to import stem.') + + +password = sys.argv[1] +with Controller.from_port(port=9051) as controller: + controller.authenticate(password) + controller.signal(Signal.NEWNYM) + if os.getenv('QUTE_FIFO'): + with open(os.environ['QUTE_FIFO'], 'w') as f: + f.write('message-info "Tor identity changed."') + else: + print('Tor identity changed.') diff --git a/pytest.ini b/pytest.ini index 4fefa0f77..89571aebc 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,5 @@ [pytest] +log_level = NOTSET addopts = --strict -rfEw --faulthandler-timeout=90 --instafail --pythonwarnings error --benchmark-columns=Min,Max,Median testpaths = tests markers = @@ -25,6 +26,7 @@ markers = this: Used to mark tests during development no_invalid_lines: Don't fail on unparseable lines in end2end tests issue2478: Tests which are broken on Windows with QtWebEngine, https://github.com/qutebrowser/qutebrowser/issues/2478 + issue3572: Tests which are broken with QtWebEngine and Qt 5.10, https://github.com/qutebrowser/qutebrowser/issues/3572 fake_os: Fake utils.is_* to a fake operating system unicode_locale: Tests which need an unicode locale to work qt_log_level_fail = WARNING diff --git a/qutebrowser.py b/qutebrowser.py index a16bd9ac7..8dd81b01a 100755 --- a/qutebrowser.py +++ b/qutebrowser.py @@ -2,7 +2,7 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2015 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/__init__.py b/qutebrowser/__init__.py index 6859abedc..3da270437 100644 --- a/qutebrowser/__init__.py +++ b/qutebrowser/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -22,11 +22,11 @@ import os.path __author__ = "Florian Bruhin" -__copyright__ = "Copyright 2014-2017 Florian Bruhin (The Compiler)" +__copyright__ = "Copyright 2014-2018 Florian Bruhin (The Compiler)" __license__ = "GPL" __maintainer__ = __author__ __email__ = "mail@qutebrowser.org" -__version_info__ = (1, 0, 4) +__version_info__ = (1, 1, 1) __version__ = '.'.join(str(e) for e in __version_info__) __description__ = "A keyboard-driven, vim-like browser based on PyQt5." diff --git a/qutebrowser/__main__.py b/qutebrowser/__main__.py index 506039890..533cf6e67 100644 --- a/qutebrowser/__main__.py +++ b/qutebrowser/__main__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/app.py b/qutebrowser/app.py index a2d768923..ec477ce8f 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -64,7 +64,7 @@ from qutebrowser.completion.models import miscmodels from qutebrowser.commands import cmdutils, runners, cmdexc from qutebrowser.config import config, websettings, configfiles, configinit from qutebrowser.browser import (urlmarks, adblock, history, browsertab, - downloads, greasemonkey) + qtnetworkdownloads, downloads, greasemonkey) from qutebrowser.browser.network import proxy from qutebrowser.browser.webkit import cookies, cache from qutebrowser.browser.webkit.network import networkmanager @@ -491,6 +491,10 @@ def _init_modules(args, crash_handler): diskcache = cache.DiskCache(standarddir.cache(), parent=qApp) objreg.register('cache', diskcache) + log.init.debug("Initializing downloads...") + download_manager = qtnetworkdownloads.DownloadManager(parent=qApp) + objreg.register('qtnetwork-download-manager', download_manager) + log.init.debug("Initializing Greasemonkey...") greasemonkey.init() @@ -872,10 +876,6 @@ class EventFilter(QObject): super().__init__(parent) self._activated = True self._handlers = { - QEvent.MouseButtonDblClick: self._handle_mouse_event, - QEvent.MouseButtonPress: self._handle_mouse_event, - QEvent.MouseButtonRelease: self._handle_mouse_event, - QEvent.MouseMove: self._handle_mouse_event, QEvent.KeyPress: self._handle_key_event, QEvent.KeyRelease: self._handle_key_event, } @@ -900,19 +900,6 @@ class EventFilter(QObject): # No window available yet, or not a MainWindow return False - def _handle_mouse_event(self, _event): - """Handle a mouse event. - - Args: - _event: The QEvent which is about to be delivered. - - Return: - True if the event should be filtered, False if it's passed through. - """ - # Mouse cursor shown (overrideCursor None) -> don't filter event - # Mouse cursor hidden (overrideCursor not None) -> filter event - return qApp.overrideCursor() is not None - def eventFilter(self, obj, event): """Handle an event. diff --git a/qutebrowser/browser/__init__.py b/qutebrowser/browser/__init__.py index c5d5e6c92..b565801d3 100644 --- a/qutebrowser/browser/__init__.py +++ b/qutebrowser/browser/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016-2017 Florian Bruhin (The Compiler) +# Copyright 2016-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/adblock.py b/qutebrowser/browser/adblock.py index 623d15717..f0462a778 100644 --- a/qutebrowser/browser/adblock.py +++ b/qutebrowser/browser/adblock.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -176,8 +176,7 @@ class HostBlocker: self._config_blocked_hosts) self._blocked_hosts = set() self._done_count = 0 - download_manager = objreg.get('qtnetwork-download-manager', - scope='window', window='last-focused') + download_manager = objreg.get('qtnetwork-download-manager') for url in config.val.content.host_blocking.lists: if url.scheme() == 'file': filename = url.toLocalFile() diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index eb0e55c4b..79c3ae4d4 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016-2017 Florian Bruhin (The Compiler) +# Copyright 2016-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -30,7 +30,8 @@ from PyQt5.QtWidgets import QWidget, QApplication from qutebrowser.keyinput import modeman from qutebrowser.config import config -from qutebrowser.utils import utils, objreg, usertypes, log, qtutils +from qutebrowser.utils import (utils, objreg, usertypes, log, qtutils, + urlutils, message) from qutebrowser.misc import miscwidgets, objects from qutebrowser.browser import mouse, hints @@ -94,19 +95,24 @@ class TabData: keep_icon: Whether the (e.g. cloned) icon should not be cleared on page load. inspector: The QWebInspector used for this webview. - viewing_source: Set if we're currently showing a source view. + open_target: Where to open the next link. + Only used for QtWebKit. override_target: Override for open_target for fake clicks (like hints). Only used for QtWebKit. pinned: Flag to pin the tab. fullscreen: Whether the tab has a video shown fullscreen currently. + netrc_used: Whether netrc authentication was performed. + input_mode: current input mode for the tab. """ keep_icon = attr.ib(False) - viewing_source = attr.ib(False) inspector = attr.ib(None) + open_target = attr.ib(usertypes.ClickTarget.normal) override_target = attr.ib(None) pinned = attr.ib(False) fullscreen = attr.ib(False) + netrc_used = attr.ib(False) + input_mode = attr.ib(usertypes.KeyMode.normal) class AbstractAction: @@ -121,8 +127,9 @@ class AbstractAction: action_class = None action_base = None - def __init__(self): + def __init__(self, tab): self._widget = None + self._tab = tab def exit_fullscreen(self): """Exit the fullscreen mode.""" @@ -139,6 +146,10 @@ class AbstractAction: raise WebTabError("{} is not a valid web action!".format(name)) self._widget.triggerPageAction(member) + def show_source(self): + """Show the source of the current page in a new tab.""" + raise NotImplementedError + class AbstractPrinting: @@ -245,10 +256,10 @@ class AbstractZoom(QObject): _default_zoom_changed: Whether the zoom was changed from the default. """ - def __init__(self, win_id, parent=None): + def __init__(self, tab, parent=None): super().__init__(parent) + self._tab = tab self._widget = None - self._win_id = win_id self._default_zoom_changed = False self._init_neighborlist() config.instance.changed.connect(self._on_config_changed) @@ -324,10 +335,9 @@ class AbstractCaret(QObject): """Attribute of AbstractTab for caret browsing.""" - def __init__(self, win_id, tab, mode_manager, parent=None): + def __init__(self, tab, mode_manager, parent=None): super().__init__(parent) self._tab = tab - self._win_id = win_id self._widget = None self.selection_enabled = False mode_manager.entered.connect(self._on_mode_entered) @@ -336,7 +346,7 @@ class AbstractCaret(QObject): def _on_mode_entered(self, mode): raise NotImplementedError - def _on_mode_left(self): + def _on_mode_left(self, mode): raise NotImplementedError def move_to_next_line(self, count=1): @@ -390,10 +400,7 @@ class AbstractCaret(QObject): def drop_selection(self): raise NotImplementedError - def has_selection(self): - raise NotImplementedError - - def selection(self, html=False): + def selection(self, callback): raise NotImplementedError def follow_selected(self, *, tab=False): @@ -609,6 +616,7 @@ class AbstractTab(QWidget): process terminated. arg 0: A TerminationStatus member. arg 1: The exit code. + predicted_navigation: Emitted before we tell Qt to open a URL. """ window_close_requested = pyqtSignal() @@ -626,6 +634,7 @@ class AbstractTab(QWidget): add_history_item = pyqtSignal(QUrl, QUrl, str) # url, requested url, title fullscreen_requested = pyqtSignal(bool) renderer_process_terminated = pyqtSignal(TerminationStatus, int) + predicted_navigation = pyqtSignal(QUrl) def __init__(self, *, win_id, mode_manager, private, parent=None): self.private = private @@ -639,16 +648,6 @@ class AbstractTab(QWidget): tab_registry[self.tab_id] = self objreg.register('tab', self, registry=self.registry) - # self.history = AbstractHistory(self) - # self.scroller = AbstractScroller(self, parent=self) - # self.caret = AbstractCaret(win_id=win_id, tab=self, - # mode_manager=mode_manager, parent=self) - # self.zoom = AbstractZoom(win_id=win_id) - # self.search = AbstractSearch(parent=self) - # self.printing = AbstractPrinting() - # self.elements = AbstractElements(self) - # self.action = AbstractAction() - self.data = TabData() self._layout = miscwidgets.WrapperLayout(self) self._widget = None @@ -666,6 +665,9 @@ class AbstractTab(QWidget): objreg.register('hintmanager', hintmanager, scope='tab', window=self.win_id, tab=self.tab_id) + self.predicted_navigation.connect( + lambda url: self.title_changed.emit(url.toDisplayString())) + def _set_widget(self, widget): # pylint: disable=protected-access self._widget = widget @@ -678,6 +680,7 @@ class AbstractTab(QWidget): self.printing._widget = widget self.action._widget = widget self.elements._widget = widget + self.settings._settings = widget.settings() self._install_event_filter() self.zoom.set_default() @@ -723,10 +726,25 @@ class AbstractTab(QWidget): def _on_load_started(self): self._progress = 0 self._has_ssl_errors = False - self.data.viewing_source = False self._set_load_status(usertypes.LoadStatus.loading) self.load_started.emit() + @pyqtSlot(usertypes.NavigationRequest) + def _on_navigation_request(self, navigation): + """Handle common acceptNavigationRequest code.""" + log.webview.debug("navigation request: url {}, type {}, is_main_frame " + "{}".format(navigation.url.toDisplayString(), + navigation.navigation_type, + navigation.is_main_frame)) + + if (navigation.navigation_type == navigation.Type.link_clicked and + not navigation.url.isValid()): + msg = urlutils.get_errstring(navigation.url, + "Invalid link clicked") + message.error(msg) + self.data.open_target = usertypes.ClickTarget.normal + navigation.accepted = False + def handle_auto_insert_mode(self, ok): """Handle `input.insert_mode.auto_load` after loading finished.""" if not config.val.input.insert_mode.auto_load or not ok: @@ -749,6 +767,10 @@ class AbstractTab(QWidget): @pyqtSlot(bool) def _on_load_finished(self, ok): + if sip.isdeleted(self._widget): + # https://github.com/qutebrowser/qutebrowser/issues/3498 + return + sess_manager = objreg.get('session-manager') sess_manager.save_autosave() @@ -761,7 +783,9 @@ class AbstractTab(QWidget): self._set_load_status(usertypes.LoadStatus.warn) else: self._set_load_status(usertypes.LoadStatus.error) + self.load_finished.emit(ok) + if not self.title(): self.title_changed.emit(self.url().toDisplayString()) @@ -792,7 +816,7 @@ class AbstractTab(QWidget): def _openurl_prepare(self, url): qtutils.ensure_valid(url) - self.title_changed.emit(url.toDisplayString()) + self.predicted_navigation.emit(url) def openurl(self, url): raise NotImplementedError @@ -811,7 +835,7 @@ class AbstractTab(QWidget): raise NotImplementedError def dump_async(self, callback, *, plain=False): - """Dump the current page to a file ascync. + """Dump the current page's html asynchronously. The given callback will be called with the result when dumping is complete. diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 823d12c99..8d7c0c2cf 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -26,12 +26,9 @@ import functools import typing from PyQt5.QtWidgets import QApplication, QTabBar, QDialog -from PyQt5.QtCore import Qt, QUrl, QEvent, QUrlQuery +from PyQt5.QtCore import pyqtSlot, Qt, QUrl, QEvent, QUrlQuery from PyQt5.QtGui import QKeyEvent from PyQt5.QtPrintSupport import QPrintDialog, QPrintPreviewDialog -import pygments -import pygments.lexers -import pygments.formatters from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners from qutebrowser.config import config, configdata @@ -39,7 +36,7 @@ from qutebrowser.browser import (urlmarks, browsertab, inspector, navigate, webelem, downloads) from qutebrowser.keyinput import modeman from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils, - objreg, utils, debug, standarddir) + objreg, utils, standarddir) from qutebrowser.utils.usertypes import KeyMode from qutebrowser.misc import editor, guiprocess from qutebrowser.completion.models import urlmodel, miscmodels @@ -536,14 +533,19 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('win_id', completion=miscmodels.window) - def tab_give(self, win_id: int = None): + @cmdutils.argument('count', count=True) + def tab_give(self, win_id: int = None, count=None): """Give the current tab to a new or existing window if win_id given. If no win_id is given, the tab will get detached into a new window. Args: win_id: The window ID of the window to give the current tab to. + count: Overrides win_id (index starts at 1 for win_id=0). """ + if count is not None: + win_id = count - 1 + if win_id == self._win_id: raise cmdexc.CommandError("Can't give a tab to the same window") @@ -638,7 +640,13 @@ class CommandDispatcher: - `next`: Open a _next_ link. - `up`: Go up a level in the current URL. - `increment`: Increment the last number in the URL. + Uses the + link:settings.html#url.incdec_segments[url.incdec_segments] + config option. - `decrement`: Decrement the last number in the URL. + Uses the + link:settings.html#url.incdec_segments[url.incdec_segments] + config option. tab: Open in a new tab. bg: Open in a background tab. @@ -849,14 +857,21 @@ class CommandDispatcher: s = self._yank_url(what) what = 'URL' # For printing elif what == 'selection': + def _selection_callback(s): + if not s: + message.info("Nothing to yank") + return + self._yank_to_target(s, sel, what, keep) + caret = self._current_widget().caret - s = caret.selection() - if not caret.has_selection() or not s: - message.info("Nothing to yank") - return + caret.selection(callback=_selection_callback) + return else: # pragma: no cover raise ValueError("Invalid value {!r} for `what'.".format(what)) + self._yank_to_target(s, sel, what, keep) + + def _yank_to_target(self, s, sel, what, keep): if sel and utils.supports_selection(): target = "primary selection" else: @@ -953,22 +968,25 @@ class CommandDispatcher: (prev and i < cur_idx) or (next_ and i > cur_idx)) - # Check to see if we are closing any pinned tabs - if not force: - for i, tab in enumerate(self._tabbed_browser.widgets()): - if _to_close(i) and tab.data.pinned: - self._tabbed_browser.tab_close_prompt_if_pinned( - tab, - force, - lambda: self.tab_only( - prev=prev, next_=next_, force=True)) - return - + # close as many tabs as we can first_tab = True + pinned_tabs_cleanup = False for i, tab in enumerate(self._tabbed_browser.widgets()): if _to_close(i): - self._tabbed_browser.close_tab(tab, new_undo=first_tab) - first_tab = False + if force or not tab.data.pinned: + self._tabbed_browser.close_tab(tab, new_undo=first_tab) + first_tab = False + else: + pinned_tabs_cleanup = tab + + # Check to see if we would like to close any pinned tabs + if pinned_tabs_cleanup: + self._tabbed_browser.tab_close_prompt_if_pinned( + pinned_tabs_cleanup, + force, + lambda: self.tab_only( + prev=prev, next_=next_, force=True), + text="Are you sure you want to close pinned tabs?") @cmdutils.register(instance='command-dispatcher', scope='window') def undo(self): @@ -1074,14 +1092,16 @@ class CommandDispatcher: Focuses window if necessary when index is given. If both index and count are given, use count. + With neither index nor count given, open the qute://tabs page. + Args: index: The [win_id/]index of the tab to focus. Or a substring in which case the closest match will be focused. count: The tab index to focus, starting with 1. """ if count is None and index is None: - raise cmdexc.CommandError("buffer: Either a count or the argument " - "index must be specified.") + self.openurl('qute://tabs/', tab=True) + return if count is not None: index = str(count) @@ -1096,7 +1116,8 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('index', choices=['last']) @cmdutils.argument('count', count=True) - def tab_focus(self, index: typing.Union[str, int] = None, count=None): + def tab_focus(self, index: typing.Union[str, int] = None, + count=None, no_last=False): """Select the tab given as argument/[count]. If neither count nor index are given, it behaves like tab-next. @@ -1108,13 +1129,14 @@ class CommandDispatcher: Negative indices count from the end, such that -1 is the last tab. count: The tab index to focus, starting with 1. + no_last: Whether to avoid focusing last tab if already focused. """ index = count if count is not None else index if index == 'last': self._tab_focus_last() return - elif index == self._current_index() + 1: + elif not no_last and index == self._current_index() + 1: self._tab_focus_last(show_error=False) return elif index is None: @@ -1204,9 +1226,29 @@ class CommandDispatcher: log.procs.debug("Executing {} with args {}, userscript={}".format( cmd, args, userscript)) + + @pyqtSlot() + def _on_proc_finished(): + if output: + tb = objreg.get('tabbed-browser', scope='window', + window='last-focused') + tb.openurl(QUrl('qute://spawn-output'), newtab=True) + if userscript: + def _selection_callback(s): + try: + runner = self._run_userscript(s, cmd, args, verbose) + runner.finished.connect(_on_proc_finished) + except cmdexc.CommandError as e: + message.error(str(e)) + # ~ expansion is handled by the userscript module. - self._run_userscript(cmd, *args, verbose=verbose) + # dirty hack for async call because of: + # https://bugreports.qt.io/browse/QTBUG-53134 + # until it fixed or blocked async call implemented: + # https://github.com/qutebrowser/qutebrowser/issues/3327 + caret = self._current_widget().caret + caret.selection(callback=_selection_callback) else: cmd = os.path.expanduser(cmd) proc = guiprocess.GUIProcess(what='command', verbose=verbose, @@ -1215,18 +1257,14 @@ class CommandDispatcher: proc.start_detached(cmd, args) else: proc.start(cmd, args) - - if output: - tabbed_browser = objreg.get('tabbed-browser', scope='window', - window='last-focused') - tabbed_browser.openurl(QUrl('qute://spawn-output'), newtab=True) + proc.finished.connect(_on_proc_finished) @cmdutils.register(instance='command-dispatcher', scope='window') def home(self): """Open main startpage in current tab.""" self.openurl(config.val.url.start_pages[0]) - def _run_userscript(self, cmd, *args, verbose=False): + def _run_userscript(self, selection, cmd, args, verbose): """Run a userscript given as argument. Args: @@ -1236,21 +1274,15 @@ class CommandDispatcher: """ env = { 'QUTE_MODE': 'command', + 'QUTE_SELECTED_TEXT': selection, } idx = self._current_index() if idx != -1: env['QUTE_TITLE'] = self._tabbed_browser.page_title(idx) - tab = self._tabbed_browser.currentWidget() - if tab is not None and tab.caret.has_selection(): - env['QUTE_SELECTED_TEXT'] = tab.caret.selection() - try: - env['QUTE_SELECTED_HTML'] = tab.caret.selection(html=True) - except browsertab.UnsupportedOperationError: - pass - # FIXME:qtwebengine: If tab is None, run_async will fail! + tab = self._tabbed_browser.currentWidget() try: url = self._tabbed_browser.current_url() @@ -1260,10 +1292,11 @@ class CommandDispatcher: env['QUTE_URL'] = url.toString(QUrl.FullyEncoded) try: - userscripts.run_async(tab, cmd, *args, win_id=self._win_id, - env=env, verbose=verbose) + runner = userscripts.run_async( + tab, cmd, *args, win_id=self._win_id, env=env, verbose=verbose) except userscripts.Error as e: raise cmdexc.CommandError(e) + return runner @cmdutils.register(instance='command-dispatcher', scope='window') def quickmark_save(self): @@ -1325,7 +1358,8 @@ class CommandDispatcher: link:qute://bookmarks[bookmarks page]. Args: - url: url to save as a bookmark. If None, use url of current page. + url: url to save as a bookmark. If not given, use url of current + page. title: title of the new bookmark. toggle: remove the bookmark instead of raising an error if it already exists. @@ -1334,7 +1368,7 @@ class CommandDispatcher: raise cmdexc.CommandError('Title must be provided if url has ' 'been provided') bookmark_manager = objreg.get('bookmark-manager') - if url is None: + if not url: url = self._current_url() else: try: @@ -1434,8 +1468,7 @@ class CommandDispatcher: mhtml_: Download the current page and all assets as mhtml file. """ # FIXME:qtwebengine do this with the QtWebEngine download manager? - download_manager = objreg.get('qtnetwork-download-manager', - scope='window', window=self._win_id) + download_manager = objreg.get('qtnetwork-download-manager') target = None if dest is not None: dest = downloads.transform_path(dest) @@ -1480,34 +1513,26 @@ class CommandDispatcher: ) @cmdutils.register(instance='command-dispatcher', scope='window') - def view_source(self): - """Show the source of the current page in a new tab.""" - tab = self._current_widget() - if tab.data.viewing_source: - raise cmdexc.CommandError("Already viewing source!") + def view_source(self, edit=False): + """Show the source of the current page in a new tab. + Args: + edit: Edit the source in the editor instead of opening a tab. + """ + tab = self._current_widget() try: current_url = self._current_url() except cmdexc.CommandError as e: message.error(str(e)) return + if current_url.scheme() == 'view-source': + raise cmdexc.CommandError("Already viewing source!") - def show_source_cb(source): - """Show source as soon as it's ready.""" - # WORKAROUND for https://github.com/PyCQA/pylint/issues/491 - # pylint: disable=no-member - lexer = pygments.lexers.HtmlLexer() - formatter = pygments.formatters.HtmlFormatter( - full=True, linenos='table', - title='Source for {}'.format(current_url.toDisplayString())) - # pylint: enable=no-member - highlighted = pygments.highlight(source, lexer, formatter) - - new_tab = self._tabbed_browser.tabopen() - new_tab.set_html(highlighted) - new_tab.data.viewing_source = True - - tab.dump_async(show_source_cb) + if edit: + ed = editor.ExternalEditor(self._tabbed_browser) + tab.dump_async(ed.edit) + else: + tab.action.show_source() @cmdutils.register(instance='command-dispatcher', scope='window', debug=True) @@ -1613,9 +1638,11 @@ class CommandDispatcher: caret_position = elem.caret_position() - ed = editor.ExternalEditor(self._tabbed_browser) - ed.editing_finished.connect(functools.partial( - self.on_editing_finished, elem)) + ed = editor.ExternalEditor(watch=True, parent=self._tabbed_browser) + ed.file_updated.connect(functools.partial( + self.on_file_updated, elem)) + ed.editing_finished.connect(lambda: mainwindow.raise_window( + objreg.last_focused_window(), alert=False)) ed.edit(text, caret_position) @cmdutils.register(instance='command-dispatcher', scope='window') @@ -1628,10 +1655,10 @@ class CommandDispatcher: tab = self._current_widget() tab.elements.find_focused(self._open_editor_cb) - def on_editing_finished(self, elem, text): + def on_file_updated(self, elem, text): """Write the editor text into the form field and clean up tempfile. - Callback for GUIProcess when the editor was closed. + Callback for GUIProcess when the edited text was updated. Args: elem: The WebElementWrapper which was modified. @@ -2137,7 +2164,7 @@ class CommandDispatcher: ed = editor.ExternalEditor(self._tabbed_browser) # Passthrough for openurl args (e.g. -t, -b, -w) - ed.editing_finished.connect(functools.partial( + ed.file_updated.connect(functools.partial( self._open_if_changed, old_url=old_url, bg=bg, tab=tab, window=window, private=private, related=related)) @@ -2195,11 +2222,4 @@ class CommandDispatcher: return window = self._tabbed_browser.window() - if window.isFullScreen(): - window.setWindowState( - window.state_before_fullscreen & ~Qt.WindowFullScreen) - else: - window.state_before_fullscreen = window.windowState() - window.showFullScreen() - log.misc.debug('state before fullscreen: {}'.format( - debug.qflags_key(Qt, window.state_before_fullscreen))) + window.setWindowState(window.windowState() ^ Qt.WindowFullScreen) diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index c064d700e..4f390b18b 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -31,7 +31,7 @@ import enum import sip from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QObject, QModelIndex, - QTimer, QAbstractListModel) + QTimer, QAbstractListModel, QUrl) from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.config import config @@ -166,6 +166,7 @@ def get_filename_question(*, suggested_filename, url, parent=None): q.title = "Save file to:" q.text = "Please enter a location for {}".format( html.escape(url.toDisplayString())) + q.url = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded) q.mode = usertypes.PromptMode.download q.completed.connect(q.deleteLater) q.default = _path_suggestion(suggested_filename) diff --git a/qutebrowser/browser/downloadview.py b/qutebrowser/browser/downloadview.py index ac44d75d9..80da117d2 100644 --- a/qutebrowser/browser/downloadview.py +++ b/qutebrowser/browser/downloadview.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/greasemonkey.py b/qutebrowser/browser/greasemonkey.py index 9a82d6a93..fb064f6c1 100644 --- a/qutebrowser/browser/greasemonkey.py +++ b/qutebrowser/browser/greasemonkey.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2017 Florian Bruhin (The Compiler) +# Copyright 2017-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -23,7 +23,6 @@ import re import os import json import fnmatch -import functools import glob import attr @@ -135,7 +134,7 @@ class GreasemonkeyManager(QObject): Signals: scripts_reloaded: Emitted when scripts are reloaded from disk. Any cached or already-injected scripts should be - considered obselete. + considered obsolete. """ scripts_reloaded = pyqtSignal() @@ -178,10 +177,10 @@ class GreasemonkeyManager(QObject): elif script.run_at == 'document-idle': self._run_idle.append(script) else: - log.greasemonkey.warning("Script {} has invalid run-at " - "defined, defaulting to " - "document-end" - .format(script_path)) + if script.run_at: + log.greasemonkey.warning( + "Script {} has invalid run-at defined, " + "defaulting to document-end".format(script_path)) # Default as per # https://wiki.greasespot.net/Metadata_Block#.40run-at self._run_end.append(script) @@ -196,11 +195,23 @@ class GreasemonkeyManager(QObject): """ if url.scheme() not in self.greaseable_schemes: return MatchingScripts(url, [], [], []) - match = functools.partial(fnmatch.fnmatch, - url.toString(QUrl.FullyEncoded)) + + string_url = url.toString(QUrl.FullyEncoded) + + def _match(pattern): + # For include and exclude rules if they start and end with '/' they + # should be treated as a (ecma syntax) regular expression. + if pattern.startswith('/') and pattern.endswith('/'): + matches = re.search(pattern[1:-1], string_url, flags=re.I) + return matches is not None + + # Otherwise they are glob expressions. + return fnmatch.fnmatch(string_url, pattern) + tester = (lambda script: - any(match(pat) for pat in script.includes) and - not any(match(pat) for pat in script.excludes)) + any(_match(pat) for pat in script.includes) and + not any(_match(pat) for pat in script.excludes)) + return MatchingScripts( url, [script for script in self._run_start if tester(script)], diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 14cc2b574..0390d5d1f 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -291,8 +291,7 @@ class HintActions: user_agent = context.tab.user_agent() # FIXME:qtwebengine do this with QtWebEngine downloads? - download_manager = objreg.get('qtnetwork-download-manager', - scope='window', window=self._win_id) + download_manager = objreg.get('qtnetwork-download-manager') download_manager.get(url, qnam=qnam, user_agent=user_agent, prompt_download_directory=prompt) diff --git a/qutebrowser/browser/history.py b/qutebrowser/browser/history.py index ecab730ae..85922f9e8 100644 --- a/qutebrowser/browser/history.py +++ b/qutebrowser/browser/history.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2017 Florian Bruhin (The Compiler) +# Copyright 2015-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -32,7 +32,7 @@ from qutebrowser.misc import objects, sql # increment to indicate that HistoryCompletion must be regenerated -_USER_VERSION = 1 +_USER_VERSION = 2 class CompletionHistory(sql.SqlTable): @@ -102,7 +102,8 @@ class WebHistory(sql.SqlTable): data = {'url': [], 'title': [], 'last_atime': []} # select the latest entry for each url q = sql.Query('SELECT url, title, max(atime) AS atime FROM History ' - 'WHERE NOT redirect GROUP BY url ORDER BY atime asc') + 'WHERE NOT redirect and url NOT LIKE "qute://back%" ' + 'GROUP BY url ORDER BY atime asc') for entry in q.run(): data['url'].append(self._format_completion_url(QUrl(entry.url))) data['title'].append(entry.title) @@ -171,7 +172,9 @@ class WebHistory(sql.SqlTable): @pyqtSlot(QUrl, QUrl, str) def add_from_tab(self, url, requested_url, title): """Add a new history entry as slot, called from a BrowserTab.""" - if url.scheme() == 'data' or requested_url.scheme() == 'data': + if any(url.scheme() in ('data', 'view-source') or + (url.scheme(), url.host()) == ('qute', 'back') + for url in (url, requested_url)): return if url.isEmpty(): # things set via setHtml diff --git a/qutebrowser/browser/inspector.py b/qutebrowser/browser/inspector.py index 9c583f4b3..608404eeb 100644 --- a/qutebrowser/browser/inspector.py +++ b/qutebrowser/browser/inspector.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2017 Florian Bruhin (The Compiler) +# Copyright 2015-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/mouse.py b/qutebrowser/browser/mouse.py index d08f191a8..b0053cbf1 100644 --- a/qutebrowser/browser/mouse.py +++ b/qutebrowser/browser/mouse.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016-2017 Florian Bruhin (The Compiler) +# Copyright 2016-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/navigate.py b/qutebrowser/browser/navigate.py index 8b406368b..257ce6fe0 100644 --- a/qutebrowser/browser/navigate.py +++ b/qutebrowser/browser/navigate.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016-2017 Florian Bruhin (The Compiler) +# Copyright 2016-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/network/pac.py b/qutebrowser/browser/network/pac.py index a3f0813c8..95ff99390 100644 --- a/qutebrowser/browser/network/pac.py +++ b/qutebrowser/browser/network/pac.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016-2017 Florian Bruhin (The Compiler) +# Copyright 2016-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/network/proxy.py b/qutebrowser/browser/network/proxy.py index 2821a840d..96be78742 100644 --- a/qutebrowser/browser/network/proxy.py +++ b/qutebrowser/browser/network/proxy.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -61,6 +61,9 @@ class ProxyFactory(QNetworkProxyFactory): """ proxy = config.val.content.proxy if proxy is configtypes.SYSTEM_PROXY: + # On Linux, use "export http_proxy=socks5://host:port" to manually + # set system proxy. + # ref. http://doc.qt.io/qt-5/qnetworkproxyfactory.html#systemProxyForQuery proxies = QNetworkProxyFactory.systemProxyForQuery(query) elif isinstance(proxy, pac.PACFetcher): proxies = proxy.resolve(query) diff --git a/qutebrowser/browser/pdfjs.py b/qutebrowser/browser/pdfjs.py index cd5bcac0a..5ce3d866e 100644 --- a/qutebrowser/browser/pdfjs.py +++ b/qutebrowser/browser/pdfjs.py @@ -1,7 +1,7 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: # Copyright 2015 Daniel Schadt -# Copyright 2016-2017 Florian Bruhin (The Compiler) +# Copyright 2016-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py index 378bc72b5..82996a803 100644 --- a/qutebrowser/browser/qtnetworkdownloads.py +++ b/qutebrowser/browser/qtnetworkdownloads.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -20,6 +20,7 @@ """Download manager.""" import io +import os.path import shutil import functools @@ -198,21 +199,23 @@ class DownloadItem(downloads.AbstractDownloadItem): def _ask_confirm_question(self, title, msg): no_action = functools.partial(self.cancel, remove_data=False) + url = 'file://{}'.format(self._filename) message.confirm_async(title=title, text=msg, yes_action=self._after_set_filename, no_action=no_action, cancel_action=no_action, - abort_on=[self.cancelled, self.error]) + abort_on=[self.cancelled, self.error], url=url) def _ask_create_parent_question(self, title, msg, force_overwrite, remember_directory): no_action = functools.partial(self.cancel, remove_data=False) + url = 'file://{}'.format(os.path.dirname(self._filename)) message.confirm_async(title=title, text=msg, yes_action=(lambda: self._after_create_parent_question( force_overwrite, remember_directory)), no_action=no_action, cancel_action=no_action, - abort_on=[self.cancelled, self.error]) + abort_on=[self.cancelled, self.error], url=url) def _set_fileobj(self, fileobj, *, autoclose=True): """Set the file object to write the download to. @@ -378,10 +381,10 @@ class DownloadManager(downloads.AbstractDownloadManager): _networkmanager: A NetworkManager for generic downloads. """ - def __init__(self, win_id, parent=None): + def __init__(self, parent=None): super().__init__(parent) self._networkmanager = networkmanager.NetworkManager( - win_id=win_id, tab_id=None, + win_id=None, tab_id=None, private=config.val.content.private_browsing, parent=self) @pyqtSlot('QUrl') diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index 8bcb7ff37..e53907798 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016-2017 Florian Bruhin (The Compiler) +# Copyright 2016-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -30,8 +30,10 @@ import time import textwrap import mimetypes import urllib +import collections import pkg_resources +import sip from PyQt5.QtCore import QUrlQuery, QUrl import qutebrowser @@ -201,6 +203,27 @@ def qute_bookmarks(_url): return 'text/html', html +@add_handler('tabs') +def qute_tabs(_url): + """Handler for qute://tabs. Display information about all open tabs.""" + tabs = collections.defaultdict(list) + for win_id, window in objreg.window_registry.items(): + if sip.isdeleted(window): + continue + tabbed_browser = objreg.get('tabbed-browser', + scope='window', + window=win_id) + for tab in tabbed_browser.widgets(): + if tab.url() not in [QUrl("qute://tabs/"), QUrl("qute://tabs")]: + urlstr = tab.url().toDisplayString() + tabs[str(win_id)].append((tab.title(), urlstr)) + + html = jinja.render('tabs.html', + title='Tabs', + tab_list_by_window=tabs) + return 'text/html', html + + def history_data(start_time, offset=None): """Return history data. @@ -435,6 +458,22 @@ def qute_settings(url): return 'text/html', html +@add_handler('bindings') +def qute_bindings(_url): + """Handler for qute://bindings. View keybindings.""" + bindings = {} + defaults = config.val.bindings.default + modes = set(defaults.keys()).union(config.val.bindings.commands) + modes.remove('normal') + modes = ['normal'] + sorted(list(modes)) + for mode in modes: + bindings[mode] = config.key_instance.get_bindings_for(mode) + + html = jinja.render('bindings.html', title='Bindings', + bindings=bindings) + return 'text/html', html + + @add_handler('back') def qute_back(url): """Handler for qute://back. @@ -460,3 +499,10 @@ def qute_configdiff(url): else: data = config.instance.dump_userconfig().encode('utf-8') return 'text/plain', data + + +@add_handler('pastebin-version') +def qute_pastebin_version(_url): + """Handler that pastebins the version string.""" + version.pastebin_version() + return 'text/plain', b'Paste called.' diff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py index b6bfefe7b..d82b741e5 100644 --- a/qutebrowser/browser/shared.py +++ b/qutebrowser/browser/shared.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016-2017 Florian Bruhin (The Compiler) +# Copyright 2016-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -19,7 +19,11 @@ """Various utilities shared between webpage/webview subclasses.""" +import os import html +import netrc + +from PyQt5.QtCore import QUrl from qutebrowser.config import config from qutebrowser.utils import usertypes, message, log, objreg, jinja, utils @@ -27,7 +31,6 @@ from qutebrowser.mainwindow import mainwindow class CallSuper(Exception): - """Raised when the caller should call the superclass instead.""" @@ -61,9 +64,10 @@ def authentication_required(url, authenticator, abort_on): else: msg = '{} needs authentication'.format( html.escape(url.toDisplayString())) + urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded) answer = message.ask(title="Authentication required", text=msg, mode=usertypes.PromptMode.user_pwd, - abort_on=abort_on) + abort_on=abort_on, url=urlstr) if answer is not None: authenticator.setUser(answer.user) authenticator.setPassword(answer.password) @@ -78,9 +82,10 @@ def javascript_confirm(url, js_msg, abort_on): msg = 'From {}:
{}'.format(html.escape(url.toDisplayString()), html.escape(js_msg)) + urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded) ans = message.ask('Javascript confirm', msg, mode=usertypes.PromptMode.yesno, - abort_on=abort_on) + abort_on=abort_on, url=urlstr) return bool(ans) @@ -94,10 +99,11 @@ def javascript_prompt(url, js_msg, default, abort_on): msg = '{} asks:
{}'.format(html.escape(url.toDisplayString()), html.escape(js_msg)) + urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded) answer = message.ask('Javascript prompt', msg, mode=usertypes.PromptMode.text, default=default, - abort_on=abort_on) + abort_on=abort_on, url=urlstr) if answer is None: return (False, "") @@ -116,8 +122,9 @@ def javascript_alert(url, js_msg, abort_on): msg = 'From {}:
{}'.format(html.escape(url.toDisplayString()), html.escape(js_msg)) + urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded) message.ask('Javascript alert', msg, mode=usertypes.PromptMode.alert, - abort_on=abort_on) + abort_on=abort_on, url=urlstr) def javascript_log_message(level, source, line, msg): @@ -164,9 +171,10 @@ def ignore_certificate_errors(url, errors, abort_on): """.strip()) msg = err_template.render(url=url, errors=errors) + urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded) ignore = message.ask(title="Certificate errors - continue?", text=msg, mode=usertypes.PromptMode.yesno, default=False, - abort_on=abort_on) + abort_on=abort_on, url=urlstr) if ignore is None: # prompt aborted ignore = False @@ -202,15 +210,17 @@ def feature_permission(url, option, msg, yes_action, no_action, abort_on): config_val = config.instance.get(option) if config_val == 'ask': if url.isValid(): + urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded) text = "Allow the website at {} to {}?".format( html.escape(url.toDisplayString()), msg) else: + urlstr = None text = "Allow the website to {}?".format(msg) return message.confirm_async( yes_action=yes_action, no_action=no_action, cancel_action=no_action, abort_on=abort_on, - title='Permission request', text=text) + title='Permission request', text=text, url=urlstr) elif config_val: yes_action() return None @@ -260,3 +270,41 @@ def get_user_stylesheet(): css += '\nhtml > ::-webkit-scrollbar { width: 0px; height: 0px; }' return css + + +def netrc_authentication(url, authenticator): + """Perform authorization using netrc. + + Args: + url: The URL the request was done for. + authenticator: QAuthenticator object used to set credentials provided. + + Return: + True if netrc found credentials for the URL. + False otherwise. + """ + if 'HOME' not in os.environ: + # We'll get an OSError by netrc if 'HOME' isn't available in + # os.environ. We don't want to log that, so we prevent it + # altogether. + return False + user, password = None, None + try: + net = netrc.netrc(config.val.content.netrc_file) + authenticators = net.authenticators(url.host()) + if authenticators is not None: + (user, _account, password) = authenticators + except FileNotFoundError: + log.misc.debug("No .netrc file found") + except OSError: + log.misc.exception("Unable to read the netrc file") + except netrc.NetrcParseError: + log.misc.exception("Error when parsing the netrc file") + + if user is None: + return False + + authenticator.setUser(user) + authenticator.setPassword(password) + + return True diff --git a/qutebrowser/browser/signalfilter.py b/qutebrowser/browser/signalfilter.py index 90b85c586..663aa67e7 100644 --- a/qutebrowser/browser/signalfilter.py +++ b/qutebrowser/browser/signalfilter.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/urlmarks.py b/qutebrowser/browser/urlmarks.py index 5e2c60dfb..0a0dfb4f2 100644 --- a/qutebrowser/browser/urlmarks.py +++ b/qutebrowser/browser/urlmarks.py @@ -1,7 +1,7 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) -# Copyright 2015-2017 Antoni Boucher +# Copyright 2014-2018 Florian Bruhin (The Compiler) +# Copyright 2015-2018 Antoni Boucher # # This file is part of qutebrowser. # @@ -161,7 +161,7 @@ class QuickmarkManager(UrlMarkManager): "Add quickmark:", usertypes.PromptMode.text, functools.partial(self.quickmark_add, urlstr), text="Please enter a quickmark name for
{}".format( - html.escape(url.toDisplayString()))) + html.escape(url.toDisplayString())), url=urlstr) @cmdutils.register(instance='quickmark-manager') def quickmark_add(self, url, name): @@ -192,7 +192,7 @@ class QuickmarkManager(UrlMarkManager): if name in self.marks: message.confirm_async( title="Override existing quickmark?", - yes_action=set_mark, default=True) + yes_action=set_mark, default=True, url=url) else: set_mark() @@ -280,7 +280,7 @@ class BookmarkManager(UrlMarkManager): if urlstr in self.marks: if toggle: - del self.marks[urlstr] + self.delete(urlstr) return False else: raise AlreadyExistsError("Bookmark already exists!") diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py index 4a1cc02b5..122e7d031 100644 --- a/qutebrowser/browser/webelem.py +++ b/qutebrowser/browser/webelem.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -42,7 +42,9 @@ Group = enum.Enum('Group', ['all', 'links', 'images', 'url', 'inputs']) SELECTORS = { Group.all: ('a, area, textarea, select, input:not([type=hidden]), button, ' 'frame, iframe, link, [onclick], [onmousedown], [role=link], ' - '[role=option], [role=button], img'), + '[role=option], [role=button], img, ' + # Angular 1 selectors + '[ng-click], [ngClick], [data-ng-click], [x-ng-click]'), Group.links: 'a[href], area[href], link[href], [role=link][href]', Group.images: 'img', Group.url: '[src], [href]', diff --git a/qutebrowser/browser/webengine/__init__.py b/qutebrowser/browser/webengine/__init__.py index 60d140540..2649645d3 100644 --- a/qutebrowser/browser/webengine/__init__.py +++ b/qutebrowser/browser/webengine/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webengine/certificateerror.py b/qutebrowser/browser/webengine/certificateerror.py index c97e51b81..47953d4cc 100644 --- a/qutebrowser/browser/webengine/certificateerror.py +++ b/qutebrowser/browser/webengine/certificateerror.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016-2017 Florian Bruhin (The Compiler) +# Copyright 2016-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webengine/interceptor.py b/qutebrowser/browser/webengine/interceptor.py index e34718791..480e8ee85 100644 --- a/qutebrowser/browser/webengine/interceptor.py +++ b/qutebrowser/browser/webengine/interceptor.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016-2017 Florian Bruhin (The Compiler) +# Copyright 2016-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webengine/spell.py b/qutebrowser/browser/webengine/spell.py index 9166180d4..beebe4da7 100644 --- a/qutebrowser/browser/webengine/spell.py +++ b/qutebrowser/browser/webengine/spell.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2017 Michal Siedlaczek +# Copyright 2017-2018 Michal Siedlaczek # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webengine/tabhistory.py b/qutebrowser/browser/webengine/tabhistory.py index 5db6faeb1..81f0a3afd 100644 --- a/qutebrowser/browser/webengine/tabhistory.py +++ b/qutebrowser/browser/webengine/tabhistory.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2017 Florian Bruhin (The Compiler) +# Copyright 2015-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webengine/webenginedownloads.py b/qutebrowser/browser/webengine/webenginedownloads.py index d467724d5..7c702a56f 100644 --- a/qutebrowser/browser/webengine/webenginedownloads.py +++ b/qutebrowser/browser/webengine/webenginedownloads.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -45,6 +45,10 @@ class DownloadItem(downloads.AbstractDownloadItem): qt_item.downloadProgress.connect(self.stats.on_download_progress) qt_item.stateChanged.connect(self._on_state_changed) + # Ensure wrapped qt_item is deleted manually when the wrapper object + # is deleted. See https://github.com/qutebrowser/qutebrowser/issues/3373 + self.destroyed.connect(self._qt_item.deleteLater) + def _is_page_download(self): """Check if this item is a page (i.e. mhtml) download.""" return (self._qt_item.savePageFormat() != @@ -96,9 +100,15 @@ class DownloadItem(downloads.AbstractDownloadItem): self._qt_item.cancel() def retry(self): - # https://bugreports.qt.io/browse/QTBUG-56840 - raise downloads.UnsupportedOperationError( - "Retrying downloads is unsupported with QtWebEngine") + state = self._qt_item.state() + assert state == QWebEngineDownloadItem.DownloadInterrupted, state + + try: + self._qt_item.resume() + except AttributeError: + raise downloads.UnsupportedOperationError( + "Retrying downloads is unsupported with QtWebEngine on " + "Qt/PyQt < 5.10") def _get_open_filename(self): return self._filename @@ -125,6 +135,7 @@ class DownloadItem(downloads.AbstractDownloadItem): question = usertypes.Question() question.title = title question.text = msg + question.url = 'file://{}'.format(self._filename) question.mode = usertypes.PromptMode.yesno question.answered_yes.connect(self._after_set_filename) question.answered_no.connect(no_action) @@ -139,6 +150,7 @@ class DownloadItem(downloads.AbstractDownloadItem): question = usertypes.Question() question.title = title question.text = msg + question.url = 'file://{}'.format(os.path.dirname(self._filename)) question.mode = usertypes.PromptMode.yesno question.answered_yes.connect(lambda: self._after_create_parent_question( diff --git a/qutebrowser/browser/webengine/webengineelem.py b/qutebrowser/browser/webengine/webengineelem.py index d6d74ebe4..127b71cf0 100644 --- a/qutebrowser/browser/webengine/webengineelem.py +++ b/qutebrowser/browser/webengine/webengineelem.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016-2017 Florian Bruhin (The Compiler) +# Copyright 2016-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webengine/webengineinspector.py b/qutebrowser/browser/webengine/webengineinspector.py index 8fa8bcb2d..b2a6ebb1e 100644 --- a/qutebrowser/browser/webengine/webengineinspector.py +++ b/qutebrowser/browser/webengine/webengineinspector.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2017 Florian Bruhin (The Compiler) +# Copyright 2015-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webengine/webenginequtescheme.py b/qutebrowser/browser/webengine/webenginequtescheme.py index 2e9aedd3e..ac583a671 100644 --- a/qutebrowser/browser/webengine/webenginequtescheme.py +++ b/qutebrowser/browser/webengine/webenginequtescheme.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016-2017 Florian Bruhin (The Compiler) +# Copyright 2016-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py index f8b54e065..2b9ae55e2 100644 --- a/qutebrowser/browser/webengine/webenginesettings.py +++ b/qutebrowser/browser/webengine/webenginesettings.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016-2017 Florian Bruhin (The Compiler) +# Copyright 2016-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -17,9 +17,6 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -# We get various "abstract but not overridden" warnings -# pylint: disable=abstract-method - """Bridge from QWebEngineSettings to our own settings. Module attributes: @@ -44,116 +41,143 @@ from qutebrowser.utils import (utils, standarddir, javascript, qtutils, default_profile = None # The QWebEngineProfile used for private (off-the-record) windows private_profile = None +# The global WebEngineSettings object +global_settings = None -class Base(websettings.Base): +class _SettingsWrapper: - """Base settings class with appropriate _get_global_settings.""" + """Expose a QWebEngineSettings interface which acts on all profiles. - def _get_global_settings(self): - return [default_profile.settings(), private_profile.settings()] - - -class Attribute(Base, websettings.Attribute): - - """A setting set via QWebEngineSettings::setAttribute.""" - - ENUM_BASE = QWebEngineSettings - - -class Setter(Base, websettings.Setter): - - """A setting set via a QWebEngineSettings setter method.""" - - pass - - -class FontFamilySetter(Base, websettings.FontFamilySetter): - - """A setter for a font family. - - Gets the default value from QFont. + For read operations, the default profile value is always used. """ - def __init__(self, font): - # Mapping from WebEngineSettings::initDefaults in - # qtwebengine/src/core/web_engine_settings.cpp - font_to_qfont = { - QWebEngineSettings.StandardFont: QFont.Serif, - QWebEngineSettings.FixedFont: QFont.Monospace, - QWebEngineSettings.SerifFont: QFont.Serif, - QWebEngineSettings.SansSerifFont: QFont.SansSerif, - QWebEngineSettings.CursiveFont: QFont.Cursive, - QWebEngineSettings.FantasyFont: QFont.Fantasy, + def __init__(self): + self._settings = [default_profile.settings(), + private_profile.settings()] + + def setAttribute(self, *args, **kwargs): + for settings in self._settings: + settings.setAttribute(*args, **kwargs) + + def setFontFamily(self, *args, **kwargs): + for settings in self._settings: + settings.setFontFamily(*args, **kwargs) + + def setFontSize(self, *args, **kwargs): + for settings in self._settings: + settings.setFontSize(*args, **kwargs) + + def setDefaultTextEncoding(self, *args, **kwargs): + for settings in self._settings: + settings.setDefaultTextEncoding(*args, **kwargs) + + def testAttribute(self, *args, **kwargs): + return self._settings[0].testAttribute(*args, **kwargs) + + def fontSize(self, *args, **kwargs): + return self._settings[0].fontSize(*args, **kwargs) + + def fontFamily(self, *args, **kwargs): + return self._settings[0].fontFamily(*args, **kwargs) + + def defaultTextEncoding(self, *args, **kwargs): + return self._settings[0].defaultTextEncoding(*args, **kwargs) + + +class WebEngineSettings(websettings.AbstractSettings): + + """A wrapper for the config for QWebEngineSettings.""" + + _ATTRIBUTES = { + 'content.xss_auditing': + [QWebEngineSettings.XSSAuditingEnabled], + 'content.images': + [QWebEngineSettings.AutoLoadImages], + 'content.javascript.enabled': + [QWebEngineSettings.JavascriptEnabled], + 'content.javascript.can_open_tabs_automatically': + [QWebEngineSettings.JavascriptCanOpenWindows], + 'content.javascript.can_access_clipboard': + [QWebEngineSettings.JavascriptCanAccessClipboard], + 'content.plugins': + [QWebEngineSettings.PluginsEnabled], + 'content.hyperlink_auditing': + [QWebEngineSettings.HyperlinkAuditingEnabled], + 'content.local_content_can_access_remote_urls': + [QWebEngineSettings.LocalContentCanAccessRemoteUrls], + 'content.local_content_can_access_file_urls': + [QWebEngineSettings.LocalContentCanAccessFileUrls], + 'content.webgl': + [QWebEngineSettings.WebGLEnabled], + 'content.local_storage': + [QWebEngineSettings.LocalStorageEnabled], + + 'input.spatial_navigation': + [QWebEngineSettings.SpatialNavigationEnabled], + 'input.links_included_in_focus_chain': + [QWebEngineSettings.LinksIncludedInFocusChain], + + 'scrolling.smooth': + [QWebEngineSettings.ScrollAnimatorEnabled], + + # Missing QtWebEngine attributes: + # - ScreenCaptureEnabled + # - Accelerated2dCanvasEnabled + # - AutoLoadIconsForPage + # - TouchIconsEnabled + # - FocusOnNavigationEnabled (5.8) + # - AllowRunningInsecureContent (5.8) + } + + _FONT_SIZES = { + 'fonts.web.size.minimum': + QWebEngineSettings.MinimumFontSize, + 'fonts.web.size.minimum_logical': + QWebEngineSettings.MinimumLogicalFontSize, + 'fonts.web.size.default': + QWebEngineSettings.DefaultFontSize, + 'fonts.web.size.default_fixed': + QWebEngineSettings.DefaultFixedFontSize, + } + + _FONT_FAMILIES = { + 'fonts.web.family.standard': QWebEngineSettings.StandardFont, + 'fonts.web.family.fixed': QWebEngineSettings.FixedFont, + 'fonts.web.family.serif': QWebEngineSettings.SerifFont, + 'fonts.web.family.sans_serif': QWebEngineSettings.SansSerifFont, + 'fonts.web.family.cursive': QWebEngineSettings.CursiveFont, + 'fonts.web.family.fantasy': QWebEngineSettings.FantasyFont, + + # Missing QtWebEngine fonts: + # - PictographFont + } + + # Mapping from WebEngineSettings::initDefaults in + # qtwebengine/src/core/web_engine_settings.cpp + _FONT_TO_QFONT = { + QWebEngineSettings.StandardFont: QFont.Serif, + QWebEngineSettings.FixedFont: QFont.Monospace, + QWebEngineSettings.SerifFont: QFont.Serif, + QWebEngineSettings.SansSerifFont: QFont.SansSerif, + QWebEngineSettings.CursiveFont: QFont.Cursive, + QWebEngineSettings.FantasyFont: QFont.Fantasy, + } + + def __init__(self, settings): + super().__init__(settings) + # Attributes which don't exist in all Qt versions. + new_attributes = { + # Qt 5.8 + 'content.print_element_backgrounds': 'PrintElementBackgrounds', } - super().__init__(setter=QWebEngineSettings.setFontFamily, font=font, - qfont=font_to_qfont[font]) + for name, attribute in new_attributes.items(): + try: + value = getattr(QWebEngineSettings, attribute) + except AttributeError: + continue - -class DefaultProfileSetter(websettings.Base): - - """A setting set on the QWebEngineProfile.""" - - def __init__(self, setter, converter=None, default=websettings.UNSET): - super().__init__(default) - self._setter = setter - self._converter = converter - - def __repr__(self): - return utils.get_repr(self, setter=self._setter, constructor=True) - - def _set(self, value, settings=None): - if settings is not None: - raise ValueError("'settings' may not be set with " - "DefaultProfileSetters!") - - setter = getattr(default_profile, self._setter) - if self._converter is not None: - value = self._converter(value) - - setter(value) - - -class PersistentCookiePolicy(DefaultProfileSetter): - - """The content.cookies.store setting is different from other settings.""" - - def __init__(self): - super().__init__('setPersistentCookiesPolicy') - - def _set(self, value, settings=None): - if settings is not None: - raise ValueError("'settings' may not be set with " - "PersistentCookiePolicy!") - setter = getattr(QWebEngineProfile.defaultProfile(), self._setter) - setter( - QWebEngineProfile.AllowPersistentCookies if value else - QWebEngineProfile.NoPersistentCookies - ) - - -class DictionaryLanguageSetter(DefaultProfileSetter): - - """Sets paths to dictionary files based on language codes.""" - - def __init__(self): - super().__init__('setSpellCheckLanguages', default=[]) - - def _find_installed(self, code): - local_filename = spell.local_filename(code) - if not local_filename: - message.warning( - "Language {} is not installed - see scripts/dictcli.py " - "in qutebrowser's sources".format(code)) - return local_filename - - def _set(self, value, settings=None): - if settings is not None: - raise ValueError("'settings' may not be set with " - "DictionaryLanguageSetter!") - filenames = [self._find_installed(code) for code in value] - log.config.debug("Found dicts: {}".format(filenames)) - super()._set([f for f in filenames if f], settings) + self._ATTRIBUTES[name] = [value] def _init_stylesheet(profile): @@ -210,9 +234,47 @@ def _set_http_headers(profile): profile.setHttpAcceptLanguage(accept_language) +def _set_http_cache_size(profile): + """Initialize the HTTP cache size for the given profile.""" + size = config.val.content.cache.size + if size is None: + size = 0 + else: + size = qtutils.check_overflow(size, 'int', fatal=False) + + # 0: automatically managed by QtWebEngine + profile.setHttpCacheMaximumSize(size) + + +def _set_persistent_cookie_policy(profile): + """Set the HTTP Cookie size for the given profile.""" + if config.val.content.cookies.store: + value = QWebEngineProfile.AllowPersistentCookies + else: + value = QWebEngineProfile.NoPersistentCookies + profile.setPersistentCookiesPolicy(value) + + +def _set_dictionary_language(profile): + filenames = [] + for code in config.val.spellcheck.languages or []: + local_filename = spell.local_filename(code) + if not local_filename: + message.warning( + "Language {} is not installed - see scripts/dictcli.py " + "in qutebrowser's sources".format(code)) + continue + + filenames.append(local_filename) + + log.config.debug("Found dicts: {}".format(filenames)) + profile.setSpellCheckLanguages(filenames) + + def _update_settings(option): """Update global settings when qwebsettings changed.""" - websettings.update_mappings(MAPPINGS, option) + global_settings.update_setting(option) + if option in ['scrolling.bar', 'content.user_stylesheets']: _init_stylesheet(default_profile) _init_stylesheet(private_profile) @@ -221,27 +283,46 @@ def _update_settings(option): 'content.headers.accept_language']: _set_http_headers(default_profile) _set_http_headers(private_profile) + elif option == 'content.cache.size': + _set_http_cache_size(default_profile) + _set_http_cache_size(private_profile) + elif (option == 'content.cookies.store' and + # https://bugreports.qt.io/browse/QTBUG-58650 + qtutils.version_check('5.9', compiled=False)): + _set_persistent_cookie_policy(default_profile) + # We're not touching the private profile's cookie policy. + elif option == 'spellcheck.languages' and qtutils.version_check('5.8'): + _set_dictionary_language(default_profile) + _set_dictionary_language(private_profile) + + +def _init_profile(profile): + """Init the given profile.""" + _init_stylesheet(profile) + _set_http_headers(profile) + _set_http_cache_size(profile) + profile.settings().setAttribute( + QWebEngineSettings.FullScreenSupportEnabled, True) + if qtutils.version_check('5.8'): + profile.setSpellCheckEnabled(True) + _set_dictionary_language(profile) def _init_profiles(): """Init the two used QWebEngineProfiles.""" global default_profile, private_profile + default_profile = QWebEngineProfile.defaultProfile() default_profile.setCachePath( os.path.join(standarddir.cache(), 'webengine')) default_profile.setPersistentStoragePath( os.path.join(standarddir.data(), 'webengine')) - _init_stylesheet(default_profile) - _set_http_headers(default_profile) + _init_profile(default_profile) + _set_persistent_cookie_policy(default_profile) private_profile = QWebEngineProfile() assert private_profile.isOffTheRecord() - _init_stylesheet(private_profile) - _set_http_headers(private_profile) - - if qtutils.version_check('5.8'): - default_profile.setSpellCheckEnabled(True) - private_profile.setSpellCheckEnabled(True) + _init_profile(private_profile) def inject_userscripts(): @@ -287,111 +368,13 @@ def init(args): os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = str(utils.random_port()) _init_profiles() - - # We need to do this here as a WORKAROUND for - # https://bugreports.qt.io/browse/QTBUG-58650 - if not qtutils.version_check('5.9', compiled=False): - PersistentCookiePolicy().set(config.val.content.cookies.store) - Attribute(QWebEngineSettings.FullScreenSupportEnabled).set(True) - - websettings.init_mappings(MAPPINGS) config.instance.changed.connect(_update_settings) + global global_settings + global_settings = WebEngineSettings(_SettingsWrapper()) + global_settings.init_settings() + def shutdown(): # FIXME:qtwebengine do we need to do something for a clean shutdown here? pass - - -# Missing QtWebEngine attributes: -# - ScreenCaptureEnabled -# - Accelerated2dCanvasEnabled -# - AutoLoadIconsForPage -# - TouchIconsEnabled -# - FocusOnNavigationEnabled (5.8) -# - AllowRunningInsecureContent (5.8) -# -# Missing QtWebEngine fonts: -# - PictographFont - - -MAPPINGS = { - 'content.images': - Attribute(QWebEngineSettings.AutoLoadImages), - 'content.javascript.enabled': - Attribute(QWebEngineSettings.JavascriptEnabled), - 'content.javascript.can_open_tabs_automatically': - Attribute(QWebEngineSettings.JavascriptCanOpenWindows), - 'content.javascript.can_access_clipboard': - Attribute(QWebEngineSettings.JavascriptCanAccessClipboard), - 'content.plugins': - Attribute(QWebEngineSettings.PluginsEnabled), - 'content.hyperlink_auditing': - Attribute(QWebEngineSettings.HyperlinkAuditingEnabled), - 'content.local_content_can_access_remote_urls': - Attribute(QWebEngineSettings.LocalContentCanAccessRemoteUrls), - 'content.local_content_can_access_file_urls': - Attribute(QWebEngineSettings.LocalContentCanAccessFileUrls), - 'content.webgl': - Attribute(QWebEngineSettings.WebGLEnabled), - 'content.local_storage': - Attribute(QWebEngineSettings.LocalStorageEnabled), - 'content.cache.size': - # 0: automatically managed by QtWebEngine - DefaultProfileSetter('setHttpCacheMaximumSize', default=0, - converter=lambda val: - qtutils.check_overflow(val, 'int', fatal=False)), - 'content.xss_auditing': - Attribute(QWebEngineSettings.XSSAuditingEnabled), - 'content.default_encoding': - Setter(QWebEngineSettings.setDefaultTextEncoding), - - 'input.spatial_navigation': - Attribute(QWebEngineSettings.SpatialNavigationEnabled), - 'input.links_included_in_focus_chain': - Attribute(QWebEngineSettings.LinksIncludedInFocusChain), - - 'fonts.web.family.standard': - FontFamilySetter(QWebEngineSettings.StandardFont), - 'fonts.web.family.fixed': - FontFamilySetter(QWebEngineSettings.FixedFont), - 'fonts.web.family.serif': - FontFamilySetter(QWebEngineSettings.SerifFont), - 'fonts.web.family.sans_serif': - FontFamilySetter(QWebEngineSettings.SansSerifFont), - 'fonts.web.family.cursive': - FontFamilySetter(QWebEngineSettings.CursiveFont), - 'fonts.web.family.fantasy': - FontFamilySetter(QWebEngineSettings.FantasyFont), - 'fonts.web.size.minimum': - Setter(QWebEngineSettings.setFontSize, - args=[QWebEngineSettings.MinimumFontSize]), - 'fonts.web.size.minimum_logical': - Setter(QWebEngineSettings.setFontSize, - args=[QWebEngineSettings.MinimumLogicalFontSize]), - 'fonts.web.size.default': - Setter(QWebEngineSettings.setFontSize, - args=[QWebEngineSettings.DefaultFontSize]), - 'fonts.web.size.default_fixed': - Setter(QWebEngineSettings.setFontSize, - args=[QWebEngineSettings.DefaultFixedFontSize]), - - 'scrolling.smooth': - Attribute(QWebEngineSettings.ScrollAnimatorEnabled), -} - -try: - MAPPINGS['content.print_element_backgrounds'] = Attribute( - QWebEngineSettings.PrintElementBackgrounds) -except AttributeError: - # Added in Qt 5.8 - pass - - -if qtutils.version_check('5.8'): - MAPPINGS['spellcheck.languages'] = DictionaryLanguageSetter() - - -if qtutils.version_check('5.9', compiled=False): - # https://bugreports.qt.io/browse/QTBUG-58650 - MAPPINGS['content.cookies.store'] = PersistentCookiePolicy() diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 9328698bc..1fef0f6c0 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016-2017 Florian Bruhin (The Compiler) +# Copyright 2016-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -21,6 +21,8 @@ import math import functools +import sys +import re import html as html_utils import sip @@ -98,6 +100,19 @@ class WebEngineAction(browsertab.AbstractAction): """Save the current page.""" self._widget.triggerPageAction(QWebEnginePage.SavePage) + def show_source(self): + try: + self._widget.triggerPageAction(QWebEnginePage.ViewSource) + except AttributeError: + # Qt < 5.8 + tb = objreg.get('tabbed-browser', scope='window', + window=self._tab.win_id) + urlstr = self._tab.url().toString(QUrl.RemoveUserInfo) + # The original URL becomes the path of a view-source: URL + # (without a host), but query/fragment should stay. + url = QUrl('view-source:' + urlstr) + tb.tabopen(url, background=False, related=True) + class WebEnginePrinting(browsertab.AbstractPrinting): @@ -201,70 +216,90 @@ class WebEngineCaret(browsertab.AbstractCaret): @pyqtSlot(usertypes.KeyMode) def _on_mode_entered(self, mode): - pass + if mode != usertypes.KeyMode.caret: + return + + self._tab.run_js_async( + javascript.assemble('caret', 'setPlatform', sys.platform)) + self._js_call('setInitialCursor') @pyqtSlot(usertypes.KeyMode) - def _on_mode_left(self): - pass + def _on_mode_left(self, mode): + if mode != usertypes.KeyMode.caret: + return + + self.drop_selection() + self._js_call('disableCaret') def move_to_next_line(self, count=1): - log.stub() + for _ in range(count): + self._js_call('moveDown') def move_to_prev_line(self, count=1): - log.stub() + for _ in range(count): + self._js_call('moveUp') def move_to_next_char(self, count=1): - log.stub() + for _ in range(count): + self._js_call('moveRight') def move_to_prev_char(self, count=1): - log.stub() + for _ in range(count): + self._js_call('moveLeft') def move_to_end_of_word(self, count=1): - log.stub() + for _ in range(count): + self._js_call('moveToEndOfWord') def move_to_next_word(self, count=1): - log.stub() + for _ in range(count): + self._js_call('moveToNextWord') def move_to_prev_word(self, count=1): - log.stub() + for _ in range(count): + self._js_call('moveToPreviousWord') def move_to_start_of_line(self): - log.stub() + self._js_call('moveToStartOfLine') def move_to_end_of_line(self): - log.stub() + self._js_call('moveToEndOfLine') def move_to_start_of_next_block(self, count=1): - log.stub() + for _ in range(count): + self._js_call('moveToStartOfNextBlock') def move_to_start_of_prev_block(self, count=1): - log.stub() + for _ in range(count): + self._js_call('moveToStartOfPrevBlock') def move_to_end_of_next_block(self, count=1): - log.stub() + for _ in range(count): + self._js_call('moveToEndOfNextBlock') def move_to_end_of_prev_block(self, count=1): - log.stub() + for _ in range(count): + self._js_call('moveToEndOfPrevBlock') def move_to_start_of_document(self): - log.stub() + self._js_call('moveToStartOfDocument') def move_to_end_of_document(self): - log.stub() + self._js_call('moveToEndOfDocument') def toggle_selection(self): - log.stub() + self._js_call('toggleSelection') def drop_selection(self): - log.stub() + self._js_call('dropSelection') - def has_selection(self): - return self._widget.hasSelection() - - def selection(self, html=False): - if html: - raise browsertab.UnsupportedOperationError - return self._widget.selectedText() + def selection(self, callback): + # Not using selectedText() as WORKAROUND for + # https://bugreports.qt.io/browse/QTBUG-53134 + # Even on Qt 5.10 selectedText() seems to work poorly, see + # https://github.com/qutebrowser/qutebrowser/issues/3523 + self._tab.run_js_async(javascript.assemble('caret', 'getSelection'), + callback) def _follow_selected_cb(self, js_elem, tab=False): """Callback for javascript which clicks the selected element. @@ -308,6 +343,10 @@ class WebEngineCaret(browsertab.AbstractCaret): self._tab.run_js_async(js_code, lambda jsret: self._follow_selected_cb(jsret, tab)) + def _js_call(self, command): + self._tab.run_js_async( + javascript.assemble('caret', command)) + class WebEngineScroller(browsertab.AbstractScroller): @@ -435,7 +474,8 @@ class WebEngineHistory(browsertab.AbstractHistory): return self._history.itemAt(i) def _go_to_item(self, item): - return self._history.goToItem(item) + self._tab.predicted_navigation.emit(item.url()) + self._history.goToItem(item) def serialize(self): if not qtutils.version_check('5.9', compiled=False): @@ -455,13 +495,18 @@ class WebEngineHistory(browsertab.AbstractHistory): def load_items(self, items): stream, _data, cur_data = tabhistory.serialize(items) qtutils.deserialize_stream(stream, self._history) + + @pyqtSlot() + def _on_load_finished(): + self._tab.scroller.to_point(cur_data['scroll-pos']) + self._tab.load_finished.disconnect(_on_load_finished) + if cur_data is not None: if 'zoom' in cur_data: self._tab.zoom.set_factor(cur_data['zoom']) if ('scroll-pos' in cur_data and self._tab.scroller.pos_px() == QPoint(0, 0)): - QTimer.singleShot(0, functools.partial( - self._tab.scroller.to_point, cur_data['scroll-pos'])) + self._tab.load_finished.connect(_on_load_finished) class WebEngineZoom(browsertab.AbstractZoom): @@ -557,19 +602,22 @@ class WebEngineTab(browsertab.AbstractTab): private=private) self.history = WebEngineHistory(self) self.scroller = WebEngineScroller(self, parent=self) - self.caret = WebEngineCaret(win_id=win_id, mode_manager=mode_manager, + self.caret = WebEngineCaret(mode_manager=mode_manager, tab=self, parent=self) - self.zoom = WebEngineZoom(win_id=win_id, parent=self) + self.zoom = WebEngineZoom(tab=self, parent=self) self.search = WebEngineSearch(parent=self) self.printing = WebEnginePrinting() - self.elements = WebEngineElements(self) - self.action = WebEngineAction() + self.elements = WebEngineElements(tab=self) + self.action = WebEngineAction(tab=self) + # We're assigning settings in _set_widget + self.settings = webenginesettings.WebEngineSettings(settings=None) self._set_widget(widget) self._connect_signals() self.backend = usertypes.Backend.QtWebEngine self._init_js() self._child_event_filter = None self._saved_zoom = None + self._reload_url = None def _init_js(self): js_code = '\n'.join([ @@ -577,9 +625,12 @@ class WebEngineTab(browsertab.AbstractTab): 'window._qutebrowser = window._qutebrowser || {};', utils.read_file('javascript/scroll.js'), utils.read_file('javascript/webelem.js'), + utils.read_file('javascript/caret.js'), ]) script = QWebEngineScript() - script.setInjectionPoint(QWebEngineScript.DocumentCreation) + # We can't use DocumentCreation here as WORKAROUND for + # https://bugreports.qt.io/browse/QTBUG-66011 + script.setInjectionPoint(QWebEngineScript.DocumentReady) script.setSourceCode(js_code) page = self._widget.page() @@ -597,6 +648,9 @@ class WebEngineTab(browsertab.AbstractTab): @pyqtSlot() def _restore_zoom(self): + if sip.isdeleted(self._widget): + # https://github.com/qutebrowser/qutebrowser/issues/3498 + return if self._saved_zoom is None: return self.zoom.set_factor(self._saved_zoom) @@ -682,6 +736,16 @@ class WebEngineTab(browsertab.AbstractTab): self.send_event(press_evt) self.send_event(release_evt) + def _show_error_page(self, url, error): + """Show an error page in the tab.""" + log.misc.debug("Showing error page for {}".format(error)) + url_string = url.toDisplayString() + error_page = jinja.render( + 'error.html', + title="Error loading page: {}".format(url_string), + url=url_string, error=error) + self.set_html(error_page) + @pyqtSlot() def _on_history_trigger(self): try: @@ -716,10 +780,11 @@ class WebEngineTab(browsertab.AbstractTab): """Called when a proxy needs authentication.""" msg = "{} requires a username and password.".format( html_utils.escape(proxy_host)) + urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded) answer = message.ask( title="Proxy authentication required", text=msg, mode=usertypes.PromptMode.user_pwd, - abort_on=[self.shutting_down, self.load_started]) + abort_on=[self.shutting_down, self.load_started], url=urlstr) if answer is not None: authenticator.setUser(answer.user) authenticator.setPassword(answer.password) @@ -729,21 +794,19 @@ class WebEngineTab(browsertab.AbstractTab): sip.assign(authenticator, QAuthenticator()) # pylint: enable=no-member, useless-suppression except AttributeError: - url_string = url.toDisplayString() - error_page = jinja.render( - 'error.html', - title="Error loading page: {}".format(url_string), - url=url_string, error="Proxy authentication required", - icon='') - self.set_html(error_page) + self._show_error_page(url, "Proxy authentication required") @pyqtSlot(QUrl, 'QAuthenticator*') def _on_authentication_required(self, url, authenticator): - # FIXME:qtwebengine support .netrc - answer = shared.authentication_required( - url, authenticator, abort_on=[self.shutting_down, - self.load_started]) - if answer is None: + netrc_success = False + if not self.data.netrc_used: + self.data.netrc_used = True + netrc_success = shared.netrc_authentication(url, authenticator) + if not netrc_success: + abort_on = [self.shutting_down, self.load_started] + answer = shared.authentication_required(url, authenticator, + abort_on) + if not netrc_success and answer is None: try: # pylint: disable=no-member, useless-suppression sip.assign(authenticator, QAuthenticator()) @@ -751,12 +814,7 @@ class WebEngineTab(browsertab.AbstractTab): except AttributeError: # WORKAROUND for # https://www.riverbankcomputing.com/pipermail/pyqt/2016-December/038400.html - url_string = url.toDisplayString() - error_page = jinja.render( - 'error.html', - title="Error loading page: {}".format(url_string), - url=url_string, error="Authentication required") - self.set_html(error_page) + self._show_error_page(url, "Authentication required") @pyqtSlot('QWebEngineFullScreenRequest') def _on_fullscreen_requested(self, request): @@ -779,6 +837,7 @@ class WebEngineTab(browsertab.AbstractTab): # https://bugreports.qt.io/browse/QTBUG-61506 self.search.clear() super()._on_load_started() + self.data.netrc_used = False @pyqtSlot(QWebEnginePage.RenderProcessTerminationStatus, int) def _on_render_process_terminated(self, status, exitcode): @@ -820,6 +879,54 @@ class WebEngineTab(browsertab.AbstractTab): if not ok: self._load_finished_fake.emit(False) + def _error_page_workaround(self, html): + """Check if we're displaying a Chromium error page. + + This gets only called if we got loadFinished(False) without JavaScript, + so we can display at least some error page. + + WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66643 + Needs to check the page content as a WORKAROUND for + https://bugreports.qt.io/browse/QTBUG-66661 + """ + match = re.search(r'"errorCode":"([^"]*)"', html) + if match is None: + return + self._show_error_page(self.url(), error=match.group(1)) + + @pyqtSlot(bool) + def _on_load_finished(self, ok): + """Display a static error page if JavaScript is disabled.""" + super()._on_load_finished(ok) + js_enabled = self.settings.test_attribute('content.javascript.enabled') + if not ok and not js_enabled: + self.dump_async(self._error_page_workaround) + + if ok and self._reload_url is not None: + # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66656 + log.config.debug( + "Reloading {} because of config change".format( + self._reload_url.toDisplayString())) + QTimer.singleShot(100, lambda url=self._reload_url: + self.openurl(url)) + self._reload_url = None + + @pyqtSlot(QUrl) + def _on_predicted_navigation(self, url): + """If we know we're going to visit an URL soon, change the settings.""" + self.settings.update_for_url(url) + + @pyqtSlot(usertypes.NavigationRequest) + def _on_navigation_request(self, navigation): + super()._on_navigation_request(navigation) + if navigation.accepted and navigation.is_main_frame: + changed = self.settings.update_for_url(navigation.url) + needs_reload = {'content.plugins', 'content.javascript.enabled'} + if (changed & needs_reload and navigation.navigation_type != + navigation.Type.link_clicked): + # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66656 + self._reload_url = navigation.url + def _connect_signals(self): view = self._widget page = view.page() @@ -834,6 +941,7 @@ class WebEngineTab(browsertab.AbstractTab): self._on_proxy_authentication_required) page.fullScreenRequested.connect(self._on_fullscreen_requested) page.contentsSizeChanged.connect(self.contents_size_changed) + page.navigation_request.connect(self._on_navigation_request) view.titleChanged.connect(self.title_changed) view.urlChanged.connect(self._on_url_changed) @@ -854,5 +962,7 @@ class WebEngineTab(browsertab.AbstractTab): page.loadFinished.connect(self._restore_zoom) page.loadFinished.connect(self._on_load_finished) + self.predicted_navigation.connect(self._on_predicted_navigation) + def event_target(self): return self._widget.focusProxy() diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py index b313fc36c..91c5bfab6 100644 --- a/qutebrowser/browser/webengine/webview.py +++ b/qutebrowser/browser/webengine/webview.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -29,8 +29,7 @@ from PyQt5.QtWebEngineWidgets import (QWebEngineView, QWebEnginePage, from qutebrowser.browser import shared from qutebrowser.browser.webengine import certificateerror, webenginesettings from qutebrowser.config import config -from qutebrowser.utils import (log, debug, usertypes, jinja, urlutils, message, - objreg, qtutils) +from qutebrowser.utils import log, debug, usertypes, jinja, objreg, qtutils class WebEngineView(QWebEngineView): @@ -124,10 +123,12 @@ class WebEnginePage(QWebEnginePage): Signals: certificate_error: Emitted on certificate errors. shutting_down: Emitted when the page is shutting down. + navigation_request: Emitted on acceptNavigationRequest. """ certificate_error = pyqtSignal() shutting_down = pyqtSignal() + navigation_request = pyqtSignal(usertypes.NavigationRequest) def __init__(self, *, theme_color, profile, parent=None): super().__init__(profile, parent) @@ -288,21 +289,26 @@ class WebEnginePage(QWebEnginePage): url: QUrl, typ: QWebEnginePage.NavigationType, is_main_frame: bool): - """Override acceptNavigationRequest to handle clicked links. - - This only show an error on invalid links - everything else is handled - in createWindow. - """ - log.webview.debug("navigation request: url {}, type {}, is_main_frame " - "{}".format(url.toDisplayString(), - debug.qenum_key(QWebEnginePage, typ), - is_main_frame)) - if (typ == QWebEnginePage.NavigationTypeLinkClicked and - not url.isValid()): - msg = urlutils.get_errstring(url, "Invalid link clicked") - message.error(msg) - return False - return True + """Override acceptNavigationRequest to forward it to the tab API.""" + type_map = { + QWebEnginePage.NavigationTypeLinkClicked: + usertypes.NavigationRequest.Type.link_clicked, + QWebEnginePage.NavigationTypeTyped: + usertypes.NavigationRequest.Type.typed, + QWebEnginePage.NavigationTypeFormSubmitted: + usertypes.NavigationRequest.Type.form_submitted, + QWebEnginePage.NavigationTypeBackForward: + usertypes.NavigationRequest.Type.back_forward, + QWebEnginePage.NavigationTypeReload: + usertypes.NavigationRequest.Type.reloaded, + QWebEnginePage.NavigationTypeOther: + usertypes.NavigationRequest.Type.other, + } + navigation = usertypes.NavigationRequest(url=url, + navigation_type=type_map[typ], + is_main_frame=is_main_frame) + self.navigation_request.emit(navigation) + return navigation.accepted @pyqtSlot('QUrl') def _inject_userjs(self, url): diff --git a/qutebrowser/browser/webkit/__init__.py b/qutebrowser/browser/webkit/__init__.py index 93b53cdba..5100b7a53 100644 --- a/qutebrowser/browser/webkit/__init__.py +++ b/qutebrowser/browser/webkit/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webkit/cache.py b/qutebrowser/browser/webkit/cache.py index 998545509..163612ce9 100644 --- a/qutebrowser/browser/webkit/cache.py +++ b/qutebrowser/browser/webkit/cache.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webkit/certificateerror.py b/qutebrowser/browser/webkit/certificateerror.py index 23353f268..cad17fba5 100644 --- a/qutebrowser/browser/webkit/certificateerror.py +++ b/qutebrowser/browser/webkit/certificateerror.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016-2017 Florian Bruhin (The Compiler) +# Copyright 2016-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webkit/cookies.py b/qutebrowser/browser/webkit/cookies.py index f08e2e546..01b6842a0 100644 --- a/qutebrowser/browser/webkit/cookies.py +++ b/qutebrowser/browser/webkit/cookies.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webkit/http.py b/qutebrowser/browser/webkit/http.py index 8997f4b0c..73e620015 100644 --- a/qutebrowser/browser/webkit/http.py +++ b/qutebrowser/browser/webkit/http.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webkit/mhtml.py b/qutebrowser/browser/webkit/mhtml.py index 67c8a5b7a..5f495274a 100644 --- a/qutebrowser/browser/webkit/mhtml.py +++ b/qutebrowser/browser/webkit/mhtml.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2017 Daniel Schadt +# Copyright 2015-2018 Daniel Schadt # # This file is part of qutebrowser. # @@ -254,7 +254,6 @@ class _Downloader: _finished_file: A flag indicating if the file has already been written. _used: A flag indicating if the downloader has already been used. - _win_id: The window this downloader belongs to. """ def __init__(self, tab, target): @@ -265,7 +264,6 @@ class _Downloader: self.pending_downloads = set() self._finished_file = False self._used = False - self._win_id = tab.win_id def run(self): """Download and save the page. @@ -365,8 +363,7 @@ class _Downloader: self.writer.add_file(urlutils.encoded_url(url), b'') return - download_manager = objreg.get('qtnetwork-download-manager', - scope='window', window=self._win_id) + download_manager = objreg.get('qtnetwork-download-manager') target = downloads.FileObjDownloadTarget(_NoCloseBytesIO()) item = download_manager.get(url, target=target, auto_remove=True) diff --git a/qutebrowser/browser/webkit/network/filescheme.py b/qutebrowser/browser/webkit/network/filescheme.py index a971e3257..840ed6a4a 100644 --- a/qutebrowser/browser/webkit/network/filescheme.py +++ b/qutebrowser/browser/webkit/network/filescheme.py @@ -1,7 +1,7 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) -# Copyright 2015-2017 Antoni Boucher (antoyo) +# Copyright 2014-2018 Florian Bruhin (The Compiler) +# Copyright 2015-2018 Antoni Boucher (antoyo) # # This file is part of qutebrowser. # @@ -25,7 +25,7 @@ import os -from qutebrowser.browser.webkit.network import schemehandler, networkreply +from qutebrowser.browser.webkit.network import networkreply from qutebrowser.utils import jinja @@ -111,27 +111,21 @@ def dirbrowser_html(path): return html.encode('UTF-8', errors='xmlcharrefreplace') -class FileSchemeHandler(schemehandler.SchemeHandler): +def handler(request): + """Handler for a file:// URL. - """Scheme handler for file: URLs.""" + Args: + request: QNetworkRequest to answer to. - def createRequest(self, _op, request, _outgoing_data): - """Create a new request. - - Args: - request: const QNetworkRequest & req - _op: Operation op - _outgoing_data: QIODevice * outgoingData - - Return: - A QNetworkReply for directories, None for files. - """ - path = request.url().toLocalFile() - try: - if os.path.isdir(path): - data = dirbrowser_html(path) - return networkreply.FixedDataNetworkReply( - request, data, 'text/html', self.parent()) - return None - except UnicodeEncodeError: - return None + Return: + A QNetworkReply for directories, None for files. + """ + path = request.url().toLocalFile() + try: + if os.path.isdir(path): + data = dirbrowser_html(path) + return networkreply.FixedDataNetworkReply( + request, data, 'text/html') + return None + except UnicodeEncodeError: + return None diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py index a19687eb1..53508aaa6 100644 --- a/qutebrowser/browser/webkit/network/networkmanager.py +++ b/qutebrowser/browser/webkit/network/networkmanager.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -19,9 +19,7 @@ """Our own QNetworkAccessManager.""" -import os import collections -import netrc import html import attr @@ -127,10 +125,13 @@ class NetworkManager(QNetworkAccessManager): _scheme_handlers: A dictionary (scheme -> handler) of supported custom schemes. _win_id: The window ID this NetworkManager is associated with. + (or None for generic network managers) _tab_id: The tab ID this NetworkManager is associated with. + (or None for generic network managers) _rejected_ssl_errors: A {QUrl: [SslError]} dict of rejected errors. _accepted_ssl_errors: A {QUrl: [SslError]} dict of accepted errors. _private: Whether we're in private browsing mode. + netrc_used: Whether netrc authentication was performed. Signals: shutting_down: Emitted when the QNAM is shutting down. @@ -150,8 +151,8 @@ class NetworkManager(QNetworkAccessManager): self._tab_id = tab_id self._private = private self._scheme_handlers = { - 'qute': webkitqutescheme.QuteSchemeHandler(win_id), - 'file': filescheme.FileSchemeHandler(win_id), + 'qute': webkitqutescheme.handler, + 'file': filescheme.handler, } self._set_cookiejar() self._set_cache() @@ -161,6 +162,7 @@ class NetworkManager(QNetworkAccessManager): self.authenticationRequired.connect(self.on_authentication_required) self.proxyAuthenticationRequired.connect( self.on_proxy_authentication_required) + self.netrc_used = False def _set_cookiejar(self): """Set the cookie jar of the NetworkManager correctly.""" @@ -194,6 +196,7 @@ class NetworkManager(QNetworkAccessManager): # This might be a generic network manager, e.g. one belonging to a # DownloadManager. In this case, just skip the webview thing. if self._tab_id is not None: + assert self._win_id is not None tab = objreg.get('tab', scope='tab', window=self._win_id, tab=self._tab_id) abort_on.append(tab.load_started) @@ -270,28 +273,12 @@ class NetworkManager(QNetworkAccessManager): @pyqtSlot('QNetworkReply*', 'QAuthenticator*') def on_authentication_required(self, reply, authenticator): """Called when a website needs authentication.""" - user, password = None, None - if not hasattr(reply, "netrc_used") and 'HOME' in os.environ: - # We'll get an OSError by netrc if 'HOME' isn't available in - # os.environ. We don't want to log that, so we prevent it - # altogether. - reply.netrc_used = True - try: - net = netrc.netrc(config.val.content.netrc_file) - authenticators = net.authenticators(reply.url().host()) - if authenticators is not None: - (user, _account, password) = authenticators - except FileNotFoundError: - log.misc.debug("No .netrc file found") - except OSError: - log.misc.exception("Unable to read the netrc file") - except netrc.NetrcParseError: - log.misc.exception("Error when parsing the netrc file") - - if user is not None: - authenticator.setUser(user) - authenticator.setPassword(password) - else: + netrc_success = False + if not self.netrc_used: + self.netrc_used = True + netrc_success = shared.netrc_authentication(reply.url(), + authenticator) + if not netrc_success: abort_on = self._get_abort_signals(reply) shared.authentication_required(reply.url(), authenticator, abort_on=abort_on) @@ -386,9 +373,9 @@ class NetworkManager(QNetworkAccessManager): scheme = req.url().scheme() if scheme in self._scheme_handlers: - result = self._scheme_handlers[scheme].createRequest( - op, req, outgoing_data) + result = self._scheme_handlers[scheme](req) if result is not None: + result.setParent(self) return result for header, value in shared.custom_headers(): @@ -408,6 +395,7 @@ class NetworkManager(QNetworkAccessManager): current_url = QUrl() if self._tab_id is not None: + assert self._win_id is not None try: tab = objreg.get('tab', scope='tab', window=self._win_id, tab=self._tab_id) diff --git a/qutebrowser/browser/webkit/network/networkreply.py b/qutebrowser/browser/webkit/network/networkreply.py index 22263c96b..dc6bed5ed 100644 --- a/qutebrowser/browser/webkit/network/networkreply.py +++ b/qutebrowser/browser/webkit/network/networkreply.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # Based on the Eric5 helpviewer, # Copyright (c) 2009 - 2014 Detlev Offenbach diff --git a/qutebrowser/browser/webkit/network/schemehandler.py b/qutebrowser/browser/webkit/network/schemehandler.py deleted file mode 100644 index c6337efa3..000000000 --- a/qutebrowser/browser/webkit/network/schemehandler.py +++ /dev/null @@ -1,51 +0,0 @@ -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# Copyright 2014-2017 Florian Bruhin (The Compiler) -# -# Based on the Eric5 helpviewer, -# Copyright (c) 2009 - 2014 Detlev Offenbach -# -# This file is part of qutebrowser. -# -# qutebrowser is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# qutebrowser is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with qutebrowser. If not, see . - -"""Base class for custom scheme handlers.""" - -from PyQt5.QtCore import QObject - - -class SchemeHandler(QObject): - - """Abstract base class for custom scheme handlers. - - Attributes: - _win_id: The window ID this scheme handler is associated with. - """ - - def __init__(self, win_id, parent=None): - super().__init__(parent) - self._win_id = win_id - - def createRequest(self, op, request, outgoing_data): - """Create a new request. - - Args: - op: Operation op - req: const QNetworkRequest & req - outgoing_data: QIODevice * outgoingData - - Return: - A QNetworkReply. - """ - raise NotImplementedError diff --git a/qutebrowser/browser/webkit/network/webkitqutescheme.py b/qutebrowser/browser/webkit/network/webkitqutescheme.py index 5413a4a8d..d732b6ab0 100644 --- a/qutebrowser/browser/webkit/network/webkitqutescheme.py +++ b/qutebrowser/browser/webkit/network/webkitqutescheme.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -24,46 +24,36 @@ import mimetypes from PyQt5.QtNetwork import QNetworkReply from qutebrowser.browser import pdfjs, qutescheme -from qutebrowser.browser.webkit.network import schemehandler, networkreply +from qutebrowser.browser.webkit.network import networkreply from qutebrowser.utils import log, usertypes, qtutils -class QuteSchemeHandler(schemehandler.SchemeHandler): +def handler(request): + """Scheme handler for qute:// URLs. - """Scheme handler for qute:// URLs.""" + Args: + request: QNetworkRequest to answer to. - def createRequest(self, _op, request, _outgoing_data): - """Create a new request. + Return: + A QNetworkReply. + """ + try: + mimetype, data = qutescheme.data_for_url(request.url()) + except qutescheme.NoHandlerFound: + errorstr = "No handler found for {}!".format( + request.url().toDisplayString()) + return networkreply.ErrorNetworkReply( + request, errorstr, QNetworkReply.ContentNotFoundError) + except qutescheme.QuteSchemeOSError as e: + return networkreply.ErrorNetworkReply( + request, str(e), QNetworkReply.ContentNotFoundError) + except qutescheme.QuteSchemeError as e: + return networkreply.ErrorNetworkReply(request, e.errorstring, e.error) + except qutescheme.Redirect as e: + qtutils.ensure_valid(e.url) + return networkreply.RedirectNetworkReply(e.url) - Args: - request: const QNetworkRequest & req - _op: Operation op - _outgoing_data: QIODevice * outgoingData - - Return: - A QNetworkReply. - """ - try: - mimetype, data = qutescheme.data_for_url(request.url()) - except qutescheme.NoHandlerFound: - errorstr = "No handler found for {}!".format( - request.url().toDisplayString()) - return networkreply.ErrorNetworkReply( - request, errorstr, QNetworkReply.ContentNotFoundError, - self.parent()) - except qutescheme.QuteSchemeOSError as e: - return networkreply.ErrorNetworkReply( - request, str(e), QNetworkReply.ContentNotFoundError, - self.parent()) - except qutescheme.QuteSchemeError as e: - return networkreply.ErrorNetworkReply(request, e.errorstring, - e.error, self.parent()) - except qutescheme.Redirect as e: - qtutils.ensure_valid(e.url) - return networkreply.RedirectNetworkReply(e.url, self.parent()) - - return networkreply.FixedDataNetworkReply(request, data, mimetype, - self.parent()) + return networkreply.FixedDataNetworkReply(request, data, mimetype) @qutescheme.add_handler('pdfjs', backend=usertypes.Backend.QtWebKit) diff --git a/qutebrowser/browser/webkit/rfc6266.py b/qutebrowser/browser/webkit/rfc6266.py index f83413ee2..139b4f9df 100644 --- a/qutebrowser/browser/webkit/rfc6266.py +++ b/qutebrowser/browser/webkit/rfc6266.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webkit/tabhistory.py b/qutebrowser/browser/webkit/tabhistory.py index d595a6e95..263bf6334 100644 --- a/qutebrowser/browser/webkit/tabhistory.py +++ b/qutebrowser/browser/webkit/tabhistory.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2017 Florian Bruhin (The Compiler) +# Copyright 2015-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webkit/webkitelem.py b/qutebrowser/browser/webkit/webkitelem.py index 829052798..a7d4f4b3e 100644 --- a/qutebrowser/browser/webkit/webkitelem.py +++ b/qutebrowser/browser/webkit/webkitelem.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webkit/webkithistory.py b/qutebrowser/browser/webkit/webkithistory.py index 0edbb3fa3..2c719acd6 100644 --- a/qutebrowser/browser/webkit/webkithistory.py +++ b/qutebrowser/browser/webkit/webkithistory.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2017 Florian Bruhin (The Compiler) +# Copyright 2015-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webkit/webkitinspector.py b/qutebrowser/browser/webkit/webkitinspector.py index 957d1b4d2..3098ab5af 100644 --- a/qutebrowser/browser/webkit/webkitinspector.py +++ b/qutebrowser/browser/webkit/webkitinspector.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2017 Florian Bruhin (The Compiler) +# Copyright 2015-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webkit/webkitsettings.py b/qutebrowser/browser/webkit/webkitsettings.py index 3354a9486..9b120e514 100644 --- a/qutebrowser/browser/webkit/webkitsettings.py +++ b/qutebrowser/browser/webkit/webkitsettings.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016-2017 Florian Bruhin (The Compiler) +# Copyright 2016-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -17,9 +17,6 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -# We get various "abstract but not overridden" warnings -# pylint: disable=abstract-method - """Bridge from QWebSettings to our own settings. Module attributes: @@ -37,85 +34,130 @@ from qutebrowser.utils import standarddir, urlutils from qutebrowser.browser import shared -class Base(websettings.Base): - - """Base settings class with appropriate _get_global_settings.""" - - def _get_global_settings(self): - return [QWebSettings.globalSettings()] +# The global WebKitSettings object +global_settings = None -class Attribute(Base, websettings.Attribute): +class WebKitSettings(websettings.AbstractSettings): - """A setting set via QWebSettings::setAttribute.""" + """A wrapper for the config for QWebSettings.""" - ENUM_BASE = QWebSettings + _ATTRIBUTES = { + 'content.images': + [QWebSettings.AutoLoadImages], + 'content.javascript.enabled': + [QWebSettings.JavascriptEnabled], + 'content.javascript.can_open_tabs_automatically': + [QWebSettings.JavascriptCanOpenWindows], + 'content.javascript.can_close_tabs': + [QWebSettings.JavascriptCanCloseWindows], + 'content.javascript.can_access_clipboard': + [QWebSettings.JavascriptCanAccessClipboard], + 'content.plugins': + [QWebSettings.PluginsEnabled], + 'content.webgl': + [QWebSettings.WebGLEnabled], + 'content.hyperlink_auditing': + [QWebSettings.HyperlinkAuditingEnabled], + 'content.local_content_can_access_remote_urls': + [QWebSettings.LocalContentCanAccessRemoteUrls], + 'content.local_content_can_access_file_urls': + [QWebSettings.LocalContentCanAccessFileUrls], + 'content.dns_prefetch': + [QWebSettings.DnsPrefetchEnabled], + 'content.frame_flattening': + [QWebSettings.FrameFlatteningEnabled], + 'content.cache.appcache': + [QWebSettings.OfflineWebApplicationCacheEnabled], + 'content.local_storage': + [QWebSettings.LocalStorageEnabled, + QWebSettings.OfflineStorageDatabaseEnabled], + 'content.developer_extras': + [QWebSettings.DeveloperExtrasEnabled], + 'content.print_element_backgrounds': + [QWebSettings.PrintElementBackgrounds], + 'content.xss_auditing': + [QWebSettings.XSSAuditingEnabled], + + 'input.spatial_navigation': + [QWebSettings.SpatialNavigationEnabled], + 'input.links_included_in_focus_chain': + [QWebSettings.LinksIncludedInFocusChain], + + 'zoom.text_only': + [QWebSettings.ZoomTextOnly], + 'scrolling.smooth': + [QWebSettings.ScrollAnimatorEnabled], + } + + _FONT_SIZES = { + 'fonts.web.size.minimum': + QWebSettings.MinimumFontSize, + 'fonts.web.size.minimum_logical': + QWebSettings.MinimumLogicalFontSize, + 'fonts.web.size.default': + QWebSettings.DefaultFontSize, + 'fonts.web.size.default_fixed': + QWebSettings.DefaultFixedFontSize, + } + + _FONT_FAMILIES = { + 'fonts.web.family.standard': QWebSettings.StandardFont, + 'fonts.web.family.fixed': QWebSettings.FixedFont, + 'fonts.web.family.serif': QWebSettings.SerifFont, + 'fonts.web.family.sans_serif': QWebSettings.SansSerifFont, + 'fonts.web.family.cursive': QWebSettings.CursiveFont, + 'fonts.web.family.fantasy': QWebSettings.FantasyFont, + } + + # Mapping from QWebSettings::QWebSettings() in + # qtwebkit/Source/WebKit/qt/Api/qwebsettings.cpp + _FONT_TO_QFONT = { + QWebSettings.StandardFont: QFont.Serif, + QWebSettings.FixedFont: QFont.Monospace, + QWebSettings.SerifFont: QFont.Serif, + QWebSettings.SansSerifFont: QFont.SansSerif, + QWebSettings.CursiveFont: QFont.Cursive, + QWebSettings.FantasyFont: QFont.Fantasy, + } -class Setter(Base, websettings.Setter): - - """A setting set via a QWebSettings setter method.""" - - pass +def _set_user_stylesheet(settings): + """Set the generated user-stylesheet.""" + stylesheet = shared.get_user_stylesheet().encode('utf-8') + url = urlutils.data_url('text/css;charset=utf-8', stylesheet) + settings.setUserStyleSheetUrl(url) -class StaticSetter(Base, websettings.StaticSetter): - - """A setting set via a static QWebSettings setter method.""" - - pass - - -class FontFamilySetter(Base, websettings.FontFamilySetter): - - """A setter for a font family. - - Gets the default value from QFont. - """ - - def __init__(self, font): - # Mapping from QWebSettings::QWebSettings() in - # qtwebkit/Source/WebKit/qt/Api/qwebsettings.cpp - font_to_qfont = { - QWebSettings.StandardFont: QFont.Serif, - QWebSettings.FixedFont: QFont.Monospace, - QWebSettings.SerifFont: QFont.Serif, - QWebSettings.SansSerifFont: QFont.SansSerif, - QWebSettings.CursiveFont: QFont.Cursive, - QWebSettings.FantasyFont: QFont.Fantasy, - } - super().__init__(setter=QWebSettings.setFontFamily, font=font, - qfont=font_to_qfont[font]) - - -class CookiePolicy(Base): - - """The ThirdPartyCookiePolicy setting is different from other settings.""" - - MAPPING = { +def _set_cookie_accept_policy(settings): + """Update the content.cookies.accept setting.""" + mapping = { 'all': QWebSettings.AlwaysAllowThirdPartyCookies, 'no-3rdparty': QWebSettings.AlwaysBlockThirdPartyCookies, 'never': QWebSettings.AlwaysBlockThirdPartyCookies, 'no-unknown-3rdparty': QWebSettings.AllowThirdPartyWithExistingCookies, } - - def _set(self, value, settings=None): - for obj in self._get_settings(settings): - obj.setThirdPartyCookiePolicy(self.MAPPING[value]) + value = config.val.content.cookies.accept + settings.setThirdPartyCookiePolicy(mapping[value]) -def _set_user_stylesheet(): - """Set the generated user-stylesheet.""" - stylesheet = shared.get_user_stylesheet().encode('utf-8') - url = urlutils.data_url('text/css;charset=utf-8', stylesheet) - QWebSettings.globalSettings().setUserStyleSheetUrl(url) +def _set_cache_maximum_pages(settings): + """Update the content.cache.maximum_pages setting.""" + value = config.val.content.cache.maximum_pages + settings.setMaximumPagesInCache(value) def _update_settings(option): """Update global settings when qwebsettings changed.""" + global_settings.update_setting(option) + + settings = QWebSettings.globalSettings() if option in ['scrollbar.hide', 'content.user_stylesheets']: - _set_user_stylesheet() - websettings.update_mappings(MAPPINGS, option) + _set_user_stylesheet(settings) + elif option == 'content.cookies.accept': + _set_cookie_accept_policy(settings) + elif option == 'content.cache.maximum_pages': + _set_cache_maximum_pages(settings) def init(_args): @@ -131,92 +173,20 @@ def init(_args): QWebSettings.setOfflineStoragePath( os.path.join(data_path, 'offline-storage')) - websettings.init_mappings(MAPPINGS) - _set_user_stylesheet() + settings = QWebSettings.globalSettings() + _set_user_stylesheet(settings) + _set_cookie_accept_policy(settings) + _set_cache_maximum_pages(settings) + config.instance.changed.connect(_update_settings) + global global_settings + global_settings = WebKitSettings(QWebSettings.globalSettings()) + global_settings.init_settings() + def shutdown(): """Disable storage so removing tmpdir will work.""" QWebSettings.setIconDatabasePath('') QWebSettings.setOfflineWebApplicationCachePath('') QWebSettings.globalSettings().setLocalStoragePath('') - - -MAPPINGS = { - 'content.images': - Attribute(QWebSettings.AutoLoadImages), - 'content.javascript.enabled': - Attribute(QWebSettings.JavascriptEnabled), - 'content.javascript.can_open_tabs_automatically': - Attribute(QWebSettings.JavascriptCanOpenWindows), - 'content.javascript.can_close_tabs': - Attribute(QWebSettings.JavascriptCanCloseWindows), - 'content.javascript.can_access_clipboard': - Attribute(QWebSettings.JavascriptCanAccessClipboard), - 'content.plugins': - Attribute(QWebSettings.PluginsEnabled), - 'content.webgl': - Attribute(QWebSettings.WebGLEnabled), - 'content.hyperlink_auditing': - Attribute(QWebSettings.HyperlinkAuditingEnabled), - 'content.local_content_can_access_remote_urls': - Attribute(QWebSettings.LocalContentCanAccessRemoteUrls), - 'content.local_content_can_access_file_urls': - Attribute(QWebSettings.LocalContentCanAccessFileUrls), - 'content.cookies.accept': - CookiePolicy(), - 'content.dns_prefetch': - Attribute(QWebSettings.DnsPrefetchEnabled), - 'content.frame_flattening': - Attribute(QWebSettings.FrameFlatteningEnabled), - 'content.cache.appcache': - Attribute(QWebSettings.OfflineWebApplicationCacheEnabled), - 'content.local_storage': - Attribute(QWebSettings.LocalStorageEnabled, - QWebSettings.OfflineStorageDatabaseEnabled), - 'content.cache.maximum_pages': - StaticSetter(QWebSettings.setMaximumPagesInCache), - 'content.developer_extras': - Attribute(QWebSettings.DeveloperExtrasEnabled), - 'content.print_element_backgrounds': - Attribute(QWebSettings.PrintElementBackgrounds), - 'content.xss_auditing': - Attribute(QWebSettings.XSSAuditingEnabled), - 'content.default_encoding': - Setter(QWebSettings.setDefaultTextEncoding), - # content.user_stylesheets is handled separately - - 'input.spatial_navigation': - Attribute(QWebSettings.SpatialNavigationEnabled), - 'input.links_included_in_focus_chain': - Attribute(QWebSettings.LinksIncludedInFocusChain), - - 'fonts.web.family.standard': - FontFamilySetter(QWebSettings.StandardFont), - 'fonts.web.family.fixed': - FontFamilySetter(QWebSettings.FixedFont), - 'fonts.web.family.serif': - FontFamilySetter(QWebSettings.SerifFont), - 'fonts.web.family.sans_serif': - FontFamilySetter(QWebSettings.SansSerifFont), - 'fonts.web.family.cursive': - FontFamilySetter(QWebSettings.CursiveFont), - 'fonts.web.family.fantasy': - FontFamilySetter(QWebSettings.FantasyFont), - 'fonts.web.size.minimum': - Setter(QWebSettings.setFontSize, args=[QWebSettings.MinimumFontSize]), - 'fonts.web.size.minimum_logical': - Setter(QWebSettings.setFontSize, - args=[QWebSettings.MinimumLogicalFontSize]), - 'fonts.web.size.default': - Setter(QWebSettings.setFontSize, args=[QWebSettings.DefaultFontSize]), - 'fonts.web.size.default_fixed': - Setter(QWebSettings.setFontSize, - args=[QWebSettings.DefaultFixedFontSize]), - - 'zoom.text_only': - Attribute(QWebSettings.ZoomTextOnly), - 'scrolling.smooth': - Attribute(QWebSettings.ScrollAnimatorEnabled), -} diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index 6b8f067fd..fcfbaa538 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016-2017 Florian Bruhin (The Compiler) +# Copyright 2016-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -23,6 +23,10 @@ import re import functools import xml.etree.ElementTree +import pygments +import pygments.lexers +import pygments.formatters + import sip from PyQt5.QtCore import (pyqtSlot, Qt, QEvent, QUrl, QPoint, QTimer, QSizeF, QSize) @@ -31,8 +35,9 @@ from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame from PyQt5.QtWebKit import QWebSettings from PyQt5.QtPrintSupport import QPrinter -from qutebrowser.browser import browsertab -from qutebrowser.browser.webkit import webview, tabhistory, webkitelem +from qutebrowser.browser import browsertab, shared +from qutebrowser.browser.webkit import (webview, tabhistory, webkitelem, + webkitsettings) from qutebrowser.utils import qtutils, objreg, usertypes, utils, log, debug @@ -50,6 +55,29 @@ class WebKitAction(browsertab.AbstractAction): """Save the current page.""" raise browsertab.UnsupportedOperationError + def show_source(self): + + def show_source_cb(source): + """Show source as soon as it's ready.""" + # WORKAROUND for https://github.com/PyCQA/pylint/issues/491 + # pylint: disable=no-member + lexer = pygments.lexers.HtmlLexer() + formatter = pygments.formatters.HtmlFormatter( + full=True, linenos='table') + # pylint: enable=no-member + highlighted = pygments.highlight(source, lexer, formatter) + + tb = objreg.get('tabbed-browser', scope='window', + window=self._tab.win_id) + new_tab = tb.tabopen(background=False, related=True) + # The original URL becomes the path of a view-source: URL + # (without a host), but query/fragment should stay. + url = QUrl('view-source:' + urlstr) + new_tab.set_html(highlighted, url) + + urlstr = self._tab.url().toString(QUrl.RemoveUserInfo) + self._tab.dump_async(show_source_cb) + class WebKitPrinting(browsertab.AbstractPrinting): @@ -161,7 +189,7 @@ class WebKitCaret(browsertab.AbstractCaret): settings = self._widget.settings() settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True) - self.selection_enabled = bool(self.selection()) + self.selection_enabled = self._widget.hasSelection() if self._widget.isVisible(): # Sometimes the caret isn't immediately visible, but unfocusing @@ -174,12 +202,12 @@ class WebKitCaret(browsertab.AbstractCaret): # # Note: We can't use hasSelection() here, as that's always # true in caret mode. - if not self.selection(): + if not self.selection_enabled: self._widget.page().currentFrame().evaluateJavaScript( utils.read_file('javascript/position_caret.js')) - @pyqtSlot() - def _on_mode_left(self): + @pyqtSlot(usertypes.KeyMode) + def _on_mode_left(self, _mode): settings = self._widget.settings() if settings.testAttribute(QWebSettings.CaretBrowsingEnabled): if self.selection_enabled and self._widget.hasSelection(): @@ -327,23 +355,16 @@ class WebKitCaret(browsertab.AbstractCaret): def toggle_selection(self): self.selection_enabled = not self.selection_enabled mainwindow = objreg.get('main-window', scope='window', - window=self._win_id) + window=self._tab.win_id) mainwindow.status.set_mode_active(usertypes.KeyMode.caret, True) def drop_selection(self): self._widget.triggerPageAction(QWebPage.MoveToNextChar) - def has_selection(self): - return self._widget.hasSelection() - - def selection(self, html=False): - if html: - return self._widget.selectedHtml() - return self._widget.selectedText() + def selection(self, callback): + callback(self._widget.selectedText()) def follow_selected(self, *, tab=False): - if not self.has_selection(): - return if QWebSettings.globalSettings().testAttribute( QWebSettings.JavascriptEnabled): if tab: @@ -351,7 +372,9 @@ class WebKitCaret(browsertab.AbstractCaret): self._tab.run_js_async( 'window.getSelection().anchorNode.parentNode.click()') else: - selection = self.selection(html=True) + selection = self._widget.selectedHtml() + if not selection: + return try: selected_element = xml.etree.ElementTree.fromstring( '{}'.format(selection)).find('a') @@ -495,7 +518,8 @@ class WebKitHistory(browsertab.AbstractHistory): return self._history.itemAt(i) def _go_to_item(self, item): - return self._history.goToItem(item) + self._tab.predicted_navigation.emit(item.url()) + self._history.goToItem(item) def serialize(self): return qtutils.serialize(self._history) @@ -615,13 +639,15 @@ class WebKitTab(browsertab.AbstractTab): self._make_private(widget) self.history = WebKitHistory(self) self.scroller = WebKitScroller(self, parent=self) - self.caret = WebKitCaret(win_id=win_id, mode_manager=mode_manager, + self.caret = WebKitCaret(mode_manager=mode_manager, tab=self, parent=self) - self.zoom = WebKitZoom(win_id=win_id, parent=self) + self.zoom = WebKitZoom(tab=self, parent=self) self.search = WebKitSearch(parent=self) self.printing = WebKitPrinting() - self.elements = WebKitElements(self) - self.action = WebKitAction() + self.elements = WebKitElements(tab=self) + self.action = WebKitAction(tab=self) + # We're assigning settings in _set_widget + self.settings = webkitsettings.WebKitSettings(settings=None) self._set_widget(widget) self._connect_signals() self.backend = usertypes.Backend.QtWebKit @@ -704,6 +730,11 @@ class WebKitTab(browsertab.AbstractTab): page = self._widget.page() return page.userAgentForUrl(self.url()) + @pyqtSlot() + def _on_load_started(self): + super()._on_load_started() + self.networkaccessmanager().netrc_used = False + @pyqtSlot() def _on_frame_load_finished(self): """Make sure we emit an appropriate status when loading finished. @@ -734,6 +765,31 @@ class WebKitTab(browsertab.AbstractTab): def _on_contents_size_changed(self, size): self.contents_size_changed.emit(QSizeF(size)) + @pyqtSlot(usertypes.NavigationRequest) + def _on_navigation_request(self, navigation): + super()._on_navigation_request(navigation) + if not navigation.accepted: + return + + log.webview.debug("target {} override {}".format( + self.data.open_target, self.data.override_target)) + + if self.data.override_target is not None: + target = self.data.override_target + self.data.override_target = None + else: + target = self.data.open_target + + if (navigation.navigation_type == navigation.Type.link_clicked and + target != usertypes.ClickTarget.normal): + tab = shared.get_tab(self.win_id, target) + tab.openurl(navigation.url) + self.data.open_target = usertypes.ClickTarget.normal + navigation.accepted = False + + if navigation.is_main_frame: + self.settings.update_for_url(navigation.url) + def _connect_signals(self): view = self._widget page = view.page() @@ -752,6 +808,7 @@ class WebKitTab(browsertab.AbstractTab): page.frameCreated.connect(self._on_frame_created) frame.contentsSizeChanged.connect(self._on_contents_size_changed) frame.initialLayoutCompleted.connect(self._on_history_trigger) + page.navigation_request.connect(self._on_navigation_request) def event_target(self): return self._widget diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py index 89407fcdf..aebf53d87 100644 --- a/qutebrowser/browser/webkit/webpage.py +++ b/qutebrowser/browser/webkit/webpage.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -33,8 +33,7 @@ from qutebrowser.config import config from qutebrowser.browser import pdfjs, shared from qutebrowser.browser.webkit import http from qutebrowser.browser.webkit.network import networkmanager -from qutebrowser.utils import (message, usertypes, log, jinja, objreg, debug, - urlutils) +from qutebrowser.utils import message, usertypes, log, jinja, objreg class BrowserPage(QWebPage): @@ -54,10 +53,12 @@ class BrowserPage(QWebPage): shutting_down: Emitted when the page is currently shutting down. reloading: Emitted before a web page reloads. arg: The URL which gets reloaded. + navigation_request: Emitted on acceptNavigationRequest. """ shutting_down = pyqtSignal() reloading = pyqtSignal(QUrl) + navigation_request = pyqtSignal(usertypes.NavigationRequest) def __init__(self, win_id, tab_id, tabdata, private, parent=None): super().__init__(parent) @@ -70,7 +71,6 @@ class BrowserPage(QWebPage): } self._ignore_load_started = False self.error_occurred = False - self.open_target = usertypes.ClickTarget.normal self._networkmanager = networkmanager.NetworkManager( win_id=win_id, tab_id=tab_id, private=private, parent=self) self.setNetworkAccessManager(self._networkmanager) @@ -147,7 +147,8 @@ class BrowserPage(QWebPage): title="Open external application for {}-link?".format(scheme), text="URL: {}".format( html.escape(url.toDisplayString())), - yes_action=functools.partial(QDesktopServices.openUrl, url)) + yes_action=functools.partial(QDesktopServices.openUrl, url), + url=info.url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)) return True elif (info.domain, info.error) in ignored_errors: log.webview.debug("Ignored error on {}: {} (error domain: {}, " @@ -219,8 +220,7 @@ class BrowserPage(QWebPage): """Prepare the web page for being deleted.""" self._is_shutting_down = True self.shutting_down.emit() - download_manager = objreg.get('qtnetwork-download-manager', - scope='window', window=self._win_id) + download_manager = objreg.get('qtnetwork-download-manager') nam = self.networkAccessManager() if download_manager.has_downloads_with_nam(nam): nam.setParent(download_manager) @@ -248,8 +248,7 @@ class BrowserPage(QWebPage): after this slot returns. """ req = QNetworkRequest(request) - download_manager = objreg.get('qtnetwork-download-manager', - scope='window', window=self._win_id) + download_manager = objreg.get('qtnetwork-download-manager') download_manager.get_request(req, qnam=self.networkAccessManager()) @pyqtSlot('QNetworkReply*') @@ -263,8 +262,7 @@ class BrowserPage(QWebPage): here: http://mimesniff.spec.whatwg.org/ """ inline, suggested_filename = http.parse_content_disposition(reply) - download_manager = objreg.get('qtnetwork-download-manager', - scope='window', window=self._win_id) + download_manager = objreg.get('qtnetwork-download-manager') if not inline: # Content-Disposition: attachment -> force download download_manager.fetch(reply, @@ -476,7 +474,7 @@ class BrowserPage(QWebPage): source, line, msg) def acceptNavigationRequest(self, - _frame: QWebFrame, + frame: QWebFrame, request: QNetworkRequest, typ: QWebPage.NavigationType): """Override acceptNavigationRequest to handle clicked links. @@ -488,36 +486,27 @@ class BrowserPage(QWebPage): Checks if it should open it in a tab (middle-click or control) or not, and then conditionally opens the URL here or in another tab/window. """ - url = request.url() - log.webview.debug("navigation request: url {}, type {}, " - "target {} override {}".format( - url.toDisplayString(), - debug.qenum_key(QWebPage, typ), - self.open_target, - self._tabdata.override_target)) + type_map = { + QWebPage.NavigationTypeLinkClicked: + usertypes.NavigationRequest.Type.link_clicked, + QWebPage.NavigationTypeFormSubmitted: + usertypes.NavigationRequest.Type.form_submitted, + QWebPage.NavigationTypeFormResubmitted: + usertypes.NavigationRequest.Type.form_resubmitted, + QWebPage.NavigationTypeBackOrForward: + usertypes.NavigationRequest.Type.back_forward, + QWebPage.NavigationTypeReload: + usertypes.NavigationRequest.Type.reloaded, + QWebPage.NavigationTypeOther: + usertypes.NavigationRequest.Type.other, + } + is_main_frame = frame is self.mainFrame() + navigation = usertypes.NavigationRequest(url=request.url(), + navigation_type=type_map[typ], + is_main_frame=is_main_frame) - if self._tabdata.override_target is not None: - target = self._tabdata.override_target - self._tabdata.override_target = None - else: - target = self.open_target + if navigation.navigation_type == navigation.Type.reloaded: + self.reloading.emit(navigation.url) - if typ == QWebPage.NavigationTypeReload: - self.reloading.emit(url) - return True - elif typ != QWebPage.NavigationTypeLinkClicked: - return True - - if not url.isValid(): - msg = urlutils.get_errstring(url, "Invalid link clicked") - message.error(msg) - self.open_target = usertypes.ClickTarget.normal - return False - - if target == usertypes.ClickTarget.normal: - return True - - tab = shared.get_tab(self._win_id, target) - tab.openurl(url) - self.open_target = usertypes.ClickTarget.normal - return False + self.navigation_request.emit(navigation) + return navigation.accepted diff --git a/qutebrowser/browser/webkit/webview.py b/qutebrowser/browser/webkit/webview.py index 4f1ff10c8..79da9778c 100644 --- a/qutebrowser/browser/webkit/webview.py +++ b/qutebrowser/browser/webkit/webview.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -262,10 +262,10 @@ class WebView(QWebView): target = usertypes.ClickTarget.tab_bg else: target = usertypes.ClickTarget.tab - self.page().open_target = target + self._tabdata.open_target = target log.mouse.debug("Ctrl/Middle click, setting target: {}".format( target)) else: - self.page().open_target = usertypes.ClickTarget.normal + self._tabdata.open_target = usertypes.ClickTarget.normal log.mouse.debug("Normal click, setting normal target") super().mousePressEvent(e) diff --git a/qutebrowser/commands/__init__.py b/qutebrowser/commands/__init__.py index 7bc59ae40..0bbc9852b 100644 --- a/qutebrowser/commands/__init__.py +++ b/qutebrowser/commands/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/commands/argparser.py b/qutebrowser/commands/argparser.py index 9dfe841ce..707324ede 100644 --- a/qutebrowser/commands/argparser.py +++ b/qutebrowser/commands/argparser.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/commands/cmdexc.py b/qutebrowser/commands/cmdexc.py index bca2f0500..5d3ac2a89 100644 --- a/qutebrowser/commands/cmdexc.py +++ b/qutebrowser/commands/cmdexc.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/commands/cmdutils.py b/qutebrowser/commands/cmdutils.py index 2f7af2f9f..f9ce91b8f 100644 --- a/qutebrowser/commands/cmdutils.py +++ b/qutebrowser/commands/cmdutils.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -105,7 +105,8 @@ class register: # noqa: N801,N806 pylint: disable=invalid-name else: assert isinstance(self._name, str), self._name name = self._name - log.commands.vdebug("Registering command {}".format(name)) + log.commands.vdebug("Registering command {} (from {}:{})".format( + name, func.__module__, func.__qualname__)) if name in cmd_dict: raise ValueError("{} is already registered!".format(name)) cmd = command.Command(name=name, instance=self._instance, diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index b4ff1cde8..8dd830495 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -193,10 +193,10 @@ class Command: return False def _inspect_func(self): - """Inspect the function to get useful informations from it. + """Inspect the function to get useful information from it. Sets instance attributes (desc, type_conv, name_conv) based on the - informations. + information. Return: How many user-visible arguments the command has. @@ -394,11 +394,12 @@ class Command: if isinstance(typ, tuple): raise TypeError("{}: Legacy tuple type annotation!".format( self.name)) - elif type(typ) is type(typing.Union): # noqa: E721 + elif getattr(typ, '__origin__', None) is typing.Union or ( + # Older Python 3.5 patch versions + # pylint: disable=no-member,useless-suppression + hasattr(typing, 'UnionMeta') and + isinstance(typ, typing.UnionMeta)): # this is... slightly evil, I know - # We also can't use isinstance here because typing.Union doesn't - # support that. - # pylint: disable=no-member,useless-suppression try: types = list(typ.__args__) except AttributeError: diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index 890a0275e..c3f5d87a1 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -63,9 +63,13 @@ def replace_variables(win_id, arglist): QUrl.FullyEncoded | QUrl.RemovePassword), 'url:pretty': lambda: _current_url(tabbed_browser).toString( QUrl.DecodeReserved | QUrl.RemovePassword), + 'url:host': lambda: _current_url(tabbed_browser).host(), 'clipboard': utils.get_clipboard, 'primary': lambda: utils.get_clipboard(selection=True), } + for key in list(variables): + modified_key = '{' + key + '}' + variables[modified_key] = lambda x=modified_key: x values = {} args = [] tabbed_browser = objreg.get('tabbed-browser', scope='window', @@ -162,7 +166,7 @@ class CommandParser: yield self.parse(sub, *args, **kwargs) def parse_all(self, *args, **kwargs): - """Wrapper over parse_all.""" + """Wrapper over _parse_all_gen.""" return list(self._parse_all_gen(*args, **kwargs)) def parse(self, text, *, fallback=False, keep=False): diff --git a/qutebrowser/commands/userscripts.py b/qutebrowser/commands/userscripts.py index d86cb8ccf..5654fd809 100644 --- a/qutebrowser/commands/userscripts.py +++ b/qutebrowser/commands/userscripts.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -446,3 +446,4 @@ def run_async(tab, cmd, *args, win_id, env, verbose=False): runner.prepare_run(cmd_path, *args, env=env, verbose=verbose) tab.dump_async(runner.store_html) tab.dump_async(runner.store_text, plain=True) + return runner diff --git a/qutebrowser/completion/__init__.py b/qutebrowser/completion/__init__.py index 8b8b9d88d..2c9121699 100644 --- a/qutebrowser/completion/__init__.py +++ b/qutebrowser/completion/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/completion/completer.py b/qutebrowser/completion/completer.py index 30a180554..09b80ed12 100644 --- a/qutebrowser/completion/completer.py +++ b/qutebrowser/completion/completer.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -87,8 +87,6 @@ class Completer(QObject): # cursor on a flag or after an explicit split (--) return None log.completion.debug("Before removing flags: {}".format(before_cursor)) - before_cursor = [x for x in before_cursor if not x.startswith('-')] - log.completion.debug("After removing flags: {}".format(before_cursor)) if not before_cursor: # '|' or 'set|' log.completion.debug('Starting command completion') @@ -99,6 +97,9 @@ class Completer(QObject): log.completion.debug("No completion for unknown command: {}" .format(before_cursor[0])) return None + + before_cursor = [x for x in before_cursor if not x.startswith('-')] + log.completion.debug("After removing flags: {}".format(before_cursor)) argpos = len(before_cursor) - 1 try: func = cmd.get_pos_arg_info(argpos).completion diff --git a/qutebrowser/completion/completiondelegate.py b/qutebrowser/completion/completiondelegate.py index b4f9c5a33..779906a83 100644 --- a/qutebrowser/completion/completiondelegate.py +++ b/qutebrowser/completion/completiondelegate.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -203,9 +203,9 @@ class CompletionItemDelegate(QStyledItemDelegate): columns_to_filter = index.model().columns_to_filter(index) if index.column() in columns_to_filter and pattern: repl = r'\g<0>' - text = re.sub(re.escape(pattern).replace(r'\ ', r'|'), - repl, html.escape(self._opt.text), - flags=re.IGNORECASE) + pat = html.escape(re.escape(pattern)).replace(r'\ ', r'|') + txt = html.escape(self._opt.text) + text = re.sub(pat, repl, txt, flags=re.IGNORECASE) self._doc.setHtml(text) else: self._doc.setPlainText(self._opt.text) diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py index 29f4f6653..740be75d9 100644 --- a/qutebrowser/completion/completionwidget.py +++ b/qutebrowser/completion/completionwidget.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/completion/models/__init__.py b/qutebrowser/completion/models/__init__.py index 5812545eb..7f62829ba 100644 --- a/qutebrowser/completion/models/__init__.py +++ b/qutebrowser/completion/models/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/completion/models/completionmodel.py b/qutebrowser/completion/models/completionmodel.py index aa4422d83..1c77e1d31 100644 --- a/qutebrowser/completion/models/completionmodel.py +++ b/qutebrowser/completion/models/completionmodel.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2017 Ryan Roden-Corrent (rcorre) +# Copyright 2017-2018 Ryan Roden-Corrent (rcorre) # # This file is part of qutebrowser. # diff --git a/qutebrowser/completion/models/configmodel.py b/qutebrowser/completion/models/configmodel.py index 1a433a460..435eb0643 100644 --- a/qutebrowser/completion/models/configmodel.py +++ b/qutebrowser/completion/models/configmodel.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/completion/models/histcategory.py b/qutebrowser/completion/models/histcategory.py index 57a2aa936..a07f78143 100644 --- a/qutebrowser/completion/models/histcategory.py +++ b/qutebrowser/completion/models/histcategory.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2017 Ryan Roden-Corrent (rcorre) +# Copyright 2017-2018 Ryan Roden-Corrent (rcorre) # # This file is part of qutebrowser. # diff --git a/qutebrowser/completion/models/listcategory.py b/qutebrowser/completion/models/listcategory.py index 657ae97aa..13bc1e6b2 100644 --- a/qutebrowser/completion/models/listcategory.py +++ b/qutebrowser/completion/models/listcategory.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2017 Ryan Roden-Corrent (rcorre) +# Copyright 2017-2018 Ryan Roden-Corrent (rcorre) # # This file is part of qutebrowser. # diff --git a/qutebrowser/completion/models/miscmodels.py b/qutebrowser/completion/models/miscmodels.py index 22c9000c3..049d89295 100644 --- a/qutebrowser/completion/models/miscmodels.py +++ b/qutebrowser/completion/models/miscmodels.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/completion/models/urlmodel.py b/qutebrowser/completion/models/urlmodel.py index 1d7a075eb..bebf6d829 100644 --- a/qutebrowser/completion/models/urlmodel.py +++ b/qutebrowser/completion/models/urlmodel.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/completion/models/util.py b/qutebrowser/completion/models/util.py index 40d5de8dc..c1b8b56f9 100644 --- a/qutebrowser/completion/models/util.py +++ b/qutebrowser/completion/models/util.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2017 Ryan Roden-Corrent (rcorre) +# Copyright 2017-2018 Ryan Roden-Corrent (rcorre) # # This file is part of qutebrowser. # diff --git a/qutebrowser/config/__init__.py b/qutebrowser/config/__init__.py index bf0bce0ec..e2c25cce8 100644 --- a/qutebrowser/config/__init__.py +++ b/qutebrowser/config/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 9ac05c7d6..14022d1d2 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -25,7 +25,7 @@ import functools from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject -from qutebrowser.config import configdata, configexc +from qutebrowser.config import configdata, configexc, configutils from qutebrowser.utils import utils, log, jinja from qutebrowser.misc import objects from qutebrowser.keyinput import keyutils @@ -38,6 +38,9 @@ key_instance = None # Keeping track of all change filters to validate them later. change_filters = [] +# Sentinel +UNSET = object() + class change_filter: # noqa: N801,N806 pylint: disable=invalid-name @@ -186,7 +189,7 @@ class KeyConfig: log.keyboard.vdebug("Adding binding {} -> {} in mode {}.".format( key, command, mode)) - bindings = self._config.get_obj('bindings.commands') + bindings = self._config.get_mutable_obj('bindings.commands') if mode not in bindings: bindings[mode] = {} bindings[mode][str(key)] = command @@ -196,7 +199,7 @@ class KeyConfig: """Restore a default keybinding.""" self._prepare(key, mode) - bindings_commands = self._config.get_obj('bindings.commands') + bindings_commands = self._config.get_mutable_obj('bindings.commands') try: del bindings_commands[mode][str(key)] except KeyError: @@ -208,7 +211,7 @@ class KeyConfig: """Unbind the given key in the given mode.""" self._prepare(key, mode) - bindings_commands = self._config.get_obj('bindings.commands') + bindings_commands = self._config.get_mutable_obj('bindings.commands') if str(key) in bindings_commands.get(mode, {}): # In custom bindings -> remove it @@ -229,8 +232,12 @@ class Config(QObject): """Main config object. + Class attributes: + MUTABLE_TYPES: Types returned from the config which could potentially + be mutated. + Attributes: - _values: A dict mapping setting names to their values. + _values: A dict mapping setting names to configutils.Values objects. _mutables: A dictionary of mutable objects to be checked for changes. _yaml: A YamlConfig object or None. @@ -238,19 +245,25 @@ class Config(QObject): changed: Emitted with the option name when an option changed. """ + MUTABLE_TYPES = (dict, list) changed = pyqtSignal(str) def __init__(self, yaml_config, parent=None): super().__init__(parent) self.changed.connect(_render_stylesheet.cache_clear) - self._values = {} self._mutables = {} self._yaml = yaml_config + self._init_values() + + def _init_values(self): + """Populate the self._values dict.""" + self._values = {} + for name, opt in configdata.DATA.items(): + self._values[name] = configutils.Values(opt) def __iter__(self): - """Iterate over Option, value tuples.""" - for name, value in sorted(self._values.items()): - yield (self.get_opt(name), value) + """Iterate over configutils.Values items.""" + yield from self._values.values() def init_save_manager(self, save_manager): """Make sure the config gets saved properly. @@ -260,14 +273,15 @@ class Config(QObject): """ self._yaml.init_save_manager(save_manager) - def _set_value(self, opt, value): + def _set_value(self, opt, value, pattern=None): """Set the given option to the given value.""" if not isinstance(objects.backend, objects.NoBackend): if objects.backend not in opt.backends: raise configexc.BackendError(opt.name, objects.backend) opt.typ.to_py(value) # for validation - self._values[opt.name] = opt.typ.from_obj(value) + + self._values[opt.name].add(opt.typ.from_obj(value), pattern) self.changed.emit(opt.name) log.config.debug("Config option changed: {} = {}".format( @@ -276,8 +290,10 @@ class Config(QObject): def read_yaml(self): """Read the YAML settings from self._yaml.""" self._yaml.load() - for name, value in self._yaml: - self._set_value(self.get_opt(name), value) + for values in self._yaml: + for scoped in values: + self._set_value(values.opt, scoped.value, + pattern=scoped.pattern) def get_opt(self, name): """Get a configdata.Option object for the given setting.""" @@ -290,53 +306,89 @@ class Config(QObject): name, deleted=deleted, renamed=renamed) raise exception from None - def get(self, name): + def get(self, name, url=None): """Get the given setting converted for Python code.""" opt = self.get_opt(name) - obj = self.get_obj(name, mutable=False) + obj = self.get_obj(name, url=url) return opt.typ.to_py(obj) - def get_obj(self, name, *, mutable=True): + def _maybe_copy(self, value): + """Copy the value if it could potentially be mutated.""" + if isinstance(value, self.MUTABLE_TYPES): + # For mutable objects, create a copy so we don't accidentally + # mutate the config's internal value. + return copy.deepcopy(value) + else: + # Shouldn't be mutable (and thus hashable) + assert value.__hash__ is not None, value + return value + + def get_obj(self, name, *, url=None): """Get the given setting as object (for YAML/config.py). - If mutable=True is set, watch the returned object for mutations. + Note that the returned values are not watched for mutation. + If a URL is given, return the value which should be used for that URL. """ - opt = self.get_opt(name) - obj = None + self.get_opt(name) # To make sure it exists + value = self._values[name].get_for_url(url) + return self._maybe_copy(value) + + def get_obj_for_pattern(self, name, *, pattern): + """Get the given setting as object (for YAML/config.py). + + This gets the overridden value for a given pattern, or + configutils.UNSET if no such override exists. + """ + self.get_opt(name) # To make sure it exists + value = self._values[name].get_for_pattern(pattern, fallback=False) + return self._maybe_copy(value) + + def get_mutable_obj(self, name, *, pattern=None): + """Get an object which can be mutated, e.g. in a config.py. + + If a pattern is given, return the value for that pattern. + Note that it's impossible to get a mutable object for an URL as we + wouldn't know what pattern to apply. + """ + self.get_opt(name) # To make sure it exists + # If we allow mutation, there is a chance that prior mutations already # entered the mutable dictionary and thus further copies are unneeded # until update_mutables() is called - if name in self._mutables and mutable: + if name in self._mutables: _copy, obj = self._mutables[name] - # Otherwise, we return a copy of the value stored internally, so the - # internal value can never be changed by mutating the object returned. - else: - obj = copy.deepcopy(self._values.get(name, opt.default)) - # Then we watch the returned object for changes. - if isinstance(obj, (dict, list)): - if mutable: - self._mutables[name] = (copy.deepcopy(obj), obj) - else: - # Shouldn't be mutable (and thus hashable) - assert obj.__hash__ is not None, obj - return obj + return obj - def get_str(self, name): - """Get the given setting as string.""" + value = self._values[name].get_for_pattern(pattern) + copy_value = self._maybe_copy(value) + + # Watch the returned object for changes if it's mutable. + if isinstance(copy_value, self.MUTABLE_TYPES): + self._mutables[name] = (value, copy_value) # old, new + + return copy_value + + def get_str(self, name, *, pattern=None): + """Get the given setting as string. + + If a pattern is given, get the setting for the given pattern or + configutils.UNSET. + """ opt = self.get_opt(name) - value = self._values.get(name, opt.default) + values = self._values[name] + value = values.get_for_pattern(pattern) return opt.typ.to_str(value) - def set_obj(self, name, value, *, save_yaml=False): + def set_obj(self, name, value, *, pattern=None, save_yaml=False): """Set the given setting from a YAML/config.py object. If save_yaml=True is given, store the new value to YAML. """ - self._set_value(self.get_opt(name), value) + self._set_value(self.get_opt(name), value, pattern=pattern) if save_yaml: - self._yaml[name] = value + self._yaml.set_obj(name, value, pattern=pattern) - def set_str(self, name, value, *, save_yaml=False): + def set_str(self, name, value, *, pattern=None, save_yaml=False): """Set the given setting from a string. If save_yaml=True is given, store the new value to YAML. @@ -346,21 +398,19 @@ class Config(QObject): log.config.debug("Setting {} (type {}) to {!r} (converted from {!r})" .format(name, opt.typ.__class__.__name__, converted, value)) - self._set_value(opt, converted) + self._set_value(opt, converted, pattern=pattern) if save_yaml: - self._yaml[name] = converted + self._yaml.set_obj(name, converted, pattern=pattern) - def unset(self, name, *, save_yaml=False): + def unset(self, name, *, save_yaml=False, pattern=None): """Set the given setting back to its default.""" - self.get_opt(name) - try: - del self._values[name] - except KeyError: - return - self.changed.emit(name) + self.get_opt(name) # To check whether it exists + changed = self._values[name].remove(pattern) + if changed: + self.changed.emit(name) if save_yaml: - self._yaml.unset(name) + self._yaml.unset(name, pattern=pattern) def clear(self, *, save_yaml=False): """Clear all settings in the config. @@ -368,10 +418,10 @@ class Config(QObject): If save_yaml=True is given, also remove all customization from the YAML file. """ - old_values = self._values - self._values = {} - for name in old_values: - self.changed.emit(name) + for name, values in self._values.items(): + if values: + values.clear() + self.changed.emit(name) if save_yaml: self._yaml.clear() @@ -397,13 +447,15 @@ class Config(QObject): Return: The changed config part as string. """ - lines = [] - for opt, value in self: - str_value = opt.typ.to_str(value) - lines.append('{} = {}'.format(opt.name, str_value)) - if not lines: - lines = [''] - return '\n'.join(lines) + blocks = [] + for values in sorted(self, key=lambda v: v.opt.name): + if values: + blocks.append(str(values)) + + if not blocks: + return '' + + return '\n'.join(blocks) class ConfigContainer: @@ -415,16 +467,21 @@ class ConfigContainer: _prefix: The __getattr__ chain leading up to this object. _configapi: If given, get values suitable for config.py and add errors to the given ConfigAPI object. + _pattern: The URL pattern to be used. """ - def __init__(self, config, configapi=None, prefix=''): + def __init__(self, config, configapi=None, prefix='', pattern=None): self._config = config self._prefix = prefix self._configapi = configapi + self._pattern = pattern + if configapi is None and pattern is not None: + raise TypeError("Can't use pattern without configapi!") def __repr__(self): return utils.get_repr(self, constructor=True, config=self._config, - configapi=self._configapi, prefix=self._prefix) + configapi=self._configapi, prefix=self._prefix, + pattern=self._pattern) @contextlib.contextmanager def _handle_error(self, action, name): @@ -452,7 +509,7 @@ class ConfigContainer: if configdata.is_valid_prefix(name): return ConfigContainer(config=self._config, configapi=self._configapi, - prefix=name) + prefix=name, pattern=self._pattern) with self._handle_error('getting', name): if self._configapi is None: @@ -460,7 +517,8 @@ class ConfigContainer: return self._config.get(name) else: # access from config.py - return self._config.get_obj(name) + return self._config.get_mutable_obj( + name, pattern=self._pattern) def __setattr__(self, attr, value): """Set the given option in the config.""" @@ -470,7 +528,7 @@ class ConfigContainer: name = self._join(attr) with self._handle_error('setting', name): - self._config.set_obj(name, value) + self._config.set_obj(name, value, pattern=self._pattern) def _join(self, attr): """Get the prefix joined with the given attribute.""" diff --git a/qutebrowser/config/configcommands.py b/qutebrowser/config/configcommands.py index d171d164f..7322b3878 100644 --- a/qutebrowser/config/configcommands.py +++ b/qutebrowser/config/configcommands.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -26,7 +26,7 @@ from PyQt5.QtCore import QUrl from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.completion.models import configmodel -from qutebrowser.utils import objreg, utils, message, standarddir +from qutebrowser.utils import objreg, utils, message, standarddir, urlmatch from qutebrowser.config import configtypes, configexc, configfiles, configdata from qutebrowser.misc import editor from qutebrowser.keyinput import keyutils @@ -48,25 +48,46 @@ class ConfigCommands: except configexc.Error as e: raise cmdexc.CommandError(str(e)) - def _print_value(self, option): + def _parse_pattern(self, pattern): + """Parse a pattern string argument to a pattern.""" + if pattern is None: + return None + + try: + return urlmatch.UrlPattern(pattern) + except urlmatch.ParseError as e: + raise cmdexc.CommandError("Error while parsing {}: {}" + .format(pattern, str(e))) + + def _print_value(self, option, pattern): """Print the value of the given option.""" with self._handle_config_error(): - value = self._config.get_str(option) - message.info("{} = {}".format(option, value)) + value = self._config.get_str(option, pattern=pattern) + + text = "{} = {}".format(option, value) + if pattern is not None: + text += " for {}".format(pattern) + message.info(text) @cmdutils.register(instance='config-commands') @cmdutils.argument('option', completion=configmodel.option) @cmdutils.argument('value', completion=configmodel.value) @cmdutils.argument('win_id', win_id=True) - def set(self, win_id, option=None, value=None, temp=False, print_=False): + @cmdutils.argument('pattern', flag='u') + def set(self, win_id, option=None, value=None, temp=False, print_=False, + *, pattern=None): """Set an option. If the option name ends with '?', the value of the option is shown instead. + Using :set without any arguments opens a page where settings can be + changed interactively. + Args: option: The name of the option. value: The value to set. + pattern: The URL pattern to use. temp: Set value temporarily until qutebrowser is closed. print_: Print the value after setting. """ @@ -80,8 +101,10 @@ class ConfigCommands: raise cmdexc.CommandError("Toggling values was moved to the " ":config-cycle command") + pattern = self._parse_pattern(pattern) + if option.endswith('?') and option != '?': - self._print_value(option[:-1]) + self._print_value(option[:-1], pattern=pattern) return with self._handle_config_error(): @@ -89,27 +112,39 @@ class ConfigCommands: raise cmdexc.CommandError("set: The following arguments " "are required: value") else: - self._config.set_str(option, value, save_yaml=not temp) + self._config.set_str(option, value, pattern=pattern, + save_yaml=not temp) if print_: - self._print_value(option) + self._print_value(option, pattern=pattern) @cmdutils.register(instance='config-commands', maxsplit=1, no_cmd_split=True, no_replace_variables=True) @cmdutils.argument('command', completion=configmodel.bind) - def bind(self, key, command=None, *, mode='normal', default=False): + @cmdutils.argument('win_id', win_id=True) + def bind(self, win_id, key=None, command=None, *, mode='normal', + default=False): """Bind a key to a command. + If no command is given, show the current binding for the given key. + Using :bind without any arguments opens a page showing all keybindings. + Args: key: The keychain or special key (inside `<...>`) to bind. - command: The command to execute, with optional args, or None to - print the current binding. + command: The command to execute, with optional args. mode: A comma-separated list of modes to bind the key in (default: `normal`). See `:help bindings.commands` for the available modes. default: If given, restore a default binding. """ + if key is None: + tabbed_browser = objreg.get('tabbed-browser', scope='window', + window=win_id) + tabbed_browser.openurl(QUrl('qute://bindings'), newtab=True) + return + seq = keyutils.KeySequence.parse(key) + if command is None: if default: # :bind --default: Restore default @@ -151,18 +186,24 @@ class ConfigCommands: @cmdutils.register(instance='config-commands', star_args_optional=True) @cmdutils.argument('option', completion=configmodel.option) @cmdutils.argument('values', completion=configmodel.value) - def config_cycle(self, option, *values, temp=False, print_=False): + @cmdutils.argument('pattern', flag='u') + def config_cycle(self, option, *values, pattern=None, temp=False, + print_=False): """Cycle an option between multiple values. Args: option: The name of the option. values: The values to cycle through. + pattern: The URL pattern to use. temp: Set value temporarily until qutebrowser is closed. print_: Print the value after setting. """ + pattern = self._parse_pattern(pattern) + with self._handle_config_error(): opt = self._config.get_opt(option) - old_value = self._config.get_obj(option, mutable=False) + old_value = self._config.get_obj_for_pattern(option, + pattern=pattern) if not values and isinstance(opt.typ, configtypes.Bool): values = ['true', 'false'] @@ -184,10 +225,11 @@ class ConfigCommands: value = values[0] with self._handle_config_error(): - self._config.set_obj(option, value, save_yaml=not temp) + self._config.set_obj(option, value, pattern=pattern, + save_yaml=not temp) if print_: - self._print_value(option) + self._print_value(option, pattern=pattern) @cmdutils.register(instance='config-commands') @cmdutils.argument('option', completion=configmodel.customized_option) @@ -243,7 +285,7 @@ class ConfigCommands: Args: no_source: Don't re-source the config file after editing. """ - def on_editing_finished(): + def on_file_updated(): """Source the new config when editing finished. This can't use cmdexc.CommandError as it's run async. @@ -253,9 +295,9 @@ class ConfigCommands: except configexc.ConfigFileErrors as e: message.error(str(e)) - ed = editor.ExternalEditor(self._config) + ed = editor.ExternalEditor(watch=True, parent=self._config) if not no_source: - ed.editing_finished.connect(on_editing_finished) + ed.file_updated.connect(on_file_updated) filename = os.path.join(standarddir.config(), 'config.py') ed.edit_file(filename) @@ -281,13 +323,16 @@ class ConfigCommands: "overwrite!".format(filename)) if defaults: - options = [(opt, opt.default) + options = [(None, opt, opt.default) for _name, opt in sorted(configdata.DATA.items())] bindings = dict(configdata.DATA['bindings.default'].default) commented = True else: - options = list(self._config) - bindings = dict(self._config.get_obj('bindings.commands')) + options = [] + for values in self._config: + for scoped in values: + options.append((scoped.pattern, values.opt, scoped.value)) + bindings = dict(self._config.get_mutable_obj('bindings.commands')) commented = False writer = configfiles.ConfigPyWriter(options, bindings, diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index b265ab8fc..52ad123e1 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -48,13 +48,19 @@ class Option: backends = attr.ib() raw_backends = attr.ib() description = attr.ib() + supports_pattern = attr.ib(default=False) restart = attr.ib(default=False) @attr.s class Migrations: - """Nigrated options in configdata.yml.""" + """Nigrated options in configdata.yml. + + Attributes: + renamed: A dict mapping old option names to new names. + deleted: A list of option names which have been removed. + """ renamed = attr.ib(default=attr.Factory(dict)) deleted = attr.ib(default=attr.Factory(list)) @@ -192,7 +198,8 @@ def _read_yaml(yaml_data): migrations = Migrations() data = utils.yaml_load(yaml_data) - keys = {'type', 'default', 'desc', 'backend', 'restart'} + keys = {'type', 'default', 'desc', 'backend', 'restart', + 'supports_pattern'} for name, option in data.items(): if set(option.keys()) == {'renamed'}: @@ -218,7 +225,9 @@ def _read_yaml(yaml_data): backends=_parse_yaml_backends(name, backends), raw_backends=backends if isinstance(backends, dict) else None, description=option['desc'], - restart=option.get('restart', False)) + restart=option.get('restart', False), + supports_pattern=option.get('supports_pattern', False), + ) # Make sure no key shadows another. for key1 in parsed: diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index a118a8b59..d23682db8 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -240,6 +240,7 @@ content.cache.appcache: default: true type: Bool backend: QtWebKit + supports_pattern: true desc: >- Enable support for the HTML 5 web application cache feature. @@ -298,12 +299,14 @@ content.dns_prefetch: default: true type: Bool backend: QtWebKit + supports_pattern: true desc: Try to pre-fetch DNS entries to speed up browsing. content.frame_flattening: default: false type: Bool backend: QtWebKit + supports_pattern: true desc: >- Expand each subframe to its contents. @@ -360,7 +363,7 @@ content.headers.referer: When to send the Referer header. The Referer header tells websites from which website you were coming from - when visting them. + when visiting them. content.headers.user_agent: default: null @@ -459,12 +462,14 @@ content.host_blocking.whitelist: content.hyperlink_auditing: default: false type: Bool + supports_pattern: true desc: Enable hyperlink auditing (`
`). content.images: default: true type: Bool desc: Load images automatically in web pages. + supports_pattern: true content.javascript.alert: default: true @@ -474,6 +479,7 @@ content.javascript.alert: content.javascript.can_access_clipboard: default: false type: Bool + supports_pattern: true desc: >- Allow JavaScript to read from or write to the clipboard. @@ -484,16 +490,19 @@ content.javascript.can_close_tabs: default: false type: Bool backend: QtWebKit + supports_pattern: true desc: Allow JavaScript to close tabs. content.javascript.can_open_tabs_automatically: default: false type: Bool + supports_pattern: true desc: Allow JavaScript to open new tabs without user interaction. content.javascript.enabled: default: true type: Bool + supports_pattern: true desc: Enable JavaScript. content.javascript.log: @@ -536,16 +545,19 @@ content.javascript.prompt: content.local_content_can_access_remote_urls: default: false type: Bool + supports_pattern: true desc: Allow locally loaded documents to access remote URLs. content.local_content_can_access_file_urls: default: true type: Bool + supports_pattern: true desc: Allow locally loaded documents to access other local URLs. content.local_storage: default: true type: Bool + supports_pattern: true desc: Enable support for HTML 5 local storage and Web SQL. content.media_capture: @@ -583,6 +595,7 @@ content.pdfjs: content.plugins: default: false type: Bool + supports_pattern: true desc: Enable plugins in Web pages. content.print_element_backgrounds: @@ -591,6 +604,7 @@ content.print_element_backgrounds: backend: QtWebKit: true QtWebEngine: Qt 5.8 + supports_pattern: true desc: >- Draw the background color and images also when the page is printed. @@ -631,11 +645,13 @@ content.user_stylesheets: content.webgl: default: true type: Bool + supports_pattern: true desc: Enable WebGL. content.xss_auditing: type: Bool default: false + supports_pattern: true desc: >- Monitor load requests for cross-site scripting attempts. @@ -978,6 +994,7 @@ input.insert_mode.plugins: input.links_included_in_focus_chain: default: true type: Bool + supports_pattern: true desc: Include hyperlinks in the keyboard focus chain when tabbing. input.partial_timeout: @@ -1003,6 +1020,7 @@ input.rocker_gestures: input.spatial_navigation: default: false type: Bool + supports_pattern: true desc: >- Enable spatial navigation. @@ -1083,6 +1101,7 @@ scrolling.bar: scrolling.smooth: type: Bool default: false + supports_pattern: true desc: >- Enable smooth scrolling for web pages. @@ -1171,6 +1190,23 @@ statusbar.position: default: bottom desc: Position of the status bar. +statusbar.widgets: + type: + name: List + valtype: + name: String + valid_values: + - url: "Current page URL." + - scroll: "Percentage of the current page position like `10%`." + - scroll_raw: "Raw percentage of the current page position like `10`." + - history: "Display an arrow when possible to go back/forward in history." + - tabs: "Current active tab, e.g. `2`." + - keypress: "Display pressed keys when composing a vi command." + - progress: "Progress bar for the current page loading." + none_ok: true + default: ['keypress', 'url', 'scroll', 'history', 'tabs', 'progress'] + desc: List of widgets displayed in the statusbar. + ## tabs tabs.background: @@ -1251,10 +1287,15 @@ tabs.padding: type: Padding desc: Padding (in pixels) around text for tabs. -tabs.persist_mode_on_change: - default: false - type: Bool - desc: Stay in insert/passthrough mode when switching tabs. +tabs.mode_on_change: + default: normal + type: + name: String + valid_values: + - persist: "Retain the current mode." + - restore: "Restore previously saved mode." + - normal: "Always revert to normal mode." + desc: When switching tabs, what input mode is applied. tabs.position: default: top @@ -1418,7 +1459,7 @@ url.default_page: url.incdec_segments: type: name: FlagList - valid_values: [host, path, query, anchor] + valid_values: [host, port, path, query, anchor] default: [path, query] desc: URL segments where `:navigate increment/decrement` will search for a number. @@ -1535,6 +1576,7 @@ zoom.text_only: type: Bool default: false backend: QtWebKit + supports_pattern: true desc: Apply the zoom factor on a frame only to the text or to all content. ## colors @@ -2287,6 +2329,12 @@ bindings.default: : tab-pin q: record-macro "@": run-macro + tsh: config-cycle -p -t -u *://{url:host}/* content.javascript.enabled ;; reload + tSh: config-cycle -p -u *://{url:host}/* content.javascript.enabled ;; reload + tsH: config-cycle -p -t -u *://*.{url:host}/* content.javascript.enabled ;; reload + tSH: config-cycle -p -u *://*.{url:host}/* content.javascript.enabled ;; reload + tsu: config-cycle -p -t -u {url} content.javascript.enabled ;; reload + tSu: config-cycle -p -u {url} content.javascript.enabled ;; reload insert: : open-editor : insert-text {primary} @@ -2338,6 +2386,8 @@ bindings.default: : prompt-item-focus prev : prompt-item-focus next : prompt-item-focus next + : prompt-yank + : prompt-yank --sel : rl-backward-char : rl-forward-char : rl-backward-word @@ -2349,9 +2399,9 @@ bindings.default: : rl-kill-word : rl-unix-word-rubout : rl-backward-kill-word - : rl-yank : rl-delete-char : rl-backward-delete-char + : rl-yank : leave-mode caret: v: toggle-selection diff --git a/qutebrowser/config/configdiff.py b/qutebrowser/config/configdiff.py index 020be2c8c..9f8b70a26 100644 --- a/qutebrowser/config/configdiff.py +++ b/qutebrowser/config/configdiff.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2017 Florian Bruhin (The Compiler) +# Copyright 2017-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/config/configexc.py b/qutebrowser/config/configexc.py index 28b269dd5..e08bec913 100644 --- a/qutebrowser/config/configexc.py +++ b/qutebrowser/config/configexc.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -40,6 +40,15 @@ class BackendError(Error): "backend!".format(name, backend.name)) +class NoPatternError(Error): + + """Raised when the given setting does not support URL patterns.""" + + def __init__(self, name): + super().__init__("The {} setting does not support URL patterns!" + .format(name)) + + class ValidationError(Error): """Raised when a value for a config type was invalid. @@ -92,6 +101,10 @@ class ConfigErrorDesc: traceback = attr.ib(None) def __str__(self): + if self.traceback: + return '{} - {}: {}'.format(self.text, + self.exception.__class__.__name__, + self.exception) return '{}: {}'.format(self.text, self.exception) def with_text(self, text): diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py index 0e9572f55..ba43e5015 100644 --- a/qutebrowser/config/configfiles.py +++ b/qutebrowser/config/configfiles.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -32,8 +32,8 @@ import yaml from PyQt5.QtCore import pyqtSignal, QObject, QSettings import qutebrowser -from qutebrowser.config import configexc, config, configdata -from qutebrowser.utils import standarddir, utils, qtutils, log +from qutebrowser.config import configexc, config, configdata, configutils +from qutebrowser.utils import standarddir, utils, qtutils, log, urlmatch # The StateConfig instance @@ -80,16 +80,19 @@ class YamlConfig(QObject): VERSION: The current version number of the config file. """ - VERSION = 1 + VERSION = 2 changed = pyqtSignal() def __init__(self, parent=None): super().__init__(parent) self._filename = os.path.join(standarddir.config(auto=True), 'autoconfig.yml') - self._values = {} self._dirty = None + self._values = {} + for name, opt in configdata.DATA.items(): + self._values[name] = configutils.Values(opt) + def init_save_manager(self, save_manager): """Make sure the config gets saved properly. @@ -98,18 +101,9 @@ class YamlConfig(QObject): """ save_manager.add_saveable('yaml-config', self._save, self.changed) - def __getitem__(self, name): - return self._values[name] - - def __setitem__(self, name, value): - self._values[name] = value - self._mark_changed() - - def __contains__(self, name): - return name in self._values - def __iter__(self): - return iter(sorted(self._values.items())) + """Iterate over configutils.Values items.""" + yield from self._values.values() def _mark_changed(self): """Mark the YAML config as changed.""" @@ -121,7 +115,17 @@ class YamlConfig(QObject): if not self._dirty: return - data = {'config_version': self.VERSION, 'global': self._values} + settings = {} + for name, values in sorted(self._values.items()): + if not values: + continue + settings[name] = {} + for scoped in values: + key = ('global' if scoped.pattern is None + else str(scoped.pattern)) + settings[name][key] = scoped.value + + data = {'config_version': self.VERSION, 'settings': settings} with qtutils.savefile_open(self._filename) as f: f.write(textwrap.dedent(""" # DO NOT edit this file by hand, qutebrowser will overwrite it. @@ -130,6 +134,29 @@ class YamlConfig(QObject): """.lstrip('\n'))) utils.yaml_dump(data, f) + def _pop_object(self, yaml_data, key, typ): + """Get a global object from the given data.""" + if not isinstance(yaml_data, dict): + desc = configexc.ConfigErrorDesc("While loading data", + "Toplevel object is not a dict") + raise configexc.ConfigFileErrors('autoconfig.yml', [desc]) + + if key not in yaml_data: + desc = configexc.ConfigErrorDesc( + "While loading data", + "Toplevel object does not contain '{}' key".format(key)) + raise configexc.ConfigFileErrors('autoconfig.yml', [desc]) + + data = yaml_data.pop(key) + + if not isinstance(data, typ): + desc = configexc.ConfigErrorDesc( + "While loading data", + "'{}' object is not a {}".format(key, typ.__name__)) + raise configexc.ConfigFileErrors('autoconfig.yml', [desc]) + + return data + def load(self): """Load configuration from the configured YAML file.""" try: @@ -144,59 +171,126 @@ class YamlConfig(QObject): desc = configexc.ConfigErrorDesc("While parsing", e) raise configexc.ConfigFileErrors('autoconfig.yml', [desc]) - try: - global_obj = yaml_data['global'] - except KeyError: + config_version = self._pop_object(yaml_data, 'config_version', int) + if config_version == 1: + settings = self._load_legacy_settings_object(yaml_data) + self._mark_changed() + elif config_version > self.VERSION: desc = configexc.ConfigErrorDesc( - "While loading data", - "Toplevel object does not contain 'global' key") - raise configexc.ConfigFileErrors('autoconfig.yml', [desc]) - except TypeError: - desc = configexc.ConfigErrorDesc("While loading data", - "Toplevel object is not a dict") + "While reading", + "Can't read config from incompatible newer version") raise configexc.ConfigFileErrors('autoconfig.yml', [desc]) + else: + settings = self._load_settings_object(yaml_data) + self._dirty = False - if not isinstance(global_obj, dict): - desc = configexc.ConfigErrorDesc( - "While loading data", - "'global' object is not a dict") - raise configexc.ConfigFileErrors('autoconfig.yml', [desc]) + settings = self._handle_migrations(settings) + self._validate(settings) + self._build_values(settings) - self._values = global_obj - self._dirty = False + def _load_settings_object(self, yaml_data): + """Load the settings from the settings: key.""" + return self._pop_object(yaml_data, 'settings', dict) - self._handle_migrations() + def _load_legacy_settings_object(self, yaml_data): + data = self._pop_object(yaml_data, 'global', dict) + settings = {} + for name, value in data.items(): + settings[name] = {'global': value} + return settings - def _handle_migrations(self): - """Handle unknown/renamed keys.""" - for name in list(self._values): + def _build_values(self, settings): + """Build up self._values from the values in the given dict.""" + errors = [] + for name, yaml_values in settings.items(): + if not isinstance(yaml_values, dict): + errors.append(configexc.ConfigErrorDesc( + "While parsing {!r}".format(name), "value is not a dict")) + continue + + values = configutils.Values(configdata.DATA[name]) + if 'global' in yaml_values: + values.add(yaml_values.pop('global')) + + for pattern, value in yaml_values.items(): + if not isinstance(pattern, str): + errors.append(configexc.ConfigErrorDesc( + "While parsing {!r}".format(name), + "pattern is not of type string")) + continue + try: + urlpattern = urlmatch.UrlPattern(pattern) + except urlmatch.ParseError as e: + errors.append(configexc.ConfigErrorDesc( + "While parsing pattern {!r} for {!r}" + .format(pattern, name), e)) + continue + values.add(value, urlpattern) + + self._values[name] = values + + if errors: + raise configexc.ConfigFileErrors('autoconfig.yml', errors) + + def _handle_migrations(self, settings): + """Migrate older configs to the newest format.""" + # Simple renamed/deleted options + for name in list(settings): if name in configdata.MIGRATIONS.renamed: new_name = configdata.MIGRATIONS.renamed[name] log.config.debug("Renaming {} to {}".format(name, new_name)) - self._values[new_name] = self._values[name] - del self._values[name] + settings[new_name] = settings[name] + del settings[name] + self._mark_changed() elif name in configdata.MIGRATIONS.deleted: log.config.debug("Removing {}".format(name)) - del self._values[name] - elif name in configdata.DATA: - pass - else: - desc = configexc.ConfigErrorDesc( - "While loading options", - "Unknown option {}".format(name)) - raise configexc.ConfigFileErrors('autoconfig.yml', [desc]) + del settings[name] + self._mark_changed() - def unset(self, name): - """Remove the given option name if it's configured.""" - try: - del self._values[name] - except KeyError: - return + # tabs.persist_mode_on_change got merged into tabs.mode_on_change + old = 'tabs.persist_mode_on_change' + new = 'tabs.mode_on_change' + if old in settings: + settings[new] = {} + for scope, val in settings[old].items(): + if val: + settings[new][scope] = 'persist' + else: + settings[new][scope] = 'normal' + + del settings[old] + self._mark_changed() + + return settings + + def _validate(self, settings): + """Make sure all settings exist.""" + unknown = [] + for name in settings: + if name not in configdata.DATA: + unknown.append(name) + + if unknown: + errors = [configexc.ConfigErrorDesc("While loading options", + "Unknown option {}".format(e)) + for e in sorted(unknown)] + raise configexc.ConfigFileErrors('autoconfig.yml', errors) + + def set_obj(self, name, value, *, pattern=None): + """Set the given setting to the given value.""" + self._values[name].add(value, pattern) self._mark_changed() + def unset(self, name, *, pattern=None): + """Remove the given option name if it's configured.""" + changed = self._values[name].remove(pattern) + if changed: + self._mark_changed() + def clear(self): """Clear all values from the YAML file.""" - self._values = [] + for values in self._values.values(): + values.clear() self._mark_changed() @@ -225,6 +319,7 @@ class ConfigAPI: @contextlib.contextmanager def _handle_error(self, action, name): + """Catch config-related exceptions and save them in self.errors.""" try: yield except configexc.ConfigFileErrors as e: @@ -234,28 +329,38 @@ class ConfigAPI: except configexc.Error as e: text = "While {} '{}'".format(action, name) self.errors.append(configexc.ConfigErrorDesc(text, e)) + except urlmatch.ParseError as e: + text = "While {} '{}' and parsing pattern".format(action, name) + self.errors.append(configexc.ConfigErrorDesc(text, e)) def finalize(self): """Do work which needs to be done after reading config.py.""" self._config.update_mutables() def load_autoconfig(self): + """Load the autoconfig.yml file which is used for :set/:bind/etc.""" with self._handle_error('reading', 'autoconfig.yml'): read_autoconfig() - def get(self, name): + def get(self, name, pattern=None): + """Get a setting value from the config, optionally with a pattern.""" with self._handle_error('getting', name): - return self._config.get_obj(name) + urlpattern = urlmatch.UrlPattern(pattern) if pattern else None + return self._config.get_mutable_obj(name, pattern=urlpattern) - def set(self, name, value): + def set(self, name, value, pattern=None): + """Set a setting value in the config, optionally with a pattern.""" with self._handle_error('setting', name): - self._config.set_obj(name, value) + urlpattern = urlmatch.UrlPattern(pattern) if pattern else None + self._config.set_obj(name, value, pattern=urlpattern) def bind(self, key, command, mode='normal'): + """Bind a key to a command, with an optional key mode.""" with self._handle_error('binding', key): self._keyconfig.bind(key, command, mode=mode) def unbind(self, key, mode='normal'): + """Unbind a key from a command, with an optional key mode.""" with self._handle_error('unbinding', key): self._keyconfig.unbind(key, mode=mode) @@ -269,6 +374,16 @@ class ConfigAPI: except configexc.ConfigFileErrors as e: self.errors += e.errors + @contextlib.contextmanager + def pattern(self, pattern): + """Get a ConfigContainer for the given pattern.""" + # We need to propagate the exception so we don't need to return + # something. + urlpattern = urlmatch.UrlPattern(pattern) + container = config.ConfigContainer(config=self._config, configapi=self, + pattern=urlpattern) + yield container + class ConfigPyWriter: @@ -327,7 +442,7 @@ class ConfigPyWriter: def _gen_options(self): """Generate the options part of the config.""" - for opt, value in self._options: + for pattern, opt, value in self._options: if opt.name in ['bindings.commands', 'bindings.default']: continue @@ -346,7 +461,11 @@ class ConfigPyWriter: except KeyError: yield self._line("# - {}".format(val)) - yield self._line('c.{} = {!r}'.format(opt.name, value)) + if pattern is None: + yield self._line('c.{} = {!r}'.format(opt.name, value)) + else: + yield self._line('config.set({!r}, {!r}, {!r})'.format( + opt.name, value, str(pattern))) yield '' def _gen_bindings(self): @@ -402,7 +521,7 @@ def read_config_py(filename, raising=False): desc = configexc.ConfigErrorDesc("Error while compiling", e) raise configexc.ConfigFileErrors(basename, [desc]) except SyntaxError as e: - desc = configexc.ConfigErrorDesc("Syntax Error", e, + desc = configexc.ConfigErrorDesc("Unhandled exception", e, traceback=traceback.format_exc()) raise configexc.ConfigFileErrors(basename, [desc]) diff --git a/qutebrowser/config/configinit.py b/qutebrowser/config/configinit.py index 510245e2e..c7c9362b0 100644 --- a/qutebrowser/config/configinit.py +++ b/qutebrowser/config/configinit.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2017 Florian Bruhin (The Compiler) +# Copyright 2017-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 218d31193..196e19647 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/config/configutils.py b/qutebrowser/config/configutils.py new file mode 100644 index 000000000..96fc0f02d --- /dev/null +++ b/qutebrowser/config/configutils.py @@ -0,0 +1,186 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2018 Florian Bruhin (The Compiler) +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see . + + +"""Utilities and data structures used by various config code.""" + + +import attr + +from qutebrowser.utils import utils +from qutebrowser.config import configexc + + +class _UnsetObject: + + """Sentinel object.""" + + __slots__ = () + + def __repr__(self): + return '' + + +UNSET = _UnsetObject() + + +@attr.s +class ScopedValue: + + """A configuration value which is valid for a UrlPattern. + + Attributes: + value: The value itself. + pattern: The UrlPattern for the value, or None for global values. + """ + + value = attr.ib() + pattern = attr.ib() + + +class Values: + + """A collection of values for a single setting. + + Currently, this is a list and iterates through all possible ScopedValues to + find matching ones. + + In the future, it should be possible to optimize this by doing + pre-selection based on hosts, by making this a dict mapping the + non-wildcard part of the host to a list of matching ScopedValues. + + That way, when searching for a setting for sub.example.com, we only have to + check 'sub.example.com', 'example.com', '.com' and '' instead of checking + all ScopedValues for the given setting. + + Attributes: + opt: The Option being customized. + """ + + def __init__(self, opt, values=None): + self.opt = opt + self._values = values or [] + + def __repr__(self): + return utils.get_repr(self, opt=self.opt, values=self._values, + constructor=True) + + def __str__(self): + """Get the values as human-readable string.""" + if not self: + return '{}: '.format(self.opt.name) + + lines = [] + for scoped in self._values: + str_value = self.opt.typ.to_str(scoped.value) + if scoped.pattern is None: + lines.append('{} = {}'.format(self.opt.name, str_value)) + else: + lines.append('{}: {} = {}'.format( + scoped.pattern, self.opt.name, str_value)) + return '\n'.join(lines) + + def __iter__(self): + """Yield ScopedValue elements. + + This yields in "normal" order, i.e. global and then first-set settings + first. + """ + yield from self._values + + def __bool__(self): + """Check whether this value is customized.""" + return bool(self._values) + + def _check_pattern_support(self, arg): + """Make sure patterns are supported if one was given.""" + if arg is not None and not self.opt.supports_pattern: + raise configexc.NoPatternError(self.opt.name) + + def add(self, value, pattern=None): + """Add a value with the given pattern to the list of values.""" + self._check_pattern_support(pattern) + self.remove(pattern) + scoped = ScopedValue(value, pattern) + self._values.append(scoped) + + def remove(self, pattern=None): + """Remove the value with the given pattern. + + If a matching pattern was removed, True is returned. + If no matching pattern was found, False is returned. + """ + self._check_pattern_support(pattern) + old_len = len(self._values) + self._values = [v for v in self._values if v.pattern != pattern] + return old_len != len(self._values) + + def clear(self): + """Clear all customization for this value.""" + self._values = [] + + def _get_fallback(self, fallback): + """Get the fallback global/default value.""" + for scoped in self._values: + if scoped.pattern is None: + return scoped.value + + if fallback: + return self.opt.default + else: + return UNSET + + def get_for_url(self, url=None, *, fallback=True): + """Get a config value, falling back when needed. + + This first tries to find a value matching the URL (if given). + If there's no match: + With fallback=True, the global/default setting is returned. + With fallback=False, UNSET is returned. + """ + self._check_pattern_support(url) + if url is not None: + for scoped in reversed(self._values): + if scoped.pattern is not None and scoped.pattern.matches(url): + return scoped.value + + if not fallback: + return UNSET + + return self._get_fallback(fallback) + + def get_for_pattern(self, pattern, *, fallback=True): + """Get a value only if it's been overridden for the given pattern. + + This is useful when showing values to the user. + + If there's no match: + With fallback=True, the global/default setting is returned. + With fallback=False, UNSET is returned. + """ + self._check_pattern_support(pattern) + if pattern is not None: + for scoped in reversed(self._values): + if scoped.pattern == pattern: + return scoped.value + + if not fallback: + return UNSET + + return self._get_fallback(fallback) diff --git a/qutebrowser/config/websettings.py b/qutebrowser/config/websettings.py index 0e57af45f..517c3dd3c 100644 --- a/qutebrowser/config/websettings.py +++ b/qutebrowser/config/websettings.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -17,195 +17,150 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -# We get various "abstract but not overridden" warnings -# pylint: disable=abstract-method - """Bridge from QWeb(Engine)Settings to our own settings.""" from PyQt5.QtGui import QFont -from qutebrowser.config import config -from qutebrowser.utils import log, utils, debug, usertypes +from qutebrowser.config import config, configutils +from qutebrowser.utils import log, usertypes from qutebrowser.misc import objects UNSET = object() -class Base: +class AbstractSettings: - """Base class for QWeb(Engine)Settings wrappers.""" + """Abstract base class for settings set via QWeb(Engine)Settings.""" - def __init__(self, default=UNSET): - self._default = default + _ATTRIBUTES = None + _FONT_SIZES = None + _FONT_FAMILIES = None + _FONT_TO_QFONT = None - def _get_global_settings(self): - """Get a list of global QWeb(Engine)Settings to use.""" - raise NotImplementedError + def __init__(self, settings): + self._settings = settings - def _get_settings(self, settings): - """Get a list of QWeb(Engine)Settings objects to use. + def set_attribute(self, name, value): + """Set the given QWebSettings/QWebEngineSettings attribute. - Args: - settings: The QWeb(Engine)Settings instance to use, or None to use - the global instance. + If the value is configutils.UNSET, the value is reset instead. Return: - A list of QWeb(Engine)Settings objects. The first one should be - used for reading. + True if there was a change, False otherwise. """ - if settings is None: - return self._get_global_settings() - else: - return [settings] + old_value = self.test_attribute(name) - def set(self, value, settings=None): - """Set the value of this setting. - - Args: - value: The value to set, or None to restore the default. - settings: The QWeb(Engine)Settings instance to use, or None to use - the global instance. - """ - if value is None: - self.set_default(settings=settings) - else: - self._set(value, settings=settings) - - def set_default(self, settings=None): - """Set the default value for this setting. - - Not implemented for most settings. - """ - if self._default is UNSET: - raise ValueError("No default set for {!r}".format(self)) - else: - self._set(self._default, settings=settings) - - def _set(self, value, settings): - """Inner function to set the value of this setting. - - Must be overridden by subclasses. - - Args: - value: The value to set. - settings: The QWeb(Engine)Settings instance to use, or None to use - the global instance. - """ - raise NotImplementedError - - -class Attribute(Base): - - """A setting set via QWeb(Engine)Settings::setAttribute. - - Attributes: - self._attributes: A list of QWeb(Engine)Settings::WebAttribute members. - """ - - ENUM_BASE = None - - def __init__(self, *attributes, default=UNSET): - super().__init__(default=default) - self._attributes = list(attributes) - - def __repr__(self): - attributes = [debug.qenum_key(self.ENUM_BASE, attr) - for attr in self._attributes] - return utils.get_repr(self, attributes=attributes, constructor=True) - - def _set(self, value, settings=None): - for obj in self._get_settings(settings): - for attribute in self._attributes: - obj.setAttribute(attribute, value) - - -class Setter(Base): - - """A setting set via a QWeb(Engine)Settings setter method. - - This will pass the QWeb(Engine)Settings instance ("self") as first argument - to the methods, so self._setter is the *unbound* method. - - Attributes: - _setter: The unbound QWeb(Engine)Settings method to set this value. - _args: An iterable of the arguments to pass to the setter (before the - value). - _unpack: Whether to unpack args (True) or pass them directly (False). - """ - - def __init__(self, setter, args=(), unpack=False, default=UNSET): - super().__init__(default=default) - self._setter = setter - self._args = args - self._unpack = unpack - - def __repr__(self): - return utils.get_repr(self, setter=self._setter, args=self._args, - unpack=self._unpack, constructor=True) - - def _set(self, value, settings=None): - for obj in self._get_settings(settings): - args = [obj] - args.extend(self._args) - if self._unpack: - args.extend(value) + for attribute in self._ATTRIBUTES[name]: + if value is configutils.UNSET: + self._settings.resetAttribute(attribute) + new_value = self.test_attribute(name) else: - args.append(value) - self._setter(*args) + self._settings.setAttribute(attribute, value) + new_value = value + return old_value != new_value -class StaticSetter(Setter): + def test_attribute(self, name): + """Get the value for the given attribute. - """A setting set via a static QWeb(Engine)Settings method. + If the setting resolves to a list of attributes, only the first + attribute is tested. + """ + return self._settings.testAttribute(self._ATTRIBUTES[name][0]) - self._setter is the *bound* method. - """ + def set_font_size(self, name, value): + """Set the given QWebSettings/QWebEngineSettings font size. - def _set(self, value, settings=None): - if settings is not None: - raise ValueError("'settings' may not be set with StaticSetters!") - args = list(self._args) - if self._unpack: - args.extend(value) - else: - args.append(value) - self._setter(*args) + Return: + True if there was a change, False otherwise. + """ + assert value is not configutils.UNSET + family = self._FONT_SIZES[name] + old_value = self._settings.fontSize(family) + self._settings.setFontSize(family, value) + return old_value != value + def set_font_family(self, name, value): + """Set the given QWebSettings/QWebEngineSettings font family. -class FontFamilySetter(Setter): + With None (the default), QFont is used to get the default font for the + family. - """A setter for a font family. + Return: + True if there was a change, False otherwise. + """ + assert value is not configutils.UNSET + family = self._FONT_FAMILIES[name] + if value is None: + font = QFont() + font.setStyleHint(self._FONT_TO_QFONT[family]) + value = font.defaultFamily() - Gets the default value from QFont. - """ + old_value = self._settings.fontFamily(family) + self._settings.setFontFamily(family, value) - def __init__(self, setter, font, qfont): - super().__init__(setter=setter, args=[font]) - self._qfont = qfont + return value != old_value - def set_default(self, settings=None): - font = QFont() - font.setStyleHint(self._qfont) - value = font.defaultFamily() - self._set(value, settings=settings) + def set_default_text_encoding(self, encoding): + """Set the default text encoding to use. + Return: + True if there was a change, False otherwise. + """ + assert encoding is not configutils.UNSET + old_value = self._settings.defaultTextEncoding() + self._settings.setDefaultTextEncoding(encoding) + return old_value != encoding -def init_mappings(mappings): - """Initialize all settings based on a settings mapping.""" - for option, mapping in mappings.items(): - value = config.instance.get(option) - log.config.vdebug("Setting {} to {!r}".format(option, value)) - mapping.set(value) + def _update_setting(self, setting, value): + """Update the given setting/value. + Unknown settings are ignored. -def update_mappings(mappings, option): - """Update global settings when QWeb(Engine)Settings changed.""" - try: - mapping = mappings[option] - except KeyError: - return - value = config.instance.get(option) - mapping.set(value) + Return: + True if there was a change, False otherwise. + """ + if setting in self._ATTRIBUTES: + return self.set_attribute(setting, value) + elif setting in self._FONT_SIZES: + return self.set_font_size(setting, value) + elif setting in self._FONT_FAMILIES: + return self.set_font_family(setting, value) + elif setting == 'content.default_encoding': + return self.set_default_text_encoding(value) + return False + + def update_setting(self, setting): + """Update the given setting.""" + value = config.instance.get(setting) + self._update_setting(setting, value) + + def update_for_url(self, url): + """Update settings customized for the given tab. + + Return: + A set of settings which actually changed. + """ + changed_settings = set() + for values in config.instance: + if not values.opt.supports_pattern: + continue + + value = values.get_for_url(url, fallback=False) + + changed = self._update_setting(values.opt.name, value) + if changed: + log.config.debug("Changed for {}: {} = {}".format( + url.toDisplayString(), values.opt.name, value)) + changed_settings.add(values.opt.name) + + return changed_settings + + def init_settings(self): + """Set all supported settings correctly.""" + for setting in (list(self._ATTRIBUTES) + list(self._FONT_SIZES) + + list(self._FONT_FAMILIES)): + self.update_setting(setting) def init(args): diff --git a/qutebrowser/html/back.html b/qutebrowser/html/back.html index 894427800..6128c41f7 100644 --- a/qutebrowser/html/back.html +++ b/qutebrowser/html/back.html @@ -27,7 +27,7 @@ function prepare_restore() { return; } - document.addEventListener("visibilitychange", go_back); + document.addEventListener("visibilitychange", go_back, {once: true}); } // there are three states diff --git a/qutebrowser/html/bindings.html b/qutebrowser/html/bindings.html new file mode 100644 index 000000000..fe6913402 --- /dev/null +++ b/qutebrowser/html/bindings.html @@ -0,0 +1,32 @@ +{% extends "styled.html" %} + +{% block style %} +{{ super() }} +th { text-align:left; } +.key { width: 25%; } +.command { width: 75% } +{% endblock %} + +{% block content %} +

{{ title }}

+{% for mode, binding in bindings.items() %} +

{{ mode | capitalize }} mode

+ + + + + + {% for key,command in binding.items() %} + + + + + {% endfor %} +
KeyCommand
+

{{ key }}

+
+

{{ command }}

+
+{% endfor %} + +{% endblock %} diff --git a/qutebrowser/html/tabs.html b/qutebrowser/html/tabs.html new file mode 100644 index 000000000..fff8bdca3 --- /dev/null +++ b/qutebrowser/html/tabs.html @@ -0,0 +1,58 @@ +{% extends "styled.html" %} + +{% block style %} +{{super()}} +h1 { + margin-bottom: 10px; +} + +.url a { + color: #444; +} + +th { + text-align: left; +} + +.qmarks .name { + padding-left: 5px; +} + +.empty-msg { + background-color: #f8f8f8; + color: #444; + display: inline-block; + text-align: center; + width: 100%; +} + +details { + margin-top: 20px; +} +{% endblock %} + +{% block content %} + +

Tab list

+{% for win_id, tabs in tab_list_by_window.items() %} +

Window {{ win_id }}

+ + + {% for name, url in tabs %} + + + + + {% endfor %} + +
{{name}}{{url}}
+{% endfor %} +
+ Raw list + +{% for win_id, tabs in tab_list_by_window.items() %}{% for name, url in tabs %} +{{url}}
{% endfor %} +{% endfor %} +
+
+{% endblock %} diff --git a/qutebrowser/html/version.html b/qutebrowser/html/version.html index 736eb7547..368fbaed6 100644 --- a/qutebrowser/html/version.html +++ b/qutebrowser/html/version.html @@ -1,8 +1,22 @@ {% extends "base.html" %} + +{% block script %} +function paste_version() { + const xhr = new XMLHttpRequest(); + xhr.open("GET", "qute://pastebin-version"); + xhr.send(); +} +{% endblock %} + +{% block style %} +html { margin-left: 10px; } +{% endblock %} + {% block content %} {{ super() }}

Version info

{{ version }}
+

Copyright info

{{ copyright }}

diff --git a/qutebrowser/javascript/.eslintrc.yaml b/qutebrowser/javascript/.eslintrc.yaml index 6fdd16639..1e6ee2a20 100644 --- a/qutebrowser/javascript/.eslintrc.yaml +++ b/qutebrowser/javascript/.eslintrc.yaml @@ -33,7 +33,7 @@ rules: no-extra-parens: off id-length: ["error", {"exceptions": ["i", "k", "x", "y"]}] object-shorthand: "off" - max-statements: ["error", {"max": 30}] + max-statements: ["error", {"max": 40}] quotes: ["error", "double", {"avoidEscape": true}] object-property-newline: ["error", {"allowMultiplePropertiesPerLine": true}] comma-dangle: ["error", "always-multiline"] @@ -54,3 +54,5 @@ rules: function-paren-newline: "off" multiline-comment-style: "off" no-bitwise: "off" + no-ternary: "off" + max-lines: "off" diff --git a/qutebrowser/javascript/caret.js b/qutebrowser/javascript/caret.js new file mode 100644 index 000000000..5088c3e2f --- /dev/null +++ b/qutebrowser/javascript/caret.js @@ -0,0 +1,1368 @@ +/* eslint-disable max-len, max-statements, complexity, +max-params, default-case, valid-jsdoc */ + +// Copyright 2014 The Chromium Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// 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 +// OWNER 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. + +/** + * Copyright 2018 Florian Bruhin (The Compiler) + * + * This file is part of qutebrowser. + * + * qutebrowser is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * qutebrowser is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with qutebrowser. If not, see . + */ + +/** + * Ported chrome-caretbrowsing extension. + * https://cs.chromium.org/chromium/src/ui/accessibility/extensions/caretbrowsing/ + * + * The behavior is based on Mozilla's spec whenever possible: + * http://www.mozilla.org/access/keyboard/proposal + * + * The one exception is that Esc is used to escape out of a form control, + * rather than their proposed key (which doesn't seem to work in the + * latest Firefox anyway). + * + * Some details about how Chrome selection works, which will help in + * understanding the code: + * + * The Selection object (window.getSelection()) has four components that + * completely describe the state of the caret or selection: + * + * base and anchor: this is the start of the selection, the fixed point. + * extent and focus: this is the end of the selection, the part that + * moves when you hold down shift and press the left or right arrows. + * + * When the selection is a cursor, the base, anchor, extent, and focus are + * all the same. + * + * There's only one time when the base and anchor are not the same, or the + * extent and focus are not the same, and that's when the selection is in + * an ambiguous state - i.e. it's not clear which edge is the focus and which + * is the anchor. As an example, if you double-click to select a word, then + * the behavior is dependent on your next action. If you press Shift+Right, + * the right edge becomes the focus. But if you press Shift+Left, the left + * edge becomes the focus. + * + * When the selection is in an ambiguous state, the base and extent are set + * to the position where the mouse clicked, and the anchor and focus are set + * to the boundaries of the selection. + * + * The only way to set the selection and give it direction is to use + * the non-standard Selection.setBaseAndExtent method. If you try to use + * Selection.addRange(), the anchor will always be on the left and the focus + * will always be on the right, making it impossible to manipulate + * selections that move from right to left. + * + * Finally, Chrome will throw an exception if you try to set an invalid + * selection - a selection where the left and right edges are not the same, + * but it doesn't span any visible characters. A common example is that + * there are often many whitespace characters in the DOM that are not + * visible on the page; trying to select them will fail. Another example is + * any node that's invisible or not displayed. + * + * While there are probably many possible methods to determine what is + * selectable, this code uses the method of determining if there's a valid + * bounding box for the range or not - keep moving the cursor forwards until + * the range from the previous position and candidate next position has a + * valid bounding box. + */ + +"use strict"; + +window._qutebrowser.caret = (function() { + function isElementInViewport(node) { + let i; + let boundingRect = (node.getClientRects()[0] || + node.getBoundingClientRect()); + + if (boundingRect.width <= 1 && boundingRect.height <= 1) { + const rects = node.getClientRects(); + for (i = 0; i < rects.length; i++) { + if (rects[i].width > rects[0].height && + rects[i].height > rects[0].height) { + boundingRect = rects[i]; + } + } + } + if (boundingRect === undefined) { + return null; + } + if (boundingRect.top > innerHeight || boundingRect.left > innerWidth) { + return null; + } + if (boundingRect.width <= 1 || boundingRect.height <= 1) { + const children = node.children; + let visibleChildNode = false; + for (i = 0; i < children.length; ++i) { + boundingRect = (children[i].getClientRects()[0] || + children[i].getBoundingClientRect()); + if (boundingRect.width > 1 && boundingRect.height > 1) { + visibleChildNode = true; + break; + } + } + if (visibleChildNode === false) { + return null; + } + } + if (boundingRect.top + boundingRect.height < 10 || + boundingRect.left + boundingRect.width < -10) { + return null; + } + const computedStyle = window.getComputedStyle(node, null); + if (computedStyle.visibility !== "visible" || + computedStyle.display === "none" || + node.hasAttribute("disabled") || + parseInt(computedStyle.width, 10) === 0 || + parseInt(computedStyle.height, 10) === 0) { + return null; + } + return boundingRect.top >= -20; + } + + function positionCaret() { + const walker = document.createTreeWalker(document.body, -1); + let node; + const textNodes = []; + let el; + while ((node = walker.nextNode())) { + if (node.nodeType === 3 && node.nodeValue.trim() !== "") { + textNodes.push(node); + } + } + for (let i = 0; i < textNodes.length; i++) { + const element = textNodes[i].parentElement; + if (isElementInViewport(element)) { + el = element; + break; + } + } + if (el !== undefined) { + /* eslint-disable no-use-before-define */ + const start = new Cursor(el, 0, ""); + const end = new Cursor(el, 0, ""); + const nodesCrossed = []; + const result = TraverseUtil.getNextChar( + start, end, nodesCrossed, true); + if (result === null) { + return; + } + CaretBrowsing.setAndValidateSelection(start, start); + /* eslint-enable no-use-before-define */ + } + } + + /** + * Return whether a node is focusable. This includes nodes whose tabindex + * attribute is set to "-1" explicitly - these nodes are not in the tab + * order, but they should still be focused if the user navigates to them + * using linear or smart DOM navigation. + * + * Note that when the tabIndex property of an Element is -1, that doesn't + * tell us whether the tabIndex attribute is missing or set to "-1" explicitly, + * so we have to check the attribute. + * + * @param {Object} targetNode The node to check if it's focusable. + * @return {boolean} True if the node is focusable. + */ + function isFocusable(targetNode) { + if (!targetNode || typeof (targetNode.tabIndex) !== "number") { + return false; + } + + if (targetNode.tabIndex >= 0) { + return true; + } + + if (targetNode.hasAttribute && + targetNode.hasAttribute("tabindex") && + targetNode.getAttribute("tabindex") === "-1") { + return true; + } + + return false; + } + + const axs = {}; + + axs.dom = {}; + + axs.color = {}; + + axs.utils = {}; + + axs.dom.parentElement = function(node) { + if (!node) { + return null; + } + const composedNode = axs.dom.composedParentNode(node); + if (!composedNode) { + return null; + } + switch (composedNode.nodeType) { + case Node.ELEMENT_NODE: + return composedNode; + default: + return axs.dom.parentElement(composedNode); + } + }; + + axs.dom.shadowHost = function(node) { + if ("host" in node) { + return node.host; + } + return null; + }; + + axs.dom.composedParentNode = function(node) { + if (!node) { + return null; + } + if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { + return axs.dom.shadowHost(node); + } + const parentNode = node.parentNode; + if (!parentNode) { + return null; + } + if (parentNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { + return axs.dom.shadowHost(parentNode); + } + if (!parentNode.shadowRoot) { + return parentNode; + } + const points = node.getDestinationInsertionPoints(); + if (points.length > 0) { + return axs.dom.composedParentNode(points[points.length - 1]); + } + return null; + }; + + axs.color.Color = function(red, green, blue, alpha) { + this.red = red; + this.green = green; + this.blue = blue; + this.alpha = alpha; + }; + + axs.color.parseColor = function(colorText) { + if (colorText === "transparent") { + return new axs.color.Color(0, 0, 0, 0); + } + let match = colorText.match(/^rgb\((\d+), (\d+), (\d+)\)$/); + if (match) { + const blue = parseInt(match[3], 10); + const green = parseInt(match[2], 10); + const red = parseInt(match[1], 10); + return new axs.color.Color(red, green, blue, 1); + } + match = colorText.match(/^rgba\((\d+), (\d+), (\d+), (\d*(\.\d+)?)\)/); + if (match) { + const red = parseInt(match[1], 10); + const green = parseInt(match[2], 10); + const blue = parseInt(match[3], 10); + const alpha = parseFloat(match[4]); + return new axs.color.Color(red, green, blue, alpha); + } + return null; + }; + + axs.color.flattenColors = function(color1, color2) { + const colorAlpha = color1.alpha; + return new axs.color.Color( + ((1 - colorAlpha) * color2.red) + (colorAlpha * color1.red), + ((1 - colorAlpha) * color2.green) + (colorAlpha * color1.green), + ((1 - colorAlpha) * color2.blue) + (colorAlpha * color2.blue), + color1.alpha + (color2.alpha * (1 - color1.alpha))); + }; + + axs.utils.getParentBgColor = function(_el) { + let el = _el; + let el2 = el; + let iter = null; + el = []; + for (iter = null; (el2 = axs.dom.parentElement(el2));) { + const style = window.getComputedStyle(el2, null); + if (style) { + const color = axs.color.parseColor(style.backgroundColor); + if (color && + (style.opacity < 1 && + (color.alpha *= style.opacity), + color.alpha !== 0 && + (el.push(color), color.alpha === 1))) { + iter = !0; + break; + } + } + } + if (!iter) { + el.push(new axs.color.Color(255, 255, 255, 1)); + } + for (el2 = el.pop(); el.length;) { + iter = el.pop(); + el2 = axs.color.flattenColors(iter, el2); + } + return el2; + }; + + axs.utils.getFgColor = function(el, el2, color) { + let color2 = axs.color.parseColor(el.color); + if (!color2) { + return null; + } + if (color2.alpha < 1) { + color2 = axs.color.flattenColors(color2, color); + } + if (el.opacity < 1) { + const el3 = axs.utils.getParentBgColor(el2); + color2.alpha *= el.opacity; + color2 = axs.color.flattenColors(color2, el3); + } + return color2; + }; + + axs.utils.getBgColor = function(el, elParent) { + let color = axs.color.parseColor(el.backgroundColor); + if (!color) { + return null; + } + if (el.opacity < 1) { + color.alpha *= el.opacity; + } + if (color.alpha < 1) { + const bgColor = axs.utils.getParentBgColor(elParent); + if (bgColor === null) { + return null; + } + color = axs.color.flattenColors(color, bgColor); + } + return color; + }; + + axs.color.colorChannelToString = function(_color) { + const color = Math.round(_color); + if (color < 15) { + return `0${color.toString(16)}`; + } + return color.toString(16); + }; + + axs.color.colorToString = function(color) { + if (color.alpha === 1) { + const red = axs.color.colorChannelToString(color.red); + const green = axs.color.colorChannelToString(color.green); + const blue = axs.color.colorChannelToString(color.blue); + return `#${red}${green}${blue}`; + } + const arr = [color.red, color.green, color.blue, color.alpha].join(); + return `rgba(${arr})`; + }; + + /** + * A class to represent a cursor location in the document, + * like the start position or end position of a selection range. + * + * Later this may be extended to support "virtual text" for an object, + * like the ALT text for an image. + * + * Note: we cache the text of a particular node at the time we + * traverse into it. Later we should add support for dynamically + * reloading it. + * @param {Node} node The DOM node. + * @param {number} index The index of the character within the node. + * @param {string} text The cached text contents of the node. + * @constructor + */ + // eslint-disable-next-line func-style + const Cursor = function(node, index, text) { + this.node = node; + this.index = index; + this.text = text; + }; + + /** + * @return {Cursor} A new cursor pointing to the same location. + */ + Cursor.prototype.clone = function() { + return new Cursor(this.node, this.index, this.text); + }; + + /** + * Modify this cursor to point to the location that another cursor points to. + * @param {Cursor} otherCursor The cursor to copy from. + */ + Cursor.prototype.copyFrom = function(otherCursor) { + this.node = otherCursor.node; + this.index = otherCursor.index; + this.text = otherCursor.text; + }; + + /** + * Utility functions for stateless DOM traversal. + * @constructor + */ + const TraverseUtil = {}; + + /** + * Gets the text representation of a node. This allows us to substitute + * alt text, names, or titles for html elements that provide them. + * @param {Node} node A DOM node. + * @return {string} A text string representation of the node. + */ + TraverseUtil.getNodeText = function(node) { + if (node.constructor === Text) { + return node.data; + } + return ""; + }; + + /** + * Return true if a node should be treated as a leaf node, because + * its children are properties of the object that shouldn't be traversed. + * + * TODO(dmazzoni): replace this with a predicate that detects nodes with + * ARIA roles and other objects that have their own description. + * For now we just detect a couple of common cases. + * + * @param {Node} node A DOM node. + * @return {boolean} True if the node should be treated as a leaf node. + */ + TraverseUtil.treatAsLeafNode = function(node) { + return node.childNodes.length === 0 || + node.nodeName === "SELECT" || + node.nodeName === "OBJECT"; + }; + + /** + * Return true only if a single character is whitespace. + * From https://developer.mozilla.org/en/Whitespace_in_the_DOM, + * whitespace is defined as one of the characters + * "\t" TAB \u0009 + * "\n" LF \u000A + * "\r" CR \u000D + * " " SPC \u0020. + * + * @param {string} c A string containing a single character. + * @return {boolean} True if the character is whitespace, otherwise false. + */ + TraverseUtil.isWhitespace = function(ch) { + return (ch === " " || ch === "\n" || ch === "\r" || ch === "\t"); + }; + + /** + * Use the computed CSS style to figure out if this DOM node is currently + * visible. + * @param {Node} node A HTML DOM node. + * @return {boolean} Whether or not the html node is visible. + */ + TraverseUtil.isVisible = function(node) { + if (!node.style) { + return true; + } + const style = window.getComputedStyle(node, null); + return (Boolean(style) && + style.display !== "none" && + style.visibility !== "hidden"); + }; + + /** + * Use the class name to figure out if this DOM node should be traversed. + * @param {Node} node A HTML DOM node. + * @return {boolean} Whether or not the html node should be traversed. + */ + TraverseUtil.isSkipped = function(_node) { + let node = _node; + if (node.constructor === Text) { + node = node.parentElement; + } + if (node.className === "CaretBrowsing_Caret") { + return true; + } + return false; + }; + + /** + * Moves the cursor forwards until it has crossed exactly one character. + * @param {Cursor} cursor The cursor location where the search should start. + * On exit, the cursor will be immediately to the right of the + * character returned. + * @param {Array} nodesCrossed Any HTML nodes crossed between the + * initial and final cursor position will be pushed onto this array. + * @return {?string} The character found, or null if the bottom of the + * document has been reached. + */ + TraverseUtil.forwardsChar = function(cursor, nodesCrossed) { + for (;;) { + let childNode = null; + if (!TraverseUtil.treatAsLeafNode(cursor.node)) { + for (let i = cursor.index; + i < cursor.node.childNodes.length; + i++) { + const node = cursor.node.childNodes[i]; + if (TraverseUtil.isSkipped(node)) { + nodesCrossed.push(node); + } else if (TraverseUtil.isVisible(node)) { + childNode = node; + break; + } + } + } + if (childNode) { + cursor.node = childNode; + cursor.index = 0; + cursor.text = TraverseUtil.getNodeText(cursor.node); + if (cursor.node.constructor !== Text) { + nodesCrossed.push(cursor.node); + } + } else { + // Return the next character from this leaf node. + if (cursor.index < cursor.text.length) { + return cursor.text[cursor.index++]; + } + + // Move to the next sibling, going up the tree as necessary. + while (cursor.node !== null) { + // Try to move to the next sibling. + let siblingNode = null; + for (let node = cursor.node.nextSibling; + node !== null; + node = node.nextSibling) { + if (TraverseUtil.isSkipped(node)) { + nodesCrossed.push(node); + } else if (TraverseUtil.isVisible(node)) { + siblingNode = node; + break; + } + } + if (siblingNode) { + cursor.node = siblingNode; + cursor.text = TraverseUtil.getNodeText(siblingNode); + cursor.index = 0; + + if (cursor.node.constructor !== Text) { + nodesCrossed.push(cursor.node); + } + + break; + } + + // Otherwise, move to the parent. + const parentNode = cursor.node.parentNode; + if (parentNode && + parentNode.constructor !== HTMLBodyElement) { + cursor.node = cursor.node.parentNode; + cursor.text = null; + cursor.index = 0; + } else { + return null; + } + } + } + } + }; + + /** + * Finds the next character, starting from endCursor. Upon exit, startCursor + * and endCursor will surround the next character. If skipWhitespace is + * true, will skip until a real character is found. Otherwise, it will + * attempt to select all of the whitespace between the initial position + * of endCursor and the next non-whitespace character. + * @param {Cursor} startCursor On exit, points to the position before + * the char. + * @param {Cursor} endCursor The position to start searching for the next + * char. On exit, will point to the position past the char. + * @param {Array} nodesCrossed Any HTML nodes crossed between the + * initial and final cursor position will be pushed onto this array. + * @param {boolean} skipWhitespace If true, will keep scanning until a + * non-whitespace character is found. + * @return {?string} The next char, or null if the bottom of the + * document has been reached. + */ + TraverseUtil.getNextChar = function( + startCursor, endCursor, nodesCrossed, skipWhitespace) { + // Save the starting position and get the first character. + startCursor.copyFrom(endCursor); + let fChar = TraverseUtil.forwardsChar(endCursor, nodesCrossed); + if (fChar === null) { + return null; + } + + // Keep track of whether the first character was whitespace. + const initialWhitespace = TraverseUtil.isWhitespace(fChar); + + // Keep scanning until we find a non-whitespace or non-skipped character. + while ((TraverseUtil.isWhitespace(fChar)) || + (TraverseUtil.isSkipped(endCursor.node))) { + fChar = TraverseUtil.forwardsChar(endCursor, nodesCrossed); + if (fChar === null) { + return null; + } + } + if (skipWhitespace || !initialWhitespace) { + // If skipWhitepace is true, or if the first character we encountered + // was not whitespace, return that non-whitespace character. + startCursor.copyFrom(endCursor); + startCursor.index--; + return fChar; + } + + for (let i = 0; i < nodesCrossed.length; i++) { + if (TraverseUtil.isSkipped(nodesCrossed[i])) { + // We need to make sure that startCursor and endCursor aren't + // surrounding a skippable node. + endCursor.index--; + startCursor.copyFrom(endCursor); + startCursor.index--; + return " "; + } + } + // Otherwise, return all of the whitespace before that last character. + endCursor.index--; + return " "; + }; + + /** + * The class handling the Caret Browsing implementation in the page. + * Sets up communication with the background page, and then when caret + * browsing is enabled, response to various key events to move the caret + * or selection within the text content of the document. + * @constructor + */ + const CaretBrowsing = {}; + + /** + * Is caret browsing enabled? + * @type {boolean} + */ + CaretBrowsing.isEnabled = false; + + /** + * Keep it enabled even when flipped off (for the options page)? + * @type {boolean} + */ + CaretBrowsing.forceEnabled = false; + + /** + * What to do when the caret appears? + * @type {string} + */ + CaretBrowsing.onEnable = undefined; + + /** + * What to do when the caret jumps? + * @type {string} + */ + CaretBrowsing.onJump = undefined; + + /** + * Is this window / iframe focused? We won't show the caret if not, + * especially so that carets aren't shown in two iframes of the same + * tab. + * @type {boolean} + */ + CaretBrowsing.isWindowFocused = false; + + /** + * Is the caret actually visible? This is true only if isEnabled and + * isWindowFocused are both true. + * @type {boolean} + */ + CaretBrowsing.isCaretVisible = false; + + /** + * The actual caret element, an absolute-positioned flashing line. + * @type {Element} + */ + CaretBrowsing.caretElement = undefined; + + /** + * The x-position of the caret, in absolute pixels. + * @type {number} + */ + CaretBrowsing.caretX = 0; + + /** + * The y-position of the caret, in absolute pixels. + * @type {number} + */ + CaretBrowsing.caretY = 0; + + /** + * The width of the caret in pixels. + * @type {number} + */ + CaretBrowsing.caretWidth = 0; + + /** + * The height of the caret in pixels. + * @type {number} + */ + CaretBrowsing.caretHeight = 0; + + /** + * The foregroundc color. + * @type {string} + */ + CaretBrowsing.caretForeground = "#000"; + + /** + * The backgroundc color. + * @type {string} + */ + CaretBrowsing.caretBackground = "#fff"; + + /** + * Is the selection collapsed, i.e. are the start and end locations + * the same? If so, our blinking caret image is shown; otherwise + * the Chrome selection is shown. + * @type {boolean} + */ + CaretBrowsing.isSelectionCollapsed = false; + + /** + * The id returned by window.setInterval for our blink function, so + * we can cancel it when caret browsing is disabled. + * @type {number?} + */ + CaretBrowsing.blinkFunctionId = null; + + /** + * The desired x-coordinate to match when moving the caret up and down. + * To match the behavior as documented in Mozilla's caret browsing spec + * (http://www.mozilla.org/access/keyboard/proposal), we keep track of the + * initial x position when the user starts moving the caret up and down, + * so that the x position doesn't drift as you move throughout lines, but + * stays as close as possible to the initial position. This is reset when + * moving left or right or clicking. + * @type {number?} + */ + CaretBrowsing.targetX = null; + + /** + * A flag that flips on or off as the caret blinks. + * @type {boolean} + */ + CaretBrowsing.blinkFlag = true; + + /** + * Check if a node is a control that normally allows the user to interact + * with it using arrow keys. We won't override the arrow keys when such a + * control has focus, the user must press Escape to do caret browsing outside + * that control. + * @param {Node} node A node to check. + * @return {boolean} True if this node is a control that the user can + * interact with using arrow keys. + */ + CaretBrowsing.isControlThatNeedsArrowKeys = function(node) { + if (!node) { + return false; + } + + if (node === document.body || node !== document.activeElement) { + return false; + } + + if (node.constructor === HTMLSelectElement) { + return true; + } + + if (node.constructor === HTMLInputElement) { + switch (node.type) { + case "email": + case "number": + case "password": + case "search": + case "text": + case "tel": + case "url": + case "": + return true; // All of these are text boxes. + case "datetime": + case "datetime-local": + case "date": + case "month": + case "radio": + case "range": + case "week": + return true; // These are other input elements that use arrows. + } + } + + // Handle focusable ARIA controls. + if (node.getAttribute && isFocusable(node)) { + const role = node.getAttribute("role"); + switch (role) { + case "combobox": + case "grid": + case "gridcell": + case "listbox": + case "menu": + case "menubar": + case "menuitem": + case "menuitemcheckbox": + case "menuitemradio": + case "option": + case "radiogroup": + case "scrollbar": + case "slider": + case "spinbutton": + case "tab": + case "tablist": + case "textbox": + case "tree": + case "treegrid": + case "treeitem": + return true; + } + } + + return false; + }; + + CaretBrowsing.injectCaretStyles = function() { + const style = ".CaretBrowsing_Caret {" + + " position: absolute;" + + " z-index: 2147483647;" + + " min-height: 10px;" + + " background-color: #000;" + + "}"; + const node = document.createElement("style"); + node.innerHTML = style; + document.body.appendChild(node); + }; + + /** + * If there's no initial selection, set the cursor just before the + * first text character in the document. + */ + CaretBrowsing.setInitialCursor = function() { + const selectionRange = window.getSelection().toString().length; + if (selectionRange === 0) { + positionCaret(); + } + + CaretBrowsing.injectCaretStyles(); + CaretBrowsing.toggle(); + CaretBrowsing.initiated = true; + CaretBrowsing.selectionEnabled = selectionRange > 0; + }; + + /** + * Try to set the window's selection to be between the given start and end + * cursors, and return whether or not it was successful. + * @param {Cursor} start The start position. + * @param {Cursor} end The end position. + * @return {boolean} True if the selection was successfully set. + */ + CaretBrowsing.setAndValidateSelection = function(start, end) { + const sel = window.getSelection(); + sel.setBaseAndExtent(start.node, start.index, end.node, end.index); + + if (sel.rangeCount !== 1) { + return false; + } + + return (sel.anchorNode === start.node && + sel.anchorOffset === start.index && + sel.focusNode === end.node && + sel.focusOffset === end.index); + }; + + /** + * Set focus to a node if it's focusable. If it's an input element, + * select the text, otherwise it doesn't appear focused to the user. + * Every other control behaves normally if you just call focus() on it. + * @param {Node} node The node to focus. + * @return {boolean} True if the node was focused. + */ + CaretBrowsing.setFocusToNode = function(nodeArg) { + let node = nodeArg; + while (node && node !== document.body) { + if (isFocusable(node) && node.constructor !== HTMLIFrameElement) { + node.focus(); + if (node.constructor === HTMLInputElement && node.select) { + node.select(); + } + return true; + } + node = node.parentNode; + } + + return false; + }; + + /** + * Set the caret element's normal style, i.e. not when animating. + */ + CaretBrowsing.setCaretElementNormalStyle = function() { + const element = CaretBrowsing.caretElement; + element.className = "CaretBrowsing_Caret"; + if (CaretBrowsing.isSelectionCollapsed) { + element.style.opacity = "1.0"; + } else { + element.style.opacity = "0.0"; + } + element.style.left = `${CaretBrowsing.caretX}px`; + element.style.top = `${CaretBrowsing.caretY}px`; + element.style.width = `${CaretBrowsing.caretWidth}px`; + element.style.height = `${CaretBrowsing.caretHeight}px`; + element.style.color = CaretBrowsing.caretForeground; + }; + + /** + * Create the caret element. This assumes that caretX, caretY, + * caretWidth, and caretHeight have all been set. The caret is + * animated in so the user can find it when it first appears. + */ + CaretBrowsing.createCaretElement = function() { + const element = document.createElement("div"); + element.className = "CaretBrowsing_Caret"; + document.body.appendChild(element); + CaretBrowsing.caretElement = element; + CaretBrowsing.setCaretElementNormalStyle(); + }; + + /** + * Recreate the caret element, triggering any intro animation. + */ + CaretBrowsing.recreateCaretElement = function() { + if (CaretBrowsing.caretElement) { + window.clearInterval(CaretBrowsing.blinkFunctionId); + CaretBrowsing.caretElement.parentElement.removeChild( + CaretBrowsing.caretElement); + CaretBrowsing.caretElement = null; + CaretBrowsing.updateIsCaretVisible(); + } + }; + + /** + * Get the rectangle for a cursor position. This is tricky because + * you can't get the bounding rectangle of an empty range, so this function + * computes the rect by trying a range including one character earlier or + * later than the cursor position. + * @param {Cursor} cursor A single cursor position. + * @return {{left: number, top: number, width: number, height: number}} + * The bounding rectangle of the cursor. + */ + CaretBrowsing.getCursorRect = function(cursor) { + let node = cursor.node; + const index = cursor.index; + const rect = { + "left": 0, + "top": 0, + "width": 1, + "height": 0, + }; + if (node.constructor === Text) { + let left = index; + let right = index; + const max = node.data.length; + const newRange = document.createRange(); + while (left > 0 || right < max) { + if (left > 0) { + left--; + newRange.setStart(node, left); + newRange.setEnd(node, index); + const rangeRect = newRange.getBoundingClientRect(); + if (rangeRect && rangeRect.width && rangeRect.height) { + rect.left = rangeRect.right; + rect.top = rangeRect.top; + rect.height = rangeRect.height; + break; + } + } + if (right < max) { + right++; + newRange.setStart(node, index); + newRange.setEnd(node, right); + const rangeRect = newRange.getBoundingClientRect(); + if (rangeRect && rangeRect.width && rangeRect.height) { + rect.left = rangeRect.left; + rect.top = rangeRect.top; + rect.height = rangeRect.height; + break; + } + } + } + } else { + rect.height = node.offsetHeight; + while (node !== null) { + rect.left += node.offsetLeft; + rect.top += node.offsetTop; + node = node.offsetParent; + } + } + rect.left += window.pageXOffset; + rect.top += window.pageYOffset; + return rect; + }; + + /** + * Compute the new location of the caret or selection and update + * the element as needed. + * @param {boolean} scrollToSelection If true, will also scroll the page + * to the caret / selection location. + */ + CaretBrowsing.updateCaretOrSelection = + function(scrollToSelection) { + const sel = window.getSelection(); + if (sel.rangeCount === 0) { + if (CaretBrowsing.caretElement) { + CaretBrowsing.isSelectionCollapsed = false; + CaretBrowsing.caretElement.style.opacity = "0.0"; + } + return; + } + + const range = sel.getRangeAt(0); + if (!range) { + if (CaretBrowsing.caretElement) { + CaretBrowsing.isSelectionCollapsed = false; + CaretBrowsing.caretElement.style.opacity = "0.0"; + } + return; + } + + if (CaretBrowsing.isControlThatNeedsArrowKeys( + document.activeElement)) { + let node = document.activeElement; + CaretBrowsing.caretWidth = node.offsetWidth; + CaretBrowsing.caretHeight = node.offsetHeight; + CaretBrowsing.caretX = 0; + CaretBrowsing.caretY = 0; + while (node.offsetParent) { + CaretBrowsing.caretX += node.offsetLeft; + CaretBrowsing.caretY += node.offsetTop; + node = node.offsetParent; + } + CaretBrowsing.isSelectionCollapsed = false; + } else if (range.startOffset !== range.endOffset || + range.startContainer !== range.endContainer) { + const rect = range.getBoundingClientRect(); + if (!rect) { + return; + } + CaretBrowsing.caretX = rect.left + window.pageXOffset; + CaretBrowsing.caretY = rect.top + window.pageYOffset; + CaretBrowsing.caretWidth = rect.width; + CaretBrowsing.caretHeight = rect.height; + CaretBrowsing.isSelectionCollapsed = false; + } else { + const rect = CaretBrowsing.getCursorRect( + new Cursor(range.startContainer, + range.startOffset, + TraverseUtil.getNodeText(range.startContainer))); + CaretBrowsing.caretX = rect.left; + CaretBrowsing.caretY = rect.top; + CaretBrowsing.caretWidth = rect.width; + CaretBrowsing.caretHeight = rect.height; + CaretBrowsing.isSelectionCollapsed = true; + } + + if (CaretBrowsing.caretElement) { + const element = CaretBrowsing.caretElement; + if (CaretBrowsing.isSelectionCollapsed) { + element.style.opacity = "1.0"; + element.style.left = `${CaretBrowsing.caretX}px`; + element.style.top = `${CaretBrowsing.caretY}px`; + element.style.width = `${CaretBrowsing.caretWidth}px`; + element.style.height = `${CaretBrowsing.caretHeight}px`; + } else { + element.style.opacity = "0.0"; + } + } else { + CaretBrowsing.createCaretElement(); + } + + let elem = range.startContainer; + if (elem.constructor === Text) { + elem = elem.parentElement; + } + const style = window.getComputedStyle(elem); + const bg = axs.utils.getBgColor(style, elem); + const fg = axs.utils.getFgColor(style, elem, bg); + CaretBrowsing.caretBackground = axs.color.colorToString(bg); + CaretBrowsing.caretForeground = axs.color.colorToString(fg); + + if (scrollToSelection) { + // Scroll just to the "focus" position of the selection, + // the part the user is manipulating. + const rect = CaretBrowsing.getCursorRect( + new Cursor(sel.focusNode, sel.focusOffset, + TraverseUtil.getNodeText(sel.focusNode))); + + const yscroll = window.pageYOffset; + const pageHeight = window.innerHeight; + const caretY = rect.top; + const caretHeight = Math.min(rect.height, 30); + if (yscroll + pageHeight < caretY + caretHeight) { + window.scroll(0, (caretY + caretHeight - pageHeight + 100)); + } else if (caretY < yscroll) { + window.scroll(0, (caretY - 100)); + } + } + }; + + CaretBrowsing.move = function(direction, granularity) { + let action = "move"; + if (CaretBrowsing.selectionEnabled) { + action = "extend"; + } + window. + getSelection(). + modify(action, direction, granularity); + + if (CaretBrowsing.isWindows && + (direction === "forward" || + direction === "right") && + granularity === "word") { + CaretBrowsing.move("left", "character"); + } else { + window.setTimeout(() => { + CaretBrowsing.updateCaretOrSelection(true); + }, 0); + } + }; + + CaretBrowsing.moveToBlock = function(paragraph, boundary) { + let action = "move"; + if (CaretBrowsing.selectionEnabled) { + action = "extend"; + } + window. + getSelection(). + modify(action, paragraph, "paragraph"); + + window. + getSelection(). + modify(action, boundary, "paragraphboundary"); + + window.setTimeout(() => { + CaretBrowsing.updateCaretOrSelection(true); + }, 0); + }; + + CaretBrowsing.toggle = function(value) { + if (CaretBrowsing.forceEnabled) { + CaretBrowsing.recreateCaretElement(); + return; + } + + if (value === undefined) { + CaretBrowsing.isEnabled = !CaretBrowsing.isEnabled; + } else { + CaretBrowsing.isEnabled = value; + } + CaretBrowsing.updateIsCaretVisible(); + }; + + /** + * Event handler, called when the mouse is clicked. Chrome already + * sets the selection when the mouse is clicked, all we need to do is + * update our cursor. + * @param {Event} evt The DOM event. + * @return {boolean} True if the default action should be performed. + */ + CaretBrowsing.onClick = function() { + if (!CaretBrowsing.isEnabled) { + return true; + } + window.setTimeout(() => { + CaretBrowsing.targetX = null; + CaretBrowsing.updateCaretOrSelection(false); + }, 0); + return true; + }; + + /** + * Update whether or not the caret is visible, based on whether caret browsing + * is enabled and whether this window / iframe has focus. + */ + CaretBrowsing.updateIsCaretVisible = function() { + CaretBrowsing.isCaretVisible = + (CaretBrowsing.isEnabled && CaretBrowsing.isWindowFocused); + if (CaretBrowsing.isCaretVisible && !CaretBrowsing.caretElement) { + CaretBrowsing.setInitialCursor(); + CaretBrowsing.updateCaretOrSelection(true); + } else if (!CaretBrowsing.isCaretVisible && + CaretBrowsing.caretElement) { + window.clearInterval(CaretBrowsing.blinkFunctionId); + if (CaretBrowsing.caretElement) { + CaretBrowsing.isSelectionCollapsed = false; + CaretBrowsing.caretElement.parentElement.removeChild( + CaretBrowsing.caretElement); + CaretBrowsing.caretElement = null; + } + } + }; + + CaretBrowsing.onWindowFocus = function() { + CaretBrowsing.isWindowFocused = true; + CaretBrowsing.updateIsCaretVisible(); + }; + + CaretBrowsing.onWindowBlur = function() { + CaretBrowsing.isWindowFocused = false; + CaretBrowsing.updateIsCaretVisible(); + }; + + CaretBrowsing.init = function() { + CaretBrowsing.isWindowFocused = document.hasFocus(); + + document.addEventListener("click", CaretBrowsing.onClick, false); + window.addEventListener("focus", CaretBrowsing.onWindowFocus, false); + window.addEventListener("blur", CaretBrowsing.onWindowBlur, false); + }; + + window.setTimeout(() => { + if (!window.caretBrowsingLoaded) { + window.caretBrowsingLoaded = true; + CaretBrowsing.init(); + + if (document.body && + document.body.getAttribute("caretbrowsing") === "on") { + CaretBrowsing.forceEnabled = true; + CaretBrowsing.isEnabled = true; + CaretBrowsing.updateIsCaretVisible(); + } + } + }, 0); + + const funcs = {}; + + funcs.setInitialCursor = () => { + if (!CaretBrowsing.initiated) { + CaretBrowsing.setInitialCursor(); + return; + } + + if (window.getSelection().toString().length === 0) { + positionCaret(); + } + CaretBrowsing.toggle(); + }; + + funcs.setPlatform = (platform) => { + CaretBrowsing.isWindows = platform.startsWith("win"); + }; + + funcs.disableCaret = () => { + CaretBrowsing.toggle(false); + }; + + funcs.toggle = () => { + CaretBrowsing.toggle(); + }; + + funcs.moveRight = () => { + CaretBrowsing.move("right", "character"); + }; + + funcs.moveLeft = () => { + CaretBrowsing.move("left", "character"); + }; + + funcs.moveDown = () => { + CaretBrowsing.move("forward", "line"); + }; + + funcs.moveUp = () => { + CaretBrowsing.move("backward", "line"); + }; + + funcs.moveToEndOfWord = () => { + funcs.moveToNextWord(); + funcs.moveLeft(); + }; + + funcs.moveToNextWord = () => { + CaretBrowsing.move("forward", "word"); + funcs.moveRight(); + }; + + funcs.moveToPreviousWord = () => { + CaretBrowsing.move("backward", "word"); + }; + + funcs.moveToStartOfLine = () => { + CaretBrowsing.move("left", "lineboundary"); + }; + + funcs.moveToEndOfLine = () => { + CaretBrowsing.move("right", "lineboundary"); + }; + + funcs.moveToStartOfNextBlock = () => { + CaretBrowsing.moveToBlock("forward", "backward"); + }; + + funcs.moveToStartOfPrevBlock = () => { + CaretBrowsing.moveToBlock("backward", "backward"); + }; + + funcs.moveToEndOfNextBlock = () => { + CaretBrowsing.moveToBlock("forward", "forward"); + }; + + funcs.moveToEndOfPrevBlock = () => { + CaretBrowsing.moveToBlock("backward", "forward"); + }; + + funcs.moveToStartOfDocument = () => { + CaretBrowsing.move("backward", "documentboundary"); + }; + + funcs.moveToEndOfDocument = () => { + CaretBrowsing.move("forward", "documentboundary"); + funcs.moveLeft(); + }; + + funcs.dropSelection = () => { + window.getSelection().removeAllRanges(); + }; + + funcs.getSelection = () => window.getSelection().toString(); + + funcs.toggleSelection = () => { + CaretBrowsing.selectionEnabled = !CaretBrowsing.selectionEnabled; + }; + + return funcs; +})(); diff --git a/qutebrowser/javascript/webelem.js b/qutebrowser/javascript/webelem.js index b5fd936b2..eb6ce2790 100644 --- a/qutebrowser/javascript/webelem.js +++ b/qutebrowser/javascript/webelem.js @@ -40,7 +40,54 @@ window._qutebrowser.webelem = (function() { const funcs = {}; const elements = []; - function serialize_elem(elem) { + function get_frame_offset(frame) { + if (frame === null) { + // Dummy object with zero offset + return { + "top": 0, + "right": 0, + "bottom": 0, + "left": 0, + "height": 0, + "width": 0, + }; + } + return frame.frameElement.getBoundingClientRect(); + } + + // Add an offset rect to a base rect, for use with frames + function add_offset_rect(base, offset) { + return { + "top": base.top + offset.top, + "left": base.left + offset.left, + "bottom": base.bottom + offset.top, + "right": base.right + offset.left, + "height": base.height, + "width": base.width, + }; + } + + function get_caret_position(elem, frame) { + // With older Chromium versions (and QtWebKit), InvalidStateError will + // be thrown if elem doesn't have selectionStart. + // With newer Chromium versions (>= Qt 5.10), we get null. + try { + return elem.selectionStart; + } catch (err) { + if (err instanceof (frame + ? frame.DOMException + : DOMException) && + err.name === "InvalidStateError") { + // nothing to do, caret_position is already null + } else { + // not the droid we're looking for + throw err; + } + } + return null; + } + + function serialize_elem(elem, frame = null) { if (!elem) { return null; } @@ -48,31 +95,18 @@ window._qutebrowser.webelem = (function() { const id = elements.length; elements[id] = elem; - // With older Chromium versions (and QtWebKit), InvalidStateError will - // be thrown if elem doesn't have selectionStart. - // With newer Chromium versions (>= Qt 5.10), we get null. - let caret_position = null; - try { - caret_position = elem.selectionStart; - } catch (err) { - if (err instanceof DOMException && - err.name === "InvalidStateError") { - // nothing to do, caret_position is already null - } else { - // not the droid we're looking for - throw err; - } - } + const caret_position = get_caret_position(elem, frame); const out = { "id": id, - "value": elem.value, - "outer_xml": elem.outerHTML, "rects": [], // Gets filled up later "caret_position": caret_position, }; + // Deal with various fun things which can happen in form elements // https://github.com/qutebrowser/qutebrowser/issues/2569 + // https://github.com/qutebrowser/qutebrowser/issues/2877 + // https://stackoverflow.com/q/22942689/2085149 if (typeof elem.tagName === "string") { out.tag_name = elem.tagName; } else if (typeof elem.nodeName === "string") { @@ -88,6 +122,18 @@ window._qutebrowser.webelem = (function() { out.class_name = ""; } + if (typeof elem.value === "string" || typeof elem.value === "number") { + out.value = elem.value; + } else { + out.value = ""; + } + + if (typeof elem.outerHTML === "string") { + out.outer_xml = elem.outerHTML; + } else { + out.outer_xml = ""; + } + if (typeof elem.textContent === "string") { out.text = elem.textContent; } else if (typeof elem.text === "string") { @@ -102,16 +148,13 @@ window._qutebrowser.webelem = (function() { out.attributes = attributes; const client_rects = elem.getClientRects(); + const frame_offset_rect = get_frame_offset(frame); + for (let k = 0; k < client_rects.length; ++k) { const rect = client_rects[k]; - out.rects.push({ - "top": rect.top, - "right": rect.right, - "bottom": rect.bottom, - "left": rect.left, - "height": rect.height, - "width": rect.width, - }); + out.rects.push( + add_offset_rect(rect, frame_offset_rect) + ); } // console.log(JSON.stringify(out)); @@ -119,9 +162,7 @@ window._qutebrowser.webelem = (function() { return out; } - function is_visible(elem) { - // FIXME:qtwebengine Handle frames and iframes - + function is_visible(elem, frame = null) { // Adopted from vimperator: // https://github.com/vimperator/vimperator-labs/blob/vimperator-3.14.0/common/content/hints.js#L259-L285 // FIXME:qtwebengine we might need something more sophisticated like @@ -129,7 +170,8 @@ window._qutebrowser.webelem = (function() { // https://github.com/1995eaton/chromium-vim/blob/1.2.85/content_scripts/dom.js#L74-L134 const win = elem.ownerDocument.defaultView; - let rect = elem.getBoundingClientRect(); + const offset_rect = get_frame_offset(frame); + let rect = add_offset_rect(elem.getBoundingClientRect(), offset_rect); if (!rect || rect.top > window.innerHeight || @@ -161,8 +203,20 @@ window._qutebrowser.webelem = (function() { return true; } + // Returns true if the iframe is accessible without + // cross domain errors, else false. + function iframe_same_domain(frame) { + try { + frame.document; // eslint-disable-line no-unused-expressions + return true; + } catch (err) { + return false; + } + } + funcs.find_css = (selector, only_visible) => { const elems = document.querySelectorAll(selector); + const subelem_frames = window.frames; const out = []; for (let i = 0; i < elems.length; ++i) { @@ -171,14 +225,70 @@ window._qutebrowser.webelem = (function() { } } + // Recurse into frames and add them + for (let i = 0; i < subelem_frames.length; i++) { + if (iframe_same_domain(subelem_frames[i])) { + const frame = subelem_frames[i]; + const subelems = frame.document. + querySelectorAll(selector); + for (let elem_num = 0; elem_num < subelems.length; ++elem_num) { + if (!only_visible || + is_visible(subelems[elem_num], frame)) { + out.push(serialize_elem(subelems[elem_num], frame)); + } + } + } + } + return out; }; + // Runs a function in a frame until the result is not null, then return + function run_frames(func) { + for (let i = 0; i < window.frames.length; ++i) { + const frame = window.frames[i]; + if (iframe_same_domain(frame)) { + const result = func(frame); + if (result) { + return result; + } + } + } + return null; + } + funcs.find_id = (id) => { const elem = document.getElementById(id); - return serialize_elem(elem); + if (elem) { + return serialize_elem(elem); + } + + const serialized_elem = run_frames((frame) => { + const element = frame.window.document.getElementById(id); + return serialize_elem(element, frame); + }); + + if (serialized_elem) { + return serialized_elem; + } + + return null; }; + // Check if elem is an iframe, and if so, return the result of func on it. + // If no iframes match, return null + function call_if_frame(elem, func) { + // Check if elem is a frame, and if so, call func on the window + if ("contentWindow" in elem) { + const frame = elem.contentWindow; + if (iframe_same_domain(frame) && + "frameElement" in elem.contentWindow) { + return func(frame); + } + } + return null; + } + funcs.find_focused = () => { const elem = document.activeElement; @@ -188,26 +298,52 @@ window._qutebrowser.webelem = (function() { return null; } + // Check if we got an iframe, and if so, recurse inside of it + const frame_elem = call_if_frame(elem, + (frame) => serialize_elem(frame.document.activeElement, frame)); + + if (frame_elem !== null) { + return frame_elem; + } return serialize_elem(elem); }; funcs.find_at_pos = (x, y) => { - // FIXME:qtwebengine - // If the element at the specified point belongs to another document - // (for example, an iframe's subdocument), the subdocument's parent - // element is returned (the iframe itself). - const elem = document.elementFromPoint(x, y); + + + // Check if we got an iframe, and if so, recurse inside of it + const frame_elem = call_if_frame(elem, + (frame) => { + // Subtract offsets due to being in an iframe + const frame_offset_rect = + frame.frameElement.getBoundingClientRect(); + return serialize_elem(frame.document. + elementFromPoint(x - frame_offset_rect.left, + y - frame_offset_rect.top), frame); + }); + + if (frame_elem !== null) { + return frame_elem; + } return serialize_elem(elem); }; // Function for returning a selection to python (so we can click it) funcs.find_selected_link = () => { - const elem = window.getSelection().anchorNode; - if (!elem) { - return null; + const elem = window.getSelection().baseNode; + if (elem) { + return serialize_elem(elem.parentNode); } - return serialize_elem(elem.parentNode); + + const serialized_frame_elem = run_frames((frame) => { + const node = frame.window.getSelection().baseNode; + if (node) { + return serialize_elem(node.parentNode, frame); + } + return null; + }); + return serialized_frame_elem; }; funcs.set_value = (id, value) => { diff --git a/qutebrowser/keyinput/__init__.py b/qutebrowser/keyinput/__init__.py index c6b95b8a9..1cff4943b 100644 --- a/qutebrowser/keyinput/__init__.py +++ b/qutebrowser/keyinput/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index a2e07cb67..1052c0eeb 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/keyinput/keyparser.py b/qutebrowser/keyinput/keyparser.py index 3df8ab193..aab92bdb0 100644 --- a/qutebrowser/keyinput/keyparser.py +++ b/qutebrowser/keyinput/keyparser.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/keyinput/macros.py b/qutebrowser/keyinput/macros.py index 9e3667590..08740d80d 100644 --- a/qutebrowser/keyinput/macros.py +++ b/qutebrowser/keyinput/macros.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016-2017 Jan Verbeek (blyxxyz) +# Copyright 2016-2018 Jan Verbeek (blyxxyz) # # This file is part of qutebrowser. # diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index ad9bd06ee..e32830f50 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -29,7 +29,6 @@ from qutebrowser.keyinput import modeparsers, keyparser from qutebrowser.config import config from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.utils import usertypes, log, objreg, utils -from qutebrowser.misc import objects @attr.s(frozen=True) @@ -267,10 +266,6 @@ class ModeManager(QObject): usertypes.KeyMode.yesno, usertypes.KeyMode.prompt]: raise cmdexc.CommandError( "Mode {} can't be entered manually!".format(mode)) - elif (m == usertypes.KeyMode.caret and - objects.backend == usertypes.Backend.QtWebEngine): - raise cmdexc.CommandError("Caret mode is not supported with " - "QtWebEngine yet.") self.enter(m, 'command') @@ -322,10 +317,13 @@ class ModeManager(QObject): if self.mode is None: # We got events before mode is set, so just pass them through. return False - if event.type() == QEvent.KeyPress: - return self._eventFilter_keypress(event) - else: - return self._eventFilter_keyrelease(event) + + handlers = { + QEvent.KeyPress: self._eventFilter_keypress, + QEvent.KeyRelease: self._eventFilter_keyrelease, + } + handler = handlers[event.type()] + return handler(event) @cmdutils.register(instance='mode-manager', scope='window') def clear_keychain(self): diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 86e03877a..2e23e2aa5 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/mainwindow/__init__.py b/qutebrowser/mainwindow/__init__.py index 43eb563a9..1b76e9b5a 100644 --- a/qutebrowser/mainwindow/__init__.py +++ b/qutebrowser/mainwindow/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index 3adce7567..05482a1d5 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -30,12 +30,11 @@ from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QSizePolicy from qutebrowser.commands import runners, cmdutils from qutebrowser.config import config, configfiles from qutebrowser.utils import (message, log, usertypes, qtutils, objreg, utils, - jinja, debug) + jinja) from qutebrowser.mainwindow import messageview, prompt from qutebrowser.completion import completionwidget, completer from qutebrowser.keyinput import modeman -from qutebrowser.browser import (commands, downloadview, hints, - qtnetworkdownloads, downloads) +from qutebrowser.browser import commands, downloadview, hints, downloads from qutebrowser.misc import crashsignal, keyhintwidget @@ -94,17 +93,18 @@ def get_window(via_ipc, force_window=False, force_tab=False, return window.win_id -def raise_window(window): +def raise_window(window, alert=True): """Raise the given MainWindow object.""" window.setWindowState(window.windowState() & ~Qt.WindowMinimized) window.setWindowState(window.windowState() | Qt.WindowActive) window.raise_() window.activateWindow() - QApplication.instance().alert(window) + + if alert: + QApplication.instance().alert(window) -# WORKAROUND for https://github.com/PyCQA/pylint/issues/1770 -def get_target_window(): # pylint: disable=inconsistent-return-statements +def get_target_window(): """Get the target window for new tabs, or None if none exist.""" try: win_mode = config.val.new_instance_open_target_window @@ -132,7 +132,6 @@ class MainWindow(QWidget): Attributes: status: The StatusBar widget. tabbed_browser: The TabbedBrowser widget. - state_before_fullscreen: window state before activation of fullscreen. _downloadview: The DownloadView widget. _vbox: The main QVBoxLayout. _commandrunner: The main CommandRunner instance. @@ -232,8 +231,6 @@ class MainWindow(QWidget): objreg.get("app").new_window.emit(self) - self.state_before_fullscreen = self.windowState() - def _init_geometry(self, geometry): """Initialize the window geometry or load it from disk.""" if geometry is not None: @@ -301,11 +298,7 @@ class MainWindow(QWidget): def _init_downloadmanager(self): log.init.debug("Initializing downloads...") - qtnetwork_download_manager = qtnetworkdownloads.DownloadManager( - self.win_id, self) - objreg.register('qtnetwork-download-manager', - qtnetwork_download_manager, - scope='window', window=self.win_id) + qtnetwork_download_manager = objreg.get('qtnetwork-download-manager') try: webengine_download_manager = objreg.get( @@ -430,7 +423,6 @@ class MainWindow(QWidget): status = self._get_object('statusbar') keyparsers = self._get_object('keyparsers') completion_obj = self._get_object('completion') - tabs = self._get_object('tabbed-browser') cmd = self._get_object('status-command') message_bridge = self._get_object('message-bridge') mode_manager = self._get_object('mode-manager') @@ -450,7 +442,7 @@ class MainWindow(QWidget): status.keystring.setText) cmd.got_cmd[str].connect(self._commandrunner.run_safely) cmd.got_cmd[str, int].connect(self._commandrunner.run_safely) - cmd.returnPressed.connect(tabs.on_cmd_return_pressed) + cmd.returnPressed.connect(self.tabbed_browser.on_cmd_return_pressed) # key hint popup for mode, parser in keyparsers.items(): @@ -468,25 +460,31 @@ class MainWindow(QWidget): message_bridge.s_maybe_reset_text.connect(status.txt.maybe_reset_text) # statusbar - tabs.current_tab_changed.connect(status.on_tab_changed) + self.tabbed_browser.current_tab_changed.connect(status.on_tab_changed) - tabs.cur_progress.connect(status.prog.setValue) - tabs.cur_load_finished.connect(status.prog.hide) - tabs.cur_load_started.connect(status.prog.on_load_started) + self.tabbed_browser.cur_progress.connect(status.prog.setValue) + self.tabbed_browser.cur_load_finished.connect(status.prog.hide) + self.tabbed_browser.cur_load_started.connect( + status.prog.on_load_started) - tabs.cur_scroll_perc_changed.connect(status.percentage.set_perc) - tabs.tab_index_changed.connect(status.tabindex.on_tab_index_changed) + self.tabbed_browser.cur_scroll_perc_changed.connect( + status.percentage.set_perc) + self.tabbed_browser.tab_index_changed.connect( + status.tabindex.on_tab_index_changed) - tabs.cur_url_changed.connect(status.url.set_url) - tabs.cur_url_changed.connect(functools.partial( - status.backforward.on_tab_cur_url_changed, tabs=tabs)) - tabs.cur_link_hovered.connect(status.url.set_hover_url) - tabs.cur_load_status_changed.connect(status.url.on_load_status_changed) - tabs.cur_fullscreen_requested.connect(self._on_fullscreen_requested) - tabs.cur_fullscreen_requested.connect(status.maybe_hide) + self.tabbed_browser.cur_url_changed.connect(status.url.set_url) + self.tabbed_browser.cur_url_changed.connect(functools.partial( + status.backforward.on_tab_cur_url_changed, + tabs=self.tabbed_browser)) + self.tabbed_browser.cur_link_hovered.connect(status.url.set_hover_url) + self.tabbed_browser.cur_load_status_changed.connect( + status.url.on_load_status_changed) + self.tabbed_browser.cur_fullscreen_requested.connect( + self._on_fullscreen_requested) + self.tabbed_browser.cur_fullscreen_requested.connect(status.maybe_hide) # command input / completion - mode_manager.left.connect(tabs.on_mode_left) + mode_manager.left.connect(self.tabbed_browser.on_mode_left) cmd.clear_completion_selection.connect( completion_obj.on_clear_completion_selection) cmd.hide_completion.connect(completion_obj.hide) @@ -495,12 +493,9 @@ class MainWindow(QWidget): def _on_fullscreen_requested(self, on): if not config.val.content.windowed_fullscreen: if on: - self.state_before_fullscreen = self.windowState() - self.showFullScreen() + self.setWindowState(self.windowState() | Qt.WindowFullScreen) elif self.isFullScreen(): - self.setWindowState(self.state_before_fullscreen) - log.misc.debug('on: {}, state before fullscreen: {}'.format( - on, debug.qflags_key(Qt, self.state_before_fullscreen))) + self.setWindowState(self.windowState() & ~Qt.WindowFullScreen) @cmdutils.register(instance='main-window', scope='window') @pyqtSlot() diff --git a/qutebrowser/mainwindow/messageview.py b/qutebrowser/mainwindow/messageview.py index 5e389e23c..43ddd5248 100644 --- a/qutebrowser/mainwindow/messageview.py +++ b/qutebrowser/mainwindow/messageview.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016-2017 Florian Bruhin (The Compiler) +# Copyright 2016-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 3d21a52e0..931d32654 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016-2017 Florian Bruhin (The Compiler) +# Copyright 2016-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -422,6 +422,27 @@ class PromptContainer(QWidget): except UnsupportedOperationError: pass + @cmdutils.register( + instance='prompt-container', scope='window', + modes=[usertypes.KeyMode.prompt, usertypes.KeyMode.yesno]) + def prompt_yank(self, sel=False): + """Yank URL to clipboard or primary selection. + + Args: + sel: Use the primary selection instead of the clipboard. + """ + question = self._prompt.question + if question.url is None: + message.error('No URL found.') + return + if sel and utils.supports_selection(): + target = 'primary selection' + else: + sel = False + target = 'clipboard' + utils.set_clipboard(question.url, sel) + message.info("Yanked to {}: {}".format(target, question.url)) + class LineEdit(QLineEdit): @@ -721,6 +742,7 @@ class DownloadFilenamePrompt(FilenamePrompt): ('prompt-accept', 'Accept'), ('leave-mode', 'Abort'), ('prompt-open-download', "Open download"), + ('prompt-yank', "Yank URL"), ] return cmds @@ -811,6 +833,7 @@ class YesNoPrompt(_BasePrompt): cmds = [ ('prompt-accept yes', "Yes"), ('prompt-accept no', "No"), + ('prompt-yank', "Yank URL"), ] if self.question.default is not None: diff --git a/qutebrowser/mainwindow/statusbar/__init__.py b/qutebrowser/mainwindow/statusbar/__init__.py index eb3ed7193..eee7cf990 100644 --- a/qutebrowser/mainwindow/statusbar/__init__.py +++ b/qutebrowser/mainwindow/statusbar/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/mainwindow/statusbar/backforward.py b/qutebrowser/mainwindow/statusbar/backforward.py index 3a566e105..8ea60ee75 100644 --- a/qutebrowser/mainwindow/statusbar/backforward.py +++ b/qutebrowser/mainwindow/statusbar/backforward.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2017 Florian Bruhin (The Compiler) +# Copyright 2017-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -26,6 +26,10 @@ class Backforward(textbase.TextBase): """Shows navigation indicator (if you can go backward and/or forward).""" + def __init__(self, parent=None): + super().__init__(parent) + self.enabled = False + def on_tab_cur_url_changed(self, tabs): """Called on URL changes.""" tab = tabs.currentWidget() @@ -45,4 +49,4 @@ class Backforward(textbase.TextBase): if text: text = '[' + text + ']' self.setText(text) - self.setVisible(bool(text)) + self.setVisible(bool(text) and self.enabled) diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index ae7a3954d..8057bfdb8 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -182,26 +182,13 @@ class StatusBar(QWidget): self.cmd.hide_cmd.connect(self._hide_cmd_widget) self._hide_cmd_widget() - self.keystring = keystring.KeyString() - self._hbox.addWidget(self.keystring) - self.url = url.UrlText() - self._hbox.addWidget(self.url) - self.percentage = percentage.Percentage() - self._hbox.addWidget(self.percentage) - self.backforward = backforward.Backforward() - self._hbox.addWidget(self.backforward) - self.tabindex = tabindex.TabIndex() - self._hbox.addWidget(self.tabindex) - - # We add a parent to Progress here because it calls self.show() based - # on some signals, and if that happens before it's added to the layout, - # it will quickly blink up as independent window. + self.keystring = keystring.KeyString() self.prog = progress.Progress(self) - self._hbox.addWidget(self.prog) + self._draw_widgets() config.instance.changed.connect(self._on_config_changed) QTimer.singleShot(0, self.maybe_hide) @@ -215,6 +202,48 @@ class StatusBar(QWidget): self.maybe_hide() elif option == 'statusbar.padding': self._set_hbox_padding() + elif option == 'statusbar.widgets': + self._draw_widgets() + + def _draw_widgets(self): + """Draw statusbar widgets.""" + # Start with widgets hidden and show them when needed + for widget in [self.url, self.percentage, + self.backforward, self.tabindex, + self.keystring, self.prog]: + widget.hide() + self._hbox.removeWidget(widget) + + tab = self._current_tab() + + # Read the list and set widgets accordingly + for segment in config.val.statusbar.widgets: + if segment == 'url': + self._hbox.addWidget(self.url) + self.url.show() + elif segment == 'scroll': + self._hbox.addWidget(self.percentage) + self.percentage.show() + elif segment == 'scroll_raw': + self._hbox.addWidget(self.percentage) + self.percentage.raw = True + self.percentage.show() + elif segment == 'history': + self._hbox.addWidget(self.backforward) + self.backforward.enabled = True + if tab: + self.backforward.on_tab_changed(tab) + elif segment == 'tabs': + self._hbox.addWidget(self.tabindex) + self.tabindex.show() + elif segment == 'keypress': + self._hbox.addWidget(self.keystring) + self.keystring.show() + elif segment == 'progress': + self._hbox.addWidget(self.prog) + self.prog.enabled = True + if tab: + self.prog.on_tab_changed(tab) @pyqtSlot() def maybe_hide(self): diff --git a/qutebrowser/mainwindow/statusbar/command.py b/qutebrowser/mainwindow/statusbar/command.py index ab8ac7b5d..681e20ac5 100644 --- a/qutebrowser/mainwindow/statusbar/command.py +++ b/qutebrowser/mainwindow/statusbar/command.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -193,7 +193,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit): if run: self.command_accept() - ed.editing_finished.connect(callback) + ed.file_updated.connect(callback) ed.edit(self.text()) @pyqtSlot(usertypes.KeyMode) @@ -232,6 +232,12 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit): Enter/Shift+Enter/etc. will cause QLineEdit to think it's finished without command_accept to be called. """ + text = self.text() + if text in modeparsers.STARTCHARS and e.key() == Qt.Key_Backspace: + e.accept() + modeman.leave(self._win_id, usertypes.KeyMode.command, + 'prefix deleted') + return if e.key() == Qt.Key_Return: e.ignore() return diff --git a/qutebrowser/mainwindow/statusbar/keystring.py b/qutebrowser/mainwindow/statusbar/keystring.py index dd9825ab2..29dd3b790 100644 --- a/qutebrowser/mainwindow/statusbar/keystring.py +++ b/qutebrowser/mainwindow/statusbar/keystring.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/mainwindow/statusbar/percentage.py b/qutebrowser/mainwindow/statusbar/percentage.py index 0d75e8163..a362fd9d6 100644 --- a/qutebrowser/mainwindow/statusbar/percentage.py +++ b/qutebrowser/mainwindow/statusbar/percentage.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -32,6 +32,7 @@ class Percentage(textbase.TextBase): """Constructor. Set percentage to 0%.""" super().__init__(parent) self.set_perc(0, 0) + self.raw = False @pyqtSlot(int, int) def set_perc(self, x, y): # pylint: disable=unused-argument @@ -48,7 +49,8 @@ class Percentage(textbase.TextBase): elif y is None: self.setText('[???]') else: - self.setText('[{:2}%]'.format(y)) + text = '[{:02}]' if self.raw else '[{:02}%]' + self.setText(text.format(y)) def on_tab_changed(self, tab): """Update scroll position when tab changed.""" diff --git a/qutebrowser/mainwindow/statusbar/progress.py b/qutebrowser/mainwindow/statusbar/progress.py index c8707e0b1..6c467150b 100644 --- a/qutebrowser/mainwindow/statusbar/progress.py +++ b/qutebrowser/mainwindow/statusbar/progress.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -46,6 +46,7 @@ class Progress(QProgressBar): def __init__(self, parent=None): super().__init__(parent) config.set_register_stylesheet(self) + self.enabled = False self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.setTextVisible(False) self.hide() @@ -57,12 +58,12 @@ class Progress(QProgressBar): def on_load_started(self): """Clear old error and show progress, used as slot to loadStarted.""" self.setValue(0) - self.show() + self.setVisible(self.enabled) def on_tab_changed(self, tab): """Set the correct value when the current tab changed.""" self.setValue(tab.progress()) - if tab.load_status() == usertypes.LoadStatus.loading: + if self.enabled and tab.load_status() == usertypes.LoadStatus.loading: self.show() else: self.hide() diff --git a/qutebrowser/mainwindow/statusbar/tabindex.py b/qutebrowser/mainwindow/statusbar/tabindex.py index 6a4cc987c..47a775f34 100644 --- a/qutebrowser/mainwindow/statusbar/tabindex.py +++ b/qutebrowser/mainwindow/statusbar/tabindex.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2017 Florian Bruhin (The Compiler) +# Copyright 2015-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/mainwindow/statusbar/text.py b/qutebrowser/mainwindow/statusbar/text.py index b232cedc8..0a57446f1 100644 --- a/qutebrowser/mainwindow/statusbar/text.py +++ b/qutebrowser/mainwindow/statusbar/text.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/mainwindow/statusbar/textbase.py b/qutebrowser/mainwindow/statusbar/textbase.py index 0ae271191..5c157e52a 100644 --- a/qutebrowser/mainwindow/statusbar/textbase.py +++ b/qutebrowser/mainwindow/statusbar/textbase.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/mainwindow/statusbar/url.py b/qutebrowser/mainwindow/statusbar/url.py index a91c67550..f24e79834 100644 --- a/qutebrowser/mainwindow/statusbar/url.py +++ b/qutebrowser/mainwindow/statusbar/url.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index c51a7aaff..299a5fb08 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -253,10 +253,15 @@ class TabbedBrowser(tabwidget.TabWidget): def shutdown(self): """Try to shut down all tabs cleanly.""" self.shutting_down = True - for tab in self.widgets(): + # Reverse tabs so we don't have to recacluate tab titles over and over + # Removing first causes [2..-1] to be recomputed + # Removing the last causes nothing to be recomputed + for tab in reversed(self.widgets()): self._remove_tab(tab) - def tab_close_prompt_if_pinned(self, tab, force, yes_action): + def tab_close_prompt_if_pinned( + self, tab, force, yes_action, + text="Are you sure you want to close a pinned tab?"): """Helper method for tab_close. If tab is pinned, prompt. If not, run yes_action. @@ -265,7 +270,7 @@ class TabbedBrowser(tabwidget.TabWidget): if tab.data.pinned and not force: message.confirm_async( title='Pinned Tab', - text="Are you sure you want to close a pinned tab?", + text=text, yes_action=yes_action, default=False, abort_on=[tab.destroyed]) else: yes_action() @@ -373,12 +378,10 @@ class TabbedBrowser(tabwidget.TabWidget): for entry in reversed(self._undo_stack.pop()): if use_current_tab: - self.openurl(entry.url, newtab=False) newtab = self.widget(0) use_current_tab = False else: - newtab = self.tabopen(entry.url, background=False, - idx=entry.index) + newtab = self.tabopen(background=False, idx=entry.index) newtab.history.deserialize(entry.history) self.set_tab_pinned(newtab, entry.pinned) @@ -641,6 +644,9 @@ class TabbedBrowser(tabwidget.TabWidget): @pyqtSlot(int) def on_current_changed(self, idx): """Set last-focused-tab and leave hinting mode when focus changed.""" + mode_on_change = config.val.tabs.mode_on_change + modes_to_save = [usertypes.KeyMode.insert, + usertypes.KeyMode.passthrough] if idx == -1 or self.shutting_down: # closing the last tab (before quitting) or shutting down return @@ -649,17 +655,23 @@ class TabbedBrowser(tabwidget.TabWidget): log.webview.debug("on_current_changed got called with invalid " "index {}".format(idx)) return + if self._now_focused is not None and mode_on_change == 'restore': + current_mode = modeman.instance(self._win_id).mode + if current_mode not in modes_to_save: + current_mode = usertypes.KeyMode.normal + self._now_focused.data.input_mode = current_mode log.modes.debug("Current tab changed, focusing {!r}".format(tab)) tab.setFocus() modes_to_leave = [usertypes.KeyMode.hint, usertypes.KeyMode.caret] - if not config.val.tabs.persist_mode_on_change: - modes_to_leave += [usertypes.KeyMode.insert, - usertypes.KeyMode.passthrough] + if mode_on_change != 'persist': + modes_to_leave += modes_to_save for mode in modes_to_leave: modeman.leave(self._win_id, mode, 'tab changed', maybe=True) - + if mode_on_change == 'restore': + modeman.enter(self._win_id, tab.data.input_mode, + 'restore input mode for tab') if self._now_focused is not None: objreg.register('last-focused-tab', self._now_focused, update=True, scope='window', window=self._win_id) diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index 28cfac0fb..965e5b219 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -110,8 +110,6 @@ class TabWidget(QTabWidget): tab.data.pinned = pinned self._update_tab_title(idx) - bar.refresh() - def tab_indicator_color(self, idx): """Get the tab indicator color for the given index.""" return self.tabBar().tab_indicator_color(idx) @@ -148,7 +146,11 @@ class TabWidget(QTabWidget): fields['index'] = idx + 1 title = '' if fmt is None else fmt.format(**fields) - self.tabBar().setTabText(idx, title) + tabbar = self.tabBar() + + if tabbar.tabText(idx) != title: + tabbar.setTabText(idx, title) + tabbar.setTabToolTip(idx, title) def get_tab_fields(self, idx): """Get the tab field data.""" diff --git a/qutebrowser/misc/__init__.py b/qutebrowser/misc/__init__.py index 03ad27aa8..2be43490a 100644 --- a/qutebrowser/misc/__init__.py +++ b/qutebrowser/misc/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/autoupdate.py b/qutebrowser/misc/autoupdate.py index 15a3b6670..d056d61b6 100644 --- a/qutebrowser/misc/autoupdate.py +++ b/qutebrowser/misc/autoupdate.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/backendproblem.py b/qutebrowser/misc/backendproblem.py index 8cc5b71b2..d6ac36d10 100644 --- a/qutebrowser/misc/backendproblem.py +++ b/qutebrowser/misc/backendproblem.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2017 Florian Bruhin (The Compiler) +# Copyright 2017-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -191,7 +191,7 @@ def _handle_nouveau_graphics(): text="

There are two ways to fix this:

" "

Forcing software rendering

" "

This allows you to use the newer QtWebEngine backend (based " - "on Chromium) but could have noticable performance impact " + "on Chromium) but could have noticeable performance impact " "(depending on your hardware). " "This sets the qt.force_software_rendering = True option " "(if you have a config.py file, you'll need to set this " diff --git a/qutebrowser/misc/checkpyver.py b/qutebrowser/misc/checkpyver.py index da71b278d..50330ef88 100644 --- a/qutebrowser/misc/checkpyver.py +++ b/qutebrowser/misc/checkpyver.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The-Compiler) +# Copyright 2014-2018 Florian Bruhin (The-Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/cmdhistory.py b/qutebrowser/misc/cmdhistory.py index a24f6b816..9fa273c1c 100644 --- a/qutebrowser/misc/cmdhistory.py +++ b/qutebrowser/misc/cmdhistory.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/consolewidget.py b/qutebrowser/misc/consolewidget.py index b5ca8dd78..661c7b805 100644 --- a/qutebrowser/misc/consolewidget.py +++ b/qutebrowser/misc/consolewidget.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/crashdialog.py b/qutebrowser/misc/crashdialog.py index 9ef0e573c..4d395b8ec 100644 --- a/qutebrowser/misc/crashdialog.py +++ b/qutebrowser/misc/crashdialog.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/crashsignal.py b/qutebrowser/misc/crashsignal.py index b8d63ad58..5224c31cc 100644 --- a/qutebrowser/misc/crashsignal.py +++ b/qutebrowser/misc/crashsignal.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py index 10530eecf..c78d0848d 100644 --- a/qutebrowser/misc/earlyinit.py +++ b/qutebrowser/misc/earlyinit.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The-Compiler) +# Copyright 2014-2018 Florian Bruhin (The-Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/editor.py b/qutebrowser/misc/editor.py index 553c700e7..154660001 100644 --- a/qutebrowser/misc/editor.py +++ b/qutebrowser/misc/editor.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -22,7 +22,8 @@ import os import tempfile -from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QProcess +from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QProcess, + QFileSystemWatcher) from qutebrowser.config import config from qutebrowser.utils import message, log @@ -39,19 +40,36 @@ class ExternalEditor(QObject): _remove_file: Whether the file should be removed when the editor is closed. _proc: The GUIProcess of the editor. + _watcher: A QFileSystemWatcher to watch the edited file for changes. + Only set if watch=True. + + Signals: + file_updated: The text in the edited file was updated. + arg: The new text. + editing_finished: The editor process was closed. """ - editing_finished = pyqtSignal(str) + file_updated = pyqtSignal(str) + editing_finished = pyqtSignal() - def __init__(self, parent=None): + def __init__(self, parent=None, watch=False): super().__init__(parent) self._filename = None self._proc = None self._remove_file = None + self._watcher = QFileSystemWatcher(parent=self) if watch else None + self._content = None def _cleanup(self): """Clean up temporary files after the editor closed.""" assert self._remove_file is not None + + watched_files = self._watcher.files() if self._watcher else [] + if watched_files: + failed = self._watcher.removePaths(watched_files) + if failed: + log.procs.error("Failed to unwatch paths: {}".format(failed)) + if self._filename is None or not self._remove_file: # Could not create initial file. return @@ -65,7 +83,7 @@ class ExternalEditor(QObject): message.error("Failed to delete tempfile... ({})".format(e)) @pyqtSlot(int, QProcess.ExitStatus) - def on_proc_closed(self, exitcode, exitstatus): + def on_proc_closed(self, _exitcode, exitstatus): """Write the editor text into the form field and clean up tempfile. Callback for QProcess when the editor was closed. @@ -75,22 +93,10 @@ class ExternalEditor(QObject): # No error/cleanup here, since we already handle this in # on_proc_error. return - try: - if exitcode != 0: - return - encoding = config.val.editor.encoding - try: - with open(self._filename, 'r', encoding=encoding) as f: - text = f.read() - except OSError as e: - # NOTE: Do not replace this with "raise CommandError" as it's - # executed async. - message.error("Failed to read back edited file: {}".format(e)) - return - log.procs.debug("Read back: {}".format(text)) - self.editing_finished.emit(text) - finally: - self._cleanup() + # do a final read to make sure we don't miss the last signal + self._on_file_changed(self._filename) + self.editing_finished.emit() + self._cleanup() @pyqtSlot(QProcess.ProcessError) def on_proc_error(self, _err): @@ -128,6 +134,21 @@ class ExternalEditor(QObject): line, column = self._calc_line_and_column(text, caret_position) self._start_editor(line=line, column=column) + @pyqtSlot(str) + def _on_file_changed(self, path): + try: + with open(path, 'r', encoding=config.val.editor.encoding) as f: + text = f.read() + except OSError as e: + # NOTE: Do not replace this with "raise CommandError" as it's + # executed async. + message.error("Failed to read back edited file: {}".format(e)) + return + log.procs.debug("Read back: {}".format(text)) + if self._content != text: + self._content = text + self.file_updated.emit(text) + def edit_file(self, filename): """Edit the file with the given filename.""" self._filename = filename @@ -147,6 +168,13 @@ class ExternalEditor(QObject): editor = config.val.editor.command executable = editor[0] + if self._watcher: + ok = self._watcher.addPath(self._filename) + if not ok: + log.procs.error("Failed to watch path: {}" + .format(self._filename)) + self._watcher.fileChanged.connect(self._on_file_changed) + args = [self._sub_placeholder(arg, line, column) for arg in editor[1:]] log.procs.debug("Calling \"{}\" with args {}".format(executable, args)) self._proc.start(executable, args) diff --git a/qutebrowser/misc/guiprocess.py b/qutebrowser/misc/guiprocess.py index 384c3ae30..52dc352a7 100644 --- a/qutebrowser/misc/guiprocess.py +++ b/qutebrowser/misc/guiprocess.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2017 Florian Bruhin (The Compiler) +# Copyright 2015-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/httpclient.py b/qutebrowser/misc/httpclient.py index 4d33d487e..b0b41af76 100644 --- a/qutebrowser/misc/httpclient.py +++ b/qutebrowser/misc/httpclient.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/ipc.py b/qutebrowser/misc/ipc.py index c9f982365..8af5ba6bf 100644 --- a/qutebrowser/misc/ipc.py +++ b/qutebrowser/misc/ipc.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/keyhintwidget.py b/qutebrowser/misc/keyhintwidget.py index 7e6791d4e..0aa52116e 100644 --- a/qutebrowser/misc/keyhintwidget.py +++ b/qutebrowser/misc/keyhintwidget.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016-2017 Ryan Roden-Corrent (rcorre) +# Copyright 2016-2018 Ryan Roden-Corrent (rcorre) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/lineparser.py b/qutebrowser/misc/lineparser.py index 155cbd1b0..6e50edb9b 100644 --- a/qutebrowser/misc/lineparser.py +++ b/qutebrowser/misc/lineparser.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/miscwidgets.py b/qutebrowser/misc/miscwidgets.py index c52600bb1..0e3def2f9 100644 --- a/qutebrowser/misc/miscwidgets.py +++ b/qutebrowser/misc/miscwidgets.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/msgbox.py b/qutebrowser/misc/msgbox.py index 459ab8b61..053534158 100644 --- a/qutebrowser/misc/msgbox.py +++ b/qutebrowser/misc/msgbox.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2017 Florian Bruhin (The Compiler) +# Copyright 2015-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/objects.py b/qutebrowser/misc/objects.py index 60d764620..d6c116eab 100644 --- a/qutebrowser/misc/objects.py +++ b/qutebrowser/misc/objects.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2017 Florian Bruhin (The Compiler) +# Copyright 2017-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/pastebin.py b/qutebrowser/misc/pastebin.py index 9609481b6..0f2ed8ce4 100644 --- a/qutebrowser/misc/pastebin.py +++ b/qutebrowser/misc/pastebin.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -42,20 +42,23 @@ class PastebinClient(QObject): """ API_URL = 'https://crashes.qutebrowser.org/api/' + MISC_API_URL = 'https://paste.the-compiler.org/api/' success = pyqtSignal(str) error = pyqtSignal(str) - def __init__(self, client, parent=None): + def __init__(self, client, parent=None, api_url=API_URL): """Constructor. Args: client: The HTTPClient to use. Will be reparented. + api_url: The Stikked pastebin endpoint to use. """ super().__init__(parent) client.setParent(self) client.error.connect(self.error) client.success.connect(self.on_client_success) self._client = client + self._api_url = api_url def paste(self, name, title, text, parent=None): """Paste the text into a pastebin and return the URL. @@ -74,7 +77,7 @@ class PastebinClient(QObject): } if parent is not None: data['reply'] = parent - url = QUrl(urllib.parse.urljoin(self.API_URL, 'create')) + url = QUrl(urllib.parse.urljoin(self._api_url, 'create')) self._client.post(url, data) @pyqtSlot(str) diff --git a/qutebrowser/misc/readline.py b/qutebrowser/misc/readline.py index 51ce6bb94..3846b77e0 100644 --- a/qutebrowser/misc/readline.py +++ b/qutebrowser/misc/readline.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/savemanager.py b/qutebrowser/misc/savemanager.py index 02001902c..0d79c97db 100644 --- a/qutebrowser/misc/savemanager.py +++ b/qutebrowser/misc/savemanager.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2017 Florian Bruhin (The Compiler) +# Copyright 2015-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -77,7 +77,7 @@ class Saveable: Args: is_exit: Whether we're currently exiting qutebrowser. explicit: Whether the user explicitly requested this save. - silent: Don't write informations to log. + silent: Don't write information to log. force: Force saving, no matter what. """ if (self._config_opt is not None and @@ -157,7 +157,7 @@ class SaveManager(QObject): Args: is_exit: Whether we're currently exiting qutebrowser. explicit: Whether this save operation was triggered explicitly. - silent: Don't write informations to log. Used to reduce log spam + silent: Don't write information to log. Used to reduce log spam when autosaving. force: Force saving, no matter what. """ diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py index b46b5a26a..a8a652dbb 100644 --- a/qutebrowser/misc/sessions.py +++ b/qutebrowser/misc/sessions.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2017 Florian Bruhin (The Compiler) +# Copyright 2015-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -457,7 +457,8 @@ class SessionManager(QObject): @cmdutils.register(instance='session-manager') @cmdutils.argument('name', completion=miscmodels.session) - def session_load(self, name, clear=False, temp=False, force=False): + def session_load(self, name, clear=False, temp=False, force=False, + delete=False): """Load a session. Args: @@ -466,6 +467,7 @@ class SessionManager(QObject): temp: Don't set the current session for :session-save. force: Force loading internal sessions (starting with an underline). + delete: Delete the saved session once it has loaded. """ if name.startswith('_') and not force: raise cmdexc.CommandError("{} is an internal session, use --force " @@ -482,6 +484,17 @@ class SessionManager(QObject): if clear: for win in old_windows: win.close() + if delete: + try: + self.delete(name) + except SessionError as e: + log.sessions.exception("Error while deleting session!") + raise cmdexc.CommandError( + "Error while deleting session: {}" + .format(e)) + else: + log.sessions.debug( + "Loaded & deleted session {}.".format(name)) @cmdutils.register(instance='session-manager') @cmdutils.argument('name', completion=miscmodels.session) diff --git a/qutebrowser/misc/split.py b/qutebrowser/misc/split.py index 2ae8b3792..31738fbcc 100644 --- a/qutebrowser/misc/split.py +++ b/qutebrowser/misc/split.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/sql.py b/qutebrowser/misc/sql.py index 00527e7fe..9b09fb132 100644 --- a/qutebrowser/misc/sql.py +++ b/qutebrowser/misc/sql.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016-2017 Ryan Roden-Corrent (rcorre) +# Copyright 2016-2018 Ryan Roden-Corrent (rcorre) # # This file is part of qutebrowser. # @@ -115,6 +115,11 @@ def init(db_path): raise SqliteError("Failed to open sqlite database at {}: {}" .format(db_path, error.text()), error) + # Enable write-ahead-logging and reduce disk write frequency + # see https://sqlite.org/pragma.html and issues #2930 and #3507 + Query("PRAGMA journal_mode=WAL").run() + Query("PRAGMA synchronous=NORMAL").run() + def close(): """Close the SQL connection.""" diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py index 4054f85bb..d2743d56e 100644 --- a/qutebrowser/misc/utilcmds.py +++ b/qutebrowser/misc/utilcmds.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -39,6 +39,7 @@ from qutebrowser.utils import log, objreg, usertypes, message, debug, utils from qutebrowser.commands import cmdutils, runners, cmdexc from qutebrowser.config import config, configdata from qutebrowser.misc import consolewidget +from qutebrowser.utils.version import pastebin_version @cmdutils.register(maxsplit=1, no_cmd_split=True, no_replace_variables=True) @@ -369,8 +370,15 @@ def nop(): @cmdutils.register() @cmdutils.argument('win_id', win_id=True) -def version(win_id): - """Show version information.""" +def version(win_id, paste=False): + """Show version information. + + Args: + paste: Paste to pastebin. + """ tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) tabbed_browser.openurl(QUrl('qute://version'), newtab=True) + + if paste: + pastebin_version() diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index fc113a5d4..815ffd5a7 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/__init__.py b/qutebrowser/utils/__init__.py index c28a40b10..16069f3ae 100644 --- a/qutebrowser/utils/__init__.py +++ b/qutebrowser/utils/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/debug.py b/qutebrowser/utils/debug.py index 813b02e7a..06bdd2909 100644 --- a/qutebrowser/utils/debug.py +++ b/qutebrowser/utils/debug.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/docutils.py b/qutebrowser/utils/docutils.py index e54effe8d..9ae2039ac 100644 --- a/qutebrowser/utils/docutils.py +++ b/qutebrowser/utils/docutils.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/error.py b/qutebrowser/utils/error.py index 0d045bf19..b9abcdfe1 100644 --- a/qutebrowser/utils/error.py +++ b/qutebrowser/utils/error.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2017 Florian Bruhin (The Compiler) +# Copyright 2015-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/javascript.py b/qutebrowser/utils/javascript.py index f536fed1f..335b1b983 100644 --- a/qutebrowser/utils/javascript.py +++ b/qutebrowser/utils/javascript.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016-2017 Florian Bruhin (The Compiler) +# Copyright 2016-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/jinja.py b/qutebrowser/utils/jinja.py index 802fc5fff..d4ce3368f 100644 --- a/qutebrowser/utils/jinja.py +++ b/qutebrowser/utils/jinja.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index 1a65b6eb8..d6cae1312 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/message.py b/qutebrowser/utils/message.py index 32395b8bd..9dc8d7411 100644 --- a/qutebrowser/utils/message.py +++ b/qutebrowser/utils/message.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -87,7 +87,8 @@ def info(message, *, replace=False): global_bridge.show(usertypes.MessageLevel.info, message, replace) -def _build_question(title, text=None, *, mode, default=None, abort_on=()): +def _build_question(title, text=None, *, mode, default=None, abort_on=(), + url=None): """Common function for ask/ask_async.""" if not isinstance(mode, usertypes.PromptMode): raise TypeError("Mode {} is no PromptMode member!".format(mode)) @@ -96,6 +97,7 @@ def _build_question(title, text=None, *, mode, default=None, abort_on=()): question.text = text question.mode = mode question.default = default + question.url = url for sig in abort_on: sig.connect(question.abort) return question diff --git a/qutebrowser/utils/objreg.py b/qutebrowser/utils/objreg.py index f65e4efab..8d44a9eb5 100644 --- a/qutebrowser/utils/objreg.py +++ b/qutebrowser/utils/objreg.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index 87978274f..8a42fb073 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/standarddir.py b/qutebrowser/utils/standarddir.py index 64f7eb9a8..40f1fa966 100644 --- a/qutebrowser/utils/standarddir.py +++ b/qutebrowser/utils/standarddir.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/urlmatch.py b/qutebrowser/utils/urlmatch.py new file mode 100644 index 000000000..0e83c7420 --- /dev/null +++ b/qutebrowser/utils/urlmatch.py @@ -0,0 +1,277 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2018 Florian Bruhin (The Compiler) +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see . + +"""A Chromium-like URL matching pattern. + +See: +https://developer.chrome.com/apps/match_patterns +https://cs.chromium.org/chromium/src/extensions/common/url_pattern.cc +https://cs.chromium.org/chromium/src/extensions/common/url_pattern.h +""" + +import ipaddress +import fnmatch +import urllib.parse + +from qutebrowser.utils import utils, qtutils + + +class ParseError(Exception): + + """Raised when a pattern could not be parsed.""" + + +class UrlPattern: + + """A Chromium-like URL matching pattern. + + Class attributes: + DEFAULT_PORTS: The default ports used for schemes which support ports. + + Attributes: + _pattern: The given pattern as string. + _match_all: Whether the pattern should match all URLs. + _match_subdomains: Whether the pattern should match subdomains of the + given host. + _scheme: The scheme to match to, or None to match any scheme. + Note that with Chromium, '*'/None only matches http/https and + not file/ftp. We deviate from that as per-URL settings aren't + security relevant. + _host: The host to match to, or None for any host. + _path: The path to match to, or None for any path. + _port: The port to match to as integer, or None for any port. + """ + + DEFAULT_PORTS = {'https': 443, 'http': 80, 'ftp': 21} + + def __init__(self, pattern): + # Make sure all attributes are initialized if we exit early. + self._pattern = pattern + self._match_all = False + self._match_subdomains = False + self._scheme = None + self._host = None + self._path = None + self._port = None + + # > The special pattern matches any URL that starts with a + # > permitted scheme. + if pattern == '': + self._match_all = True + return + + if '\0' in pattern: + raise ParseError("May not contain NUL byte") + + pattern = self._fixup_pattern(pattern) + + # We use urllib.parse instead of QUrl here because it can handle + # hosts with * in them. + try: + parsed = urllib.parse.urlparse(pattern) + except ValueError as e: + raise ParseError(str(e)) + + assert parsed is not None + + self._init_scheme(parsed) + self._init_host(parsed) + self._init_path(parsed) + self._init_port(parsed) + + def _to_tuple(self): + """Get a pattern with information used for __eq__/__hash__.""" + return (self._match_all, self._match_subdomains, self._scheme, + self._host, self._path, self._port) + + def __hash__(self): + return hash(self._to_tuple()) + + def __eq__(self, other): + if not isinstance(other, UrlPattern): + return NotImplemented + # pylint: disable=protected-access + return self._to_tuple() == other._to_tuple() + + def __repr__(self): + return utils.get_repr(self, pattern=self._pattern, constructor=True) + + def __str__(self): + return self._pattern + + def _fixup_pattern(self, pattern): + """Make sure the given pattern is parseable by urllib.parse.""" + if pattern.startswith('*:'): # Any scheme, but *:// is unparseable + pattern = 'any:' + pattern[2:] + + # Chromium handles file://foo like file:///foo + # FIXME This doesn't actually strip the hostname correctly. + if (pattern.startswith('file://') and + not pattern.startswith('file:///')): + pattern = 'file:///' + pattern[len("file://"):] + + return pattern + + def _init_scheme(self, parsed): + if not parsed.scheme: + raise ParseError("No scheme given") + elif parsed.scheme == 'any': + self._scheme = None + return + + self._scheme = parsed.scheme + + def _init_path(self, parsed): + if self._scheme == 'about' and not parsed.path.strip(): + raise ParseError("Pattern without path") + + if parsed.path == '/*': + self._path = None + elif parsed.path == '': + # We want to make it possible to leave off a trailing slash. + self._path = '/' + else: + self._path = parsed.path + + def _init_host(self, parsed): + """Parse the host from the given URL. + + Deviation from Chromium: + - http://:1234/ is not a valid URL because it has no host. + """ + if parsed.hostname is None or not parsed.hostname.strip(): + if self._scheme not in ['about', 'file', 'data', 'javascript']: + raise ParseError("Pattern without host") + assert self._host is None + return + + # FIXME what about multiple dots? + host_parts = parsed.hostname.rstrip('.').split('.') + if host_parts[0] == '*': + host_parts = host_parts[1:] + self._match_subdomains = True + + if not host_parts: + self._host = None + return + + self._host = '.'.join(host_parts) + + if self._host.endswith('.*'): + # Special case to have a nicer error + raise ParseError("TLD wildcards are not implemented yet") + elif '*' in self._host: + # Only * or *.foo is allowed as host. + raise ParseError("Invalid host wildcard") + + def _init_port(self, parsed): + """Parse the port from the given URL. + + Deviation from Chromium: + - We use None instead of "*" if there's no port filter. + """ + if parsed.netloc.endswith(':*'): + # We can't access parsed.port as it tries to run int() + self._port = None + elif parsed.netloc.endswith(':'): + raise ParseError("Invalid port: Port is empty") + else: + try: + self._port = parsed.port + except ValueError as e: + raise ParseError("Invalid port: {}".format(e)) + + if (self._scheme not in list(self.DEFAULT_PORTS) + [None] and + self._port is not None): + raise ParseError("Ports are unsupported with {} scheme".format( + self._scheme)) + + def _matches_scheme(self, scheme): + return self._scheme is None or self._scheme == scheme + + def _matches_host(self, host): + # FIXME what about multiple dots? + host = host.rstrip('.') + + # If we have no host in the match pattern, that means that we're + # matching all hosts, which means we have a match no matter what the + # test host is. + # Contrary to Chromium, we don't need to check for + # self._match_subdomains, as we want to return True here for e.g. + # file:// as well. + if self._host is None: + return True + + # If the hosts are exactly equal, we have a match. + if host == self._host: + return True + + # Otherwise, we can only match if our match pattern matches subdomains. + if not self._match_subdomains: + return False + + # We don't do subdomain matching against IP addresses, so we can give + # up now if the test host is an IP address. + if not utils.raises(ValueError, ipaddress.ip_address, host): + return False + + # Check if the test host is a subdomain of our host. + if len(host) <= (len(self._host) + 1): + return False + + if not host.endswith(self._host): + return False + + return host[len(host) - len(self._host) - 1] == '.' + + def _matches_port(self, scheme, port): + if port == -1 and scheme in self.DEFAULT_PORTS: + port = self.DEFAULT_PORTS[scheme] + return self._port is None or self._port == port + + def _matches_path(self, path): + if self._path is None: + return True + + # Match 'google.com' with 'google.com/' + if path + '/*' == self._path: + return True + + # FIXME Chromium seems to have a more optimized glob matching which + # doesn't rely on regexes. Do we need that too? + return fnmatch.fnmatchcase(path, self._path) + + def matches(self, qurl): + """Check if the pattern matches the given QUrl.""" + qtutils.ensure_valid(qurl) + + if self._match_all: + return True + + if not self._matches_scheme(qurl.scheme()): + return False + # FIXME ignore for file:// like Chromium? + if not self._matches_host(qurl.host()): + return False + if not self._matches_port(qurl.scheme(), qurl.port()): + return False + if not self._matches_path(qurl.path()): + return False + + return True diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index 97e03c072..2ed466dd1 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -537,7 +537,7 @@ def incdec_number(url, incdec, count=1, segments=None): incdec: Either 'increment' or 'decrement' count: The number to increment or decrement by segments: A set of URL segments to search. Valid segments are: - 'host', 'path', 'query', 'anchor'. + 'host', 'port', 'path', 'query', 'anchor'. Default: {'path', 'query'} Return: @@ -550,7 +550,7 @@ def incdec_number(url, incdec, count=1, segments=None): if segments is None: segments = {'path', 'query'} - valid_segments = {'host', 'path', 'query', 'anchor'} + valid_segments = {'host', 'port', 'path', 'query', 'anchor'} if segments - valid_segments: extra_elements = segments - valid_segments raise IncDecError("Invalid segments: {}".format( @@ -561,6 +561,8 @@ def incdec_number(url, incdec, count=1, segments=None): # Order as they appear in a URL segment_modifiers = [ ('host', url.host, url.setHost), + ('port', lambda: str(url.port()) if url.port() > 0 else '', + lambda x: url.setPort(int(x))), ('path', url.path, url.setPath), ('query', url.query, url.setQuery), ('anchor', url.fragment, url.setFragment), diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index aad685d07..039d805f9 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -27,6 +27,7 @@ import operator import collections.abc import enum +import attr from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer from qutebrowser.utils import log, qtutils, utils @@ -266,6 +267,7 @@ class Question(QObject): For user_pwd, a default username as string. title: The question title to show. text: The prompt text to display to the user. + url: Any URL referenced in prompts. answer: The value the user entered (as password for user_pwd). is_aborted: Whether the question was aborted. interrupted: Whether the question was interrupted by another one. @@ -296,6 +298,7 @@ class Question(QObject): self.default = None self.title = None self.text = None + self.url = None self.answer = None self.is_aborted = False self.interrupted = False @@ -392,3 +395,24 @@ class AbstractCertificateErrorWrapper: def is_overridable(self): raise NotImplementedError + + +@attr.s +class NavigationRequest: + + """A request to navigate to the given URL.""" + + Type = enum.Enum('Type', [ + 'link_clicked', + 'typed', # QtWebEngine only + 'form_submitted', + 'form_resubmitted', # QtWebKit only + 'back_forward', + 'reloaded', + 'other' + ]) + + url = attr.ib() + navigation_type = attr.ib() + is_main_frame = attr.ib() + accepted = attr.ib(default=True) diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index 6cf38229b..7c1d43d2b 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 47cbf6eff..71e33886f 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -"""Utilities to show various version informations.""" +"""Utilities to show various version information.""" import re import sys @@ -29,6 +29,7 @@ import importlib import collections import enum import datetime +import getpass import attr import pkg_resources @@ -49,8 +50,8 @@ except ImportError: # pragma: no cover QWebEngineProfile = None import qutebrowser -from qutebrowser.utils import log, utils, standarddir, usertypes -from qutebrowser.misc import objects, earlyinit, sql +from qutebrowser.utils import log, utils, standarddir, usertypes, message +from qutebrowser.misc import objects, earlyinit, sql, httpclient, pastebin from qutebrowser.browser import pdfjs @@ -65,6 +66,7 @@ class DistributionInfo: pretty = attr.ib() +pastebin_url = None Distribution = enum.Enum( 'Distribution', ['unknown', 'ubuntu', 'debian', 'void', 'arch', 'gentoo', 'fedora', 'opensuse', 'linuxmint', 'manjaro']) @@ -167,7 +169,7 @@ def _git_str_subprocess(gitpath): def _release_info(): - """Try to gather distribution release informations. + """Try to gather distribution release information. Return: list of (filename, content) tuples. @@ -335,7 +337,7 @@ def _uptime() -> datetime.timedelta: def version(): - """Return a string with various version informations.""" + """Return a string with various version information.""" lines = ["qutebrowser v{}".format(qutebrowser.__version__)] gitver = _git_str() if gitver is not None: @@ -449,3 +451,39 @@ def opengl_vendor(): # pragma: no cover ctx.doneCurrent() if old_context and old_surface: old_context.makeCurrent(old_surface) + + +def pastebin_version(pbclient=None): + """Pastebin the version and log the url to messages.""" + def _yank_url(url): + utils.set_clipboard(url) + message.info("Version url {} yanked to clipboard.".format(url)) + + def _on_paste_version_success(url): + global pastebin_url + _yank_url(url) + pbclient.deleteLater() + pastebin_url = url + + def _on_paste_version_err(text): + message.error("Failed to pastebin version" + " info: {}".format(text)) + pbclient.deleteLater() + + if pastebin_url: + _yank_url(pastebin_url) + return + + app = QApplication.instance() + http_client = httpclient.HTTPClient() + + misc_api = pastebin.PastebinClient.MISC_API_URL + pbclient = pbclient or pastebin.PastebinClient(http_client, parent=app, + api_url=misc_api) + + pbclient.success.connect(_on_paste_version_success) + pbclient.error.connect(_on_paste_version_err) + + pbclient.paste(getpass.getuser(), + "qute version info {}".format(qutebrowser.__version__), + version()) diff --git a/requirements.txt b/requirements.txt index 8d95759b2..0d2652698 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -attrs==17.3.0 +attrs==17.4.0 colorama==0.3.9 cssutils==1.0.2 Jinja2==2.10 diff --git a/scripts/asciidoc2html.py b/scripts/asciidoc2html.py index 01af53693..c4af174b2 100755 --- a/scripts/asciidoc2html.py +++ b/scripts/asciidoc2html.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # @@ -85,9 +85,9 @@ class AsciiDoc: # patch image links to use local copy replacements = [ - ("https://qutebrowser.org/img/cheatsheet-big.png", + ("https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/cheatsheet-big.png", "qute://help/img/cheatsheet-big.png"), - ("https://qutebrowser.org/img/cheatsheet-small.png", + ("https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/cheatsheet-small.png", "qute://help/img/cheatsheet-small.png") ] asciidoc_args = ['-a', 'source-highlighter=pygments'] diff --git a/scripts/cycle-inputs.js b/scripts/cycle-inputs.js new file mode 100644 index 000000000..bb667bda7 --- /dev/null +++ b/scripts/cycle-inputs.js @@ -0,0 +1,46 @@ +/* Cycle text boxes. + * works with the types defined in 'types'. + * Note: Does not work for