Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9d5b2df0c | ||
|
|
d6fd5a817e | ||
|
|
301186d407 | ||
|
|
ab121a98da | ||
|
|
a463038834 | ||
|
|
22761b4373 | ||
|
|
78f6f3a0e1 | ||
|
|
6166ea51e2 | ||
|
|
4d4065dfac | ||
|
|
81f350ee99 | ||
|
|
4b98e6e9ce | ||
|
|
11f8ab1f85 | ||
|
|
0449da048f | ||
|
|
c78e938dea | ||
|
|
99fb8a5d87 | ||
|
|
b1e0b8f119 | ||
|
|
8d49e001e9 | ||
|
|
965c176acf | ||
|
|
896da1c27e | ||
|
|
8f33fcfc52 | ||
|
|
91b0a33ab0 | ||
|
|
b059f4058f | ||
|
|
b63ce438b4 |
@@ -1,18 +0,0 @@
|
||||
shallow_clone: true
|
||||
version: '{branch}-{build}'
|
||||
cache: C:\Users\appveyor\pip\wheels
|
||||
build: off
|
||||
environment:
|
||||
PYTHON: 'C:\Python34'
|
||||
PYTHONUNBUFFERED: 1
|
||||
|
||||
install:
|
||||
- C:\Python27\python -u scripts\ci_install.py
|
||||
|
||||
test_script:
|
||||
- C:\Python34\Scripts\tox -e smoke
|
||||
- C:\Python34\Scripts\tox -e smoke-frozen
|
||||
- C:\Python34\Scripts\tox -e unittests
|
||||
- C:\Python34\Scripts\tox -e unittests-frozen
|
||||
- C:\Python34\Scripts\tox -e pyflakes
|
||||
- C:\Python34\Scripts\tox -e pylint
|
||||
14
.coveragerc
14
.coveragerc
@@ -1,14 +0,0 @@
|
||||
[run]
|
||||
branch = true
|
||||
omit =
|
||||
qutebrowser/__main__.py
|
||||
*/__init__.py
|
||||
qutebrowser/resources.py
|
||||
|
||||
[report]
|
||||
exclude_lines =
|
||||
pragma: no cover
|
||||
def __repr__
|
||||
raise AssertionError
|
||||
raise NotImplementedError
|
||||
if __name__ == ["']__main__["']:
|
||||
47
.eslintrc
47
.eslintrc
@@ -1,47 +0,0 @@
|
||||
# vim: ft=yaml
|
||||
|
||||
env:
|
||||
browser: true
|
||||
|
||||
rules:
|
||||
block-scoped-var: 2
|
||||
dot-location: 2
|
||||
default-case: 2
|
||||
guard-for-in: 2
|
||||
no-div-regex: 2
|
||||
no-param-reassign: 2
|
||||
no-eq-null: 2
|
||||
no-floating-decimal: 2
|
||||
no-self-compare: 2
|
||||
no-throw-literal: 2
|
||||
no-void: 2
|
||||
radix: 2
|
||||
wrap-iife: [2, "inside"]
|
||||
brace-style: [2, "1tbs", {"allowSingleLine": true}]
|
||||
comma-style: [2, "last"]
|
||||
consistent-this: [2, "self"]
|
||||
func-style: [2, "declaration"]
|
||||
indent: [2, 4, {"indentSwitchCase": true}]
|
||||
linebreak-style: [2, "unix"]
|
||||
max-nested-callbacks: [2, 3]
|
||||
no-lonely-if: 2
|
||||
no-multiple-empty-lines: [2, {"max": 2}]
|
||||
no-nested-ternary: 2
|
||||
no-unneeded-ternary: 2
|
||||
operator-assignment: [2, "always"]
|
||||
operator-linebreak: [2, "after"]
|
||||
space-after-keywords: [2, "always"]
|
||||
space-before-blocks: [2, "always"]
|
||||
space-before-function-paren: [2, {"anonymous": "never", "named": "never"}]
|
||||
space-in-brackets: [2, "never"]
|
||||
space-in-parens: [2, "never"]
|
||||
space-unary-ops: [2, {"words": true, "nonwords": false}]
|
||||
spaced-line-comment: [2, "always"]
|
||||
max-depth: [2, 5]
|
||||
max-len: [2, 79, 4]
|
||||
max-params: [2, 5]
|
||||
max-statements: [2, 30]
|
||||
no-bitwise: 2
|
||||
no-reserved-keys: 2
|
||||
global-strict: 0
|
||||
quotes: 0
|
||||
19
.flake8
Normal file
19
.flake8
Normal file
@@ -0,0 +1,19 @@
|
||||
# vim: ft=dosini fileencoding=utf-8:
|
||||
|
||||
[flake8]
|
||||
# E241: Multiple spaces after ,
|
||||
# E265: Block comment should start with '#'
|
||||
# checked by pylint:
|
||||
# F401: Unused import
|
||||
# E501: Line too long
|
||||
# F821: undefined name
|
||||
# F841: unused variable
|
||||
# E222: Multiple spaces after operator
|
||||
# F811: Redifiniton
|
||||
# W292: No newline at end of file
|
||||
# E701: multiple statements on one line
|
||||
# E702: multiple statements on one line
|
||||
# E225: missing whitespace around operator
|
||||
ignore=E241,E265,F401,E501,F821,F841,E222,F811,W292,E701,E702,E225
|
||||
max_complexity = 12
|
||||
exclude = ez_setup.py
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -12,14 +12,4 @@ __pycache__
|
||||
/qutebrowser/git-commit-id
|
||||
/doc/*.html
|
||||
/README.html
|
||||
/CHANGELOG.html
|
||||
/CONTRIBUTING.html
|
||||
/FAQ.html
|
||||
/INSTALL.html
|
||||
/qutebrowser/html/doc/
|
||||
/.venv
|
||||
/.coverage
|
||||
/htmlcov
|
||||
/.tox
|
||||
/testresults.html
|
||||
/.cache
|
||||
|
||||
24
.pylintrc
24
.pylintrc
@@ -1,34 +1,29 @@
|
||||
# vim: ft=dosini fileencoding=utf-8:
|
||||
|
||||
[MASTER]
|
||||
ignore=resources.py
|
||||
extension-pkg-whitelist=PyQt5,sip
|
||||
load-plugins=pylint_checkers.config,
|
||||
pylint_checkers.modeline,
|
||||
pylint_checkers.openencoding,
|
||||
pylint_checkers.settrace
|
||||
ignore=ez_setup.py
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
disable=no-self-use,
|
||||
super-on-old-class,
|
||||
old-style-class,
|
||||
abstract-class-little-used,
|
||||
bad-builtin,
|
||||
star-args,
|
||||
fixme,
|
||||
global-statement,
|
||||
no-init,
|
||||
locally-disabled,
|
||||
too-many-ancestors,
|
||||
too-few-public-methods,
|
||||
too-many-public-methods,
|
||||
cyclic-import,
|
||||
bad-option-value,
|
||||
bad-continuation,
|
||||
too-many-instance-attributes,
|
||||
unnecessary-lambda,
|
||||
blacklisted-name,
|
||||
too-many-lines,
|
||||
logging-format-interpolation,
|
||||
interface-not-implemented,
|
||||
broad-except,
|
||||
bare-except,
|
||||
eval-used,
|
||||
exec-used,
|
||||
file-ignored
|
||||
too-many-lines
|
||||
|
||||
[BASIC]
|
||||
module-rgx=(__)?[a-z][a-z0-9_]*(__)?$
|
||||
@@ -40,7 +35,6 @@ argument-rgx=[a-z_][a-z0-9_]{0,30}$
|
||||
variable-rgx=[a-z_][a-z0-9_]{0,30}$
|
||||
class-attribute-rgx=[A-Za-z_][A-Za-z0-9_]{1,30}$
|
||||
inlinevar-rgx=[a-z_][a-z0-9_]*$
|
||||
docstring-min-length=2
|
||||
|
||||
[FORMAT]
|
||||
max-line-length=79
|
||||
|
||||
20
.run_checks
Normal file
20
.run_checks
Normal file
@@ -0,0 +1,20 @@
|
||||
# vim: ft=dosini
|
||||
|
||||
[DEFAULT]
|
||||
targets=qutebrowser,scripts
|
||||
|
||||
[pep257]
|
||||
# D102: Docstring missing, will be handled by others
|
||||
# D209: Blank line before closing """ (removed from PEP257)
|
||||
# D402: First line should not be function's signature (false-positives)
|
||||
disable=D102,D209,D402
|
||||
exclude=test_.*
|
||||
|
||||
[pylint]
|
||||
args=--output-format=colorized,--reports=no,--rcfile=.pylintrc
|
||||
plugins=config,crlf,modeline,settrace,openencoding
|
||||
exclude=resources.py
|
||||
|
||||
[flake8]
|
||||
args=--config=.flake8
|
||||
exclude=resources.py
|
||||
28
.travis.yml
28
.travis.yml
@@ -1,28 +0,0 @@
|
||||
dist: trusty
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
|
||||
# Not really, but this is here so we can do stuff by hand.
|
||||
language: c
|
||||
|
||||
install:
|
||||
- python scripts/ci_install.py
|
||||
|
||||
script:
|
||||
- xvfb-run -s "-screen 0 640x480x16" tox -e unittests,smoke
|
||||
- tox -e misc
|
||||
- tox -e pep257
|
||||
- tox -e pyflakes
|
||||
- tox -e pep8
|
||||
- tox -e mccabe
|
||||
- tox -e pylint
|
||||
- tox -e pyroma
|
||||
- tox -e check-manifest
|
||||
|
||||
# Travis bug - OS X builds get routed to Ubuntu Trusty if "dist: trusty" is
|
||||
# given.
|
||||
matrix:
|
||||
allow_failures:
|
||||
- os: osx
|
||||
@@ -1,417 +0,0 @@
|
||||
Change Log
|
||||
===========
|
||||
|
||||
// http://keepachangelog.com/
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
This project adheres to http://semver.org/[Semantic Versioning].
|
||||
|
||||
// tags:
|
||||
// `Added` for new features.
|
||||
// `Changed` for changes in existing functionality.
|
||||
// `Deprecated` for once-stable features removed in upcoming releases.
|
||||
// `Removed` for deprecated features removed in this release.
|
||||
// `Fixed` for any bug fixes.
|
||||
// `Security` to invite users to upgrade in case of vulnerabilities.
|
||||
|
||||
v0.3.0 (unreleased)
|
||||
-------------------
|
||||
|
||||
Added
|
||||
~~~~~
|
||||
|
||||
- New commands `:message-info`, `:message-error` and `:message-warning` to show messages in the statusbar, e.g. from an userscript.
|
||||
- New command `:scroll-px` which replaces `:scroll` for pixel-exact scrolling.
|
||||
- New command `:jseval` to run a javascript snippet on the current page.
|
||||
- New (hidden) command `:follow-selected` (bound to `Enter`/`Ctrl-Enter` by default) to follow the link which is currently selected (e.g. after searching via `/`).
|
||||
- New (hidden) command `:clear-keychain` to clear a partially entered keychain (bound to `<Escape>` by default, in addition to clearing search).
|
||||
- New setting `ui -> smooth-scrolling`.
|
||||
- New setting `content -> webgl` to enable/disable https://www.khronos.org/webgl/[WebGL].
|
||||
- New setting `content -> css-regions` to enable/disable support for http://dev.w3.org/csswg/css-regions/[CSS Regions].
|
||||
- New setting `content -> hyperlink-auditing` to enable/disable support for https://html.spec.whatwg.org/multipage/semantics.html#hyperlink-auditing[hyperlink auditing].
|
||||
- New setting `tabs -> mousewheel-tab-switching` to control mousewheel behavior on the tab bar.
|
||||
- New arguments `--datadir` and `--cachedir` to set the data/cache location.
|
||||
- New arguments `--basedir` and `--temp-basedir` (intended for debugging) to set a different base directory for all data, which allows multiple invocations.
|
||||
- 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.
|
||||
- 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.
|
||||
- New setting `completion -> auto-open` to only open the completion when tab is pressed (if set to false).
|
||||
- New visual/caret mode (bound to `v`) to select text by keyboard.
|
||||
- There are now some example userscripts in `misc/userscripts`.
|
||||
- Support for Qt 5.5 and tox 2.0
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
|
||||
- *Breaking change for userscripts:* `QUTE_HTML` and `QUTE_TEXT` for userscripts now don't store the contents directly, and instead contain a filename.
|
||||
- The `content -> geolocation` and `notifications` settings now support a `true` value to always allow those. However, this is *not recommended*.
|
||||
- New bindings `<Ctrl-R>` (rapid), `<Ctrl-F>` (foreground) and `<Ctrl-B>` (background) to switch hint modes while hinting.
|
||||
- `<Ctrl-M>` and numpad-enter are now bound by default for bindings where `<Return>` was bound.
|
||||
- `:hint tab` and `F` now respect the `background-tabs` setting. To enforce a foreground tab (what `F` did before), use `:hint tab-fg` or `;f`.
|
||||
- `:scroll` now takes a direction argument (`up`/`down`/`left`/`right`/`top`/`bottom`/`page-up`/`page-down`) instead of two pixel arguments (`dx`/`dy`). The old form still works but is deprecated.
|
||||
- The `ui -> user-stylesheet` setting now also takes file paths relative to the config directory.
|
||||
- The `content -> cookies-accept` setting now has new `no-3rdparty` (default) and `no-unknown-3rdparty` values to block third-party cookies. The `default` value got renamed to `all`.
|
||||
- Improved startup time by reading the webpage history while qutebrowser is open.
|
||||
- The way `:spawn` splits its commandline has been changed slightly to allow commands with flags.
|
||||
- The default for the `new-instance-open-target` setting has been changed to `tab`.
|
||||
- Sessions now store zoom/scroll-position separately for each entry.
|
||||
|
||||
Deprecated
|
||||
~~~~~~~~~~
|
||||
|
||||
- `:scroll` with two pixel-arguments is now deprecated - `:scroll-px` should be used instead.
|
||||
|
||||
Removed
|
||||
~~~~~~~
|
||||
|
||||
- The `--no-crash-dialog` argument which was intended for debugging only was removed as it's replaced by `--no-err-windows` which suppresses all error windows.
|
||||
- Support for Qt installations without SSL support was dropped.
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- Scrolling should now work more reliably on some pages where arrow keys worked but `hjkl` didn't.
|
||||
- Small improvements when checking if an input is an URL or not.
|
||||
- Fixed wrong cursor position when completing the first item in the completion.
|
||||
- Fixed exception when using search engines with {foo} in their name.
|
||||
- Fixed a bug where the same title was shown for all tabs on some systems.
|
||||
- Don't install the scripts package when installing qutebrowser.
|
||||
- Fixed searching for terms starting with a hyphen (e.g. `/-foo`)
|
||||
- Proxy authentication credentials are now remembered between different tabs.
|
||||
- Fixed updating of the tab title on pages without title.
|
||||
- Fixed AssertionError when closing many windows quickly.
|
||||
- Various fixes for deprecated key bindings and auto-migrations.
|
||||
- Workaround for qutebrowser not starting when there are NUL-bytes in the history (because of a currently unknown bug).
|
||||
- Fixed handling of keybindings containing Ctrl/Meta on OS X.
|
||||
- Fixed crash when downloading an URL without filename (e.g. magnet links) via "Save as...".
|
||||
- Fixed exception when starting qutebrowser with `:set` as argument.
|
||||
- Fixed horrible completion performance when the `shrink` option was set.
|
||||
- Sessions now store zoom/scroll-position correctly.
|
||||
|
||||
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.1[v0.2.1]
|
||||
-----------------------------------------------------------------------
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- Added missing manpage (doc/qutebrowser.1.asciidoc) to archive.
|
||||
|
||||
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.0[v0.2.0]
|
||||
-----------------------------------------------------------------------
|
||||
|
||||
Added
|
||||
~~~~~
|
||||
|
||||
- Session support
|
||||
* new command `:session-load` to load a session.
|
||||
* new command `:session-save` to save a session.
|
||||
* new command `:session-delete` to delete a session.
|
||||
* new setting `general -> save-session` to always save the session on quit.
|
||||
* new setting `general -> session-default-name` to configure the session name to use if none is given.
|
||||
* new argument `-r`/`--restore` to specify a session to load.
|
||||
* new argument `-R`/`--override-restore` to not load a session even if one was saved.
|
||||
- New commands to manage downloads:
|
||||
* `:download` to download a URL or the current page.
|
||||
* `:download-cancel` to cancel a download.
|
||||
* `:download-delete` to delete a download from disk.
|
||||
* `:download-open` to open a finished download.
|
||||
* `:download-remove` to remove a download from the list. `:download-remove --all` or the new 'cd' keybinding can be used to clear all finished downloads.
|
||||
- History completion
|
||||
* New option `completion -> timestamp-format` to set the format used to display the history timestamps.
|
||||
* New option `completion -> web-history-max-items` to configure how many history items to show in the completion.
|
||||
* The option `completion -> history-length` for the command history got renamed to `cmd-history-max-items`.
|
||||
- Better save logic for the config/state:
|
||||
* Only save files if modified (e.g. don't overwrite the config if it was edited outside of qutebrowser and nothing was changed in qutebrowser).
|
||||
* Save things (cookies, config, quickmarks, ...) periodically all 15 seconds (time can be changed with the `general -> auto-save-interval` option).
|
||||
- Opera-like mouse rocker gestures
|
||||
* New option `input -> rocker-gestures`. When turned on, the history can be navigated back/forward by holding a mouse button and pressing the other one.
|
||||
- New `-f` option for `:reload` to reload and bypass the cache.
|
||||
- Pass more information (`QUTE_MODE`, `QUTE_SELECTED_TEXT`, `QUTE_SELECTED_HTML`, `QUTE_USER_AGENT`, `QUTE_HTML`, `QUTE_TEXT`) to userscripts.
|
||||
- New `--userscript` option to `:spawn` (which deprecates `:run-userscript`).
|
||||
- Ability to toggle a value to `:set` by appending a `!` to the value.
|
||||
- New options to hide the tab-/statusbar:
|
||||
* `tabs -> hide-always` for the tabbar
|
||||
* `ui -> hide-statusbar` for the statusbar
|
||||
- New options to configure how the tab/window titles should look:
|
||||
* `tabs -> title-format` for the tabbar
|
||||
* `ui -> window-title-format` for the window title
|
||||
- HTML5 Geolocation/Notification support:
|
||||
* New option `content -> geolocation` to permanently turn the geolocation off.
|
||||
* New option `content -> notifications` to permanently turn notifications off.
|
||||
- New options to disable javascript prompts/alerts:
|
||||
* `content -> ignore-javascript-prompt` to turn off prompts.
|
||||
* `content -> ignore-javascript-alerts` to turn off alerts.
|
||||
- Two new options to customize the behavior of hints:
|
||||
* `hints -> min-chars` to set minimum number of chars in hints.
|
||||
* `hints -> scatter` which when turned off distributes the hints sequentially (like dwb) instead of scattering their positions (like Vimium).
|
||||
- Make it possible to use `:open -[twb]` without url.
|
||||
* New option `general -> default-page` to set the page to be opened when doing that.
|
||||
- New `input -> partial-timeout` option to clear partial keystrings.
|
||||
- New option `completion -> download-path-suggestion` to configure what to show in the completion for downloads.
|
||||
- Queue messages shown in unfocused windows and show them when the window is focused.
|
||||
* New option `ui -> message-unfocused` to disable this behavior.
|
||||
- New `--relaxed-config` argument which ignores unknown options.
|
||||
- New `:tab-detach` command to open the current tab in a new window.
|
||||
- Zooming via Ctrl-Mousewheel.
|
||||
* New option `input -> mouse-zoom-divider` to control how much the page is zoomed when rotating the wheel.
|
||||
- New option (`content -> host-blocking-enabled`) to enable/disable host blocking.
|
||||
- New values `tab-bg`/`tab-bg-silent` for `new-instance-open-target` to open a background tab.
|
||||
- New `ui -> downloads-position` setting to move the downloads to the bottom.
|
||||
- New `ui -> hide-mouse-cursor` option to hide the mouse cursor inside qutebrowser.
|
||||
- New argument `-s` for qutebrowser to set a temporary config option.
|
||||
- New argument `-p` for the `:set` command to print the new value.
|
||||
- New `--rapid` option to `:hint`. The `rapid`/`rapid-win` targets are now deprecated, and `--rapid` can be used as well with the targets run/hover/userscript/spawn as well.
|
||||
- New `-f` argument to `:bind` to overwrite the old binding.
|
||||
- New `--qt-name` argument to qutebrowser which is passed to Qt to set `WM_CLASS`.
|
||||
- Alternating row colors in completion. This adds a new `colors -> completion.alternate-bg` option.
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
|
||||
- Ignore quotes with maxsplit-commands (`:open`, `:quickmark-load`, etc.) and don't quote arguments for those commands in the completions. This also means some commands needed adjustments:
|
||||
* Clear search when `:search` without arguments is given. (`:search ""` will now search for the literal text `""`)
|
||||
* Add `-s`/`--space` argument to `:set-cmd-text` (as `:set-cmd-text "foo "` will now set the literal text `"foo "`)
|
||||
- Ignore `;;` for splitting with some commands like `:bind`.
|
||||
- Add unbound (new) default keybindings to config. This also adds a new `<unbound>` special command.
|
||||
* To unbind a command keybinding without binding it to a new key, you now have to bind it to `<unbound>` or it'll be readded automatically.
|
||||
- If an SSL error is raised multiple times with the same error/certificate/host/scheme/port, the user is only asked once.
|
||||
- Jump to last instead of first item when pressing Shift-Tab the first time in the completion.
|
||||
- Add a fullscreen keybinding.
|
||||
- Add a `:search` command in addition to `/foo` so it's more visible and can be used from scripts.
|
||||
- Various improvements to documentation, logging, and the crash reporter.
|
||||
- Expand `~` to the users home directory with `:run-userscript`.
|
||||
- Improve the userscript runner on Linux/OS X by using `QSocketNotifier`.
|
||||
- Add luakit-like `gt`/`gT` keybindings to cycle through tabs.
|
||||
- Show default value for config values in the completion.
|
||||
- Clone tab icon, tab text and zoom level when cloning tabs.
|
||||
- Don't open relative file paths with `:open`, only with commandline arguments.
|
||||
- Expand environment variables in config settings which take a file path.
|
||||
- Add a list of common user agents to the user agent setting completion.
|
||||
- Move cursor to end of textboxes when hinting.
|
||||
- Don't start searches on invalid URLs for quickmarks/startpage.
|
||||
- Various performance improvements for the completion.
|
||||
- Always open URLs given as argument in the foreground.
|
||||
- Improve various error messages.
|
||||
- Add `startpage`/`default-page` values to `tabs -> last-close`.
|
||||
- Various improvements to `:restart` - it should be more robust now and uses sessions so all state (focused tab, scroll position, etc.) gets remembered.
|
||||
- Add tab index display to the statusbar.
|
||||
- Keep progress bar height fixed when the statusbar is multiline.
|
||||
- Many improvements to tests and related infrastructure:
|
||||
* `init_venv.py` and `run_checks.py` have been replaced by http://tox.readthedocs.org/[tox]. Install tox and run `tox -e mkvenv` instead.
|
||||
* The tests now use http://pytest.org/[pytest]
|
||||
* Many new tests added
|
||||
* Mac Mini buildbot to run the tests on OS X.
|
||||
* Coverage recording via http://nedbatchelder.com/code/coverage/[coverage.py].
|
||||
* New `--pdb-postmortem argument` to drop into the pdb debugger on exceptions.
|
||||
* Use https://github.com/ionelmc/python-hunter[hunter] for line tracing instead of a selfmade solution.
|
||||
|
||||
Deprecated
|
||||
~~~~~~~~~~
|
||||
|
||||
- The `:run-userscript` command - use `:spawn --userscript` instead.
|
||||
- The `rapid` and `rapid-win` targets for `:hint` - use the `--rapid` argument to `:hint` instead.
|
||||
- The `:cancel-download` command - use `:download-cancel` instead.
|
||||
- The `:download-page` command - use `:download` instead.
|
||||
|
||||
Removed
|
||||
~~~~~~~
|
||||
|
||||
- `init_venv.py` and `run_checks.py` have been replaced by http://tox.readthedocs.org/[tox]. Install tox and run `tox -e mkvenv` instead..
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- Fix for cache never being used.
|
||||
- Fixed handling of key release events (e.g. for javascript) when holding a key and pressing a second one.
|
||||
- Fix handling of commands using `;;` at various places (key config, command parser, `:bind`)
|
||||
- Fix splitting of flags with arguments (`:bind -m`/`--mode`).
|
||||
- Fix bindings of special keys with lower-case modifiers (e.g. `<ctrl-x>`)
|
||||
- Fix for weird search highlights when changing tabs while search is active.
|
||||
- Fix starting with `-c ""`.
|
||||
- Fix removing of partial downloads when a download is cancelled via context menu.
|
||||
- Fix retrying of downloads which were started in a now closed tab.
|
||||
- Highlight text case-insensitively in completion.
|
||||
- Scroll completion to top when showing it.
|
||||
- Handle unencodable file paths in config types correctly.
|
||||
- Fix for crash when executing a delayed command (because of a shadowed keybinding) and then unfocusing the window.
|
||||
- Fix for crash when hinting on a page which doesn't have an URL yet.
|
||||
- Fix exception when using `:set-cmd-text` with an empty argument.
|
||||
- Add a timeout to pastebin HTTP replies.
|
||||
- Various other fixes for small/rare bugs.
|
||||
|
||||
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.1.4[v0.1.4]
|
||||
-----------------------------------------------------------------------
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
|
||||
* The Windows builds come with Qt 5.4.1 which has some https://lists.schokokeks.org/pipermail/qutebrowser/2015-March/000054.html[related bugfixes].
|
||||
* Improvements to CPU usage when idle.
|
||||
* Ensure there's no size for `font-family` settings.
|
||||
* Handle URLs with double-colon as search strings.
|
||||
* Adjust prompt size hint based on content.
|
||||
* Refactor websettings and save/restore defaults.
|
||||
* Various small improvements to logging.
|
||||
* Various improvements for hinting.
|
||||
* Improve parsing of `faulthandler` logs.
|
||||
|
||||
Removed
|
||||
~~~~~~~
|
||||
|
||||
* Remove default search engines.
|
||||
* Remove debug console completing completely.
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
* Ignore RuntimeError in `mouserelease_insertmode`.
|
||||
* Hide Qt warning when aborting download reply.
|
||||
* Hide "Error while shutting down tabs" message.
|
||||
* Clear open target in `acceptNavigationRequest`.
|
||||
* Fix handling of signals with deleted tabs.
|
||||
* Restore `sys.std*` in `utils.fake_io` on exceptions.
|
||||
* Allow font names with integers in them.
|
||||
* Fix `QIODevice` warnings when closing tabs.
|
||||
* Set the `QSettings` path to a config-subdirectory.
|
||||
* Add workaround for adblock-message without window.
|
||||
* Fix searching for terms starting with a slash.
|
||||
* Ignore tab key presses if they'd switch focus.
|
||||
|
||||
Security
|
||||
~~~~~~~~
|
||||
|
||||
* Stop the icon database from being created when private-browsing is set to true.
|
||||
* Disable insecure SSL ciphers.
|
||||
|
||||
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.1.3[v0.1.3]
|
||||
-----------------------------------------------------------------------
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
|
||||
* Various small logging improvements.
|
||||
* Don't open relative files in `fuzzy_url` with `:open`
|
||||
* Various crashdialog improvements.
|
||||
* Hide adblocked iframes.
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
* Handle shutdown of page with prompt correctly.
|
||||
* fuzzy_url: handle invalid URLs with autosearch off
|
||||
* Handle explicit searches with `auto-search=false`.
|
||||
* Abort download override question on error/cancel.
|
||||
* Set a higher z-index for hint labels.
|
||||
* Close contextmenu when closing tab to avoid crash.
|
||||
* Fix statusbar quickly popping up as window.
|
||||
* Clean up `NetworkManager` after downloads finished.
|
||||
* Fix restoring of cmd widget after an error.
|
||||
* Fix retrying of downloads after the tab is closed.
|
||||
* Fix `check_libraries()` output for Arch Linux.
|
||||
* Handle all `IPCErrors` properly.
|
||||
* Handle another `webelem.IsNullError` with hints.
|
||||
* Handle `UnicodeDecodeError` when reading configs.
|
||||
|
||||
Security
|
||||
~~~~~~~~
|
||||
|
||||
* Fix for HTTP passwords accidentally being written to debug log.
|
||||
|
||||
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.1.2[v0.1.2]
|
||||
-----------------------------------------------------------------------
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
|
||||
* Uncheck sending of debug log by default when private browsing is on.
|
||||
* Add SSL info to version info.
|
||||
|
||||
Removed
|
||||
~~~~~~~
|
||||
|
||||
* Remove hosts-file.net from blocker default lists.
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
* Fix rare exception when a key is pressed shorly after opening a window
|
||||
* Fix exception with certain invalid URLs like `http:foo:0`
|
||||
* Work around Qt bug which renders checkboxes on OS X unusable
|
||||
* Fix exception when a local files can't be read in `:adblock-update`
|
||||
* Hide 2 more Qt warnings.
|
||||
* Add `!important` to hint CSS so websites don't override the hint look
|
||||
* Make `init_venv.py` work with multiple sip `.so` files.
|
||||
* Fix splitting with certain commands with an empty argument
|
||||
* Fix uppercase hints.
|
||||
* Fix segfaults if another page is loaded while a prompt is open
|
||||
* Fix exception with invalid `ShellCommand` config values.
|
||||
* Replace unencodable chars
|
||||
* Fix user-stylesheet setting with an empty value.
|
||||
|
||||
|
||||
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.1.1[v0.1.1]
|
||||
-----------------------------------------------------------------------
|
||||
|
||||
Added
|
||||
~~~~~
|
||||
|
||||
* Set window icon and add a qutebrowser.ico file for Windows.
|
||||
* Ask the user when downloading to an already existing file.
|
||||
* Add a `network -> proxy-dns-requests` option.
|
||||
* Add "Remove finished" to the download context menu
|
||||
* Open and remove clicked downloads.
|
||||
|
||||
Changes
|
||||
~~~~~~~
|
||||
|
||||
* Windows releases are now built with Qt 5.4 which brings many improvements and bugfixes.
|
||||
* Add a troubleshooting section to the FAQ.
|
||||
* Display IPC errors to the user.
|
||||
* Rewrite keymode handling to use only one mode which also fixes various bugs.
|
||||
* Save version to state config.
|
||||
* Set zoom to default instead of 100% with `:zoom`/`=`.
|
||||
* Adjust page zoom if default zoom changed.
|
||||
* Force tabs to be focused on `:undo`.
|
||||
* Replace manual installation instructions on OS X with homebrew/macports.
|
||||
* Allow min-/maximizing of print preview on Windows.
|
||||
* Various documentation improvements.
|
||||
* Various other small improvements and cleanups.
|
||||
|
||||
Removed
|
||||
~~~~~~~
|
||||
|
||||
* Clean up and temporarily disable alias completion.
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
* Fix setting of `QWebSettings` (e.g. web fonts) with empty strings.
|
||||
* Re-focus web view when leaving prompt/yesno mode.
|
||||
* Handle `:restart` correctly with Python eggs.
|
||||
* Handle an invalid cwd properly.
|
||||
* Fix popping of a dead question in prompter.
|
||||
* Fix `AttributeError` on config changes on Ubuntu.
|
||||
* Don't treat things like "31c3" as IP address.
|
||||
* Handle category being `None` in Qt message handler.
|
||||
* Force-include pygments in `freeze.py`.
|
||||
* Fix scroll percentage not updating on some pages like twitter.
|
||||
* Encode `Content-Disposition` header name properly.
|
||||
* Fix item sorting in `NeighborList`.
|
||||
* Handle data being `None` in download read timer.
|
||||
* Stop download read timer when reply has finished.
|
||||
* Fix handling of small/big `fuzzyval`'s in `NeighborList`.
|
||||
* Fix crashes when entering invalid values in `qute:settings`.
|
||||
* Abort questions in `NetworkManager` when destroyed.
|
||||
* Fix height calculation of download view.
|
||||
* Always auto-remove adblock downloads when done.
|
||||
* Ensure the docs get included in `freeze.py`.
|
||||
* Fix crash with `:zoom`.
|
||||
|
||||
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.1[v0.1]
|
||||
-------------------------------------------------------------------
|
||||
|
||||
Initial release.
|
||||
44
MANIFEST.in
44
MANIFEST.in
@@ -1,39 +1,17 @@
|
||||
global-exclude __pycache__ *.pyc *.pyo
|
||||
|
||||
recursive-include qutebrowser *.py
|
||||
recursive-include qutebrowser/html *.html
|
||||
recursive-include qutebrowser/test *.py
|
||||
recursive-include qutebrowser/javascript *.js
|
||||
graft icons
|
||||
graft doc/img
|
||||
graft misc
|
||||
graft scripts
|
||||
include qutebrowser/utils/testfile
|
||||
recursive-include icons *
|
||||
include qutebrowser/test/testfile
|
||||
include qutebrowser/git-commit-id
|
||||
include COPYING doc/* README.asciidoc CONTRIBUTING.asciidoc FAQ.asciidoc INSTALL.asciidoc CHANGELOG.asciidoc
|
||||
include COPYING doc/* README.asciidoc
|
||||
include qutebrowser.desktop
|
||||
include requirements.txt
|
||||
include tox.ini
|
||||
include qutebrowser.py
|
||||
|
||||
include scripts/__init__.py
|
||||
include scripts/hostblock_blame.py
|
||||
include scripts/importer.py
|
||||
include scripts/keytester.py
|
||||
include scripts/link_pyqt.py
|
||||
include scripts/minimal_webkit_testbrowser.py
|
||||
include scripts/setupcommon.py
|
||||
include scripts/utils.py
|
||||
|
||||
exclude doc/notes
|
||||
recursive-exclude doc *.asciidoc
|
||||
include doc/qutebrowser.1.asciidoc
|
||||
prune tests
|
||||
exclude qutebrowser.rcc
|
||||
exclude .coveragerc
|
||||
exclude scripts/run_checks.py
|
||||
exclude scripts/cleanup.py
|
||||
exclude scripts/minimal_webkit_testbrowser.py
|
||||
exclude scripts/run_profile.py
|
||||
exclude scripts/generate_authors.sh
|
||||
exclude .flake8
|
||||
exclude .pylintrc
|
||||
exclude .eslintrc
|
||||
exclude doc/help
|
||||
exclude .appveyor.yml
|
||||
exclude .travis.yml
|
||||
exclude misc/appveyor_install.py
|
||||
exclude doc/notes
|
||||
prune pkg
|
||||
|
||||
@@ -6,13 +6,8 @@
|
||||
qutebrowser
|
||||
===========
|
||||
|
||||
image:icons/qutebrowser-64x64.png[qutebrowser logo] *A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit.*
|
||||
|
||||
image:https://img.shields.io/pypi/l/qutebrowser.svg?style=flat["license badge",link="https://github.com/The-Compiler/qutebrowser/blob/master/COPYING"]
|
||||
image:https://img.shields.io/pypi/v/qutebrowser.svg?style=flat["version badge",link="https://pypi.python.org/pypi/qutebrowser/"]
|
||||
image:https://img.shields.io/github/issues/The-Compiler/qutebrowser.svg?style=flat["issues badge",link="https://github.com/The-Compiler/qutebrowser/issues"]
|
||||
image:https://requires.io/github/The-Compiler/qutebrowser/requirements.svg?branch=master["requirements badge",link="https://requires.io/github/The-Compiler/qutebrowser/requirements/?branch=master"]
|
||||
image:http://qutebrowser.org:8010/png?builder=archlinux["build badge",link="http://qutebrowser.org:8010/waterfall"]
|
||||
image:icons/qutebrowser-64x64.png[] _A keyboard-driven, vim-like browser based
|
||||
on PyQt5 and QtWebKit._
|
||||
|
||||
qutebrowser is a keyboard-focused browser with with a minimal GUI. It's based
|
||||
on Python, PyQt5 and QtWebKit and free software, licensed under the GPL.
|
||||
@@ -22,10 +17,10 @@ It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl.
|
||||
Screenshots
|
||||
-----------
|
||||
|
||||
image:doc/img/main.png["screenshot 1",width=300,link="doc/img/main.png"]
|
||||
image:doc/img/downloads.png["screenshot 2",width=300,link="doc/img/downloads.png"]
|
||||
image:doc/img/completion.png["screenshot 3",width=300,link="doc/img/completion.png"]
|
||||
image:doc/img/hints.png["screenshot 4",width=300,link="doc/img/hints.png"]
|
||||
image:doc/img/main.png[width=300,link="doc/img/main.png"]
|
||||
image:doc/img/downloads.png[width=300,link="doc/img/downloads.png"]
|
||||
image:doc/img/completion.png[width=300,link="doc/img/completion.png"]
|
||||
image:doc/img/hints.png[width=300,link="doc/img/hints.png"]
|
||||
|
||||
Downloads
|
||||
---------
|
||||
@@ -34,7 +29,7 @@ See the https://github.com/The-Compiler/qutebrowser/releases[github releases
|
||||
page] for available downloads (currently a source archive, and standalone
|
||||
packages as well as MSI installers for Windows).
|
||||
|
||||
See link:INSTALL.asciidoc[INSTALL] for detailed instructions on how to get
|
||||
See link:doc/INSTALL.asciidoc[INSTALL] for detailed instructions on how to get
|
||||
qutebrowser running for various platforms.
|
||||
|
||||
Documentation
|
||||
@@ -43,15 +38,13 @@ Documentation
|
||||
In addition to the topics mentioned in this README, the following documents are
|
||||
available:
|
||||
|
||||
* A http://qutebrowser.org/img/cheatsheet-big.png[key binding cheatsheet]: +
|
||||
image:http://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding cheatsheet",link="http://qutebrowser.org/img/cheatsheet-big.png"]
|
||||
* A http://qutebrowser.org/img/cheatsheet-big.png[keybinding cheatsheet]: +
|
||||
image:http://qutebrowser.org/img/cheatsheet-small.png["qutebrowser keybinding cheatsheet",link="http://qutebrowser.org/img/cheatsheet-big.png"]
|
||||
* link:doc/quickstart.asciidoc[Quick start guide]
|
||||
* link:FAQ.asciidoc[Frequently asked questions]
|
||||
* link:CONTRIBUTING.asciidoc[Contributing to qutebrowser]
|
||||
* link:INSTALL.asciidoc[INSTALL]
|
||||
* link:CHANGELOG.asciidoc[Change Log]
|
||||
* link:doc/FAQ.asciidoc[Frequently asked questions]
|
||||
* link:doc/HACKING.asciidoc[HACKING]
|
||||
* link:doc/INSTALL.asciidoc[INSTALL]
|
||||
* link:doc/stacktrace.asciidoc[Reporting segfaults]
|
||||
* link:doc/userscripts.asciidoc[How to write userscripts]
|
||||
|
||||
Getting help
|
||||
------------
|
||||
@@ -68,8 +61,7 @@ Contributions / Bugs
|
||||
--------------------
|
||||
|
||||
You want to contribute to qutebrowser? Awesome! Please read
|
||||
link:CONTRIBUTING.asciidoc[the contribution guidelines] for details and
|
||||
useful hints.
|
||||
link:doc/HACKING.asciidoc[HACKING] for details and useful hints.
|
||||
|
||||
If you found a bug or have a feature request, you can report it in several
|
||||
ways:
|
||||
@@ -89,15 +81,14 @@ Requirements
|
||||
The following software and libraries are required to run qutebrowser:
|
||||
|
||||
* http://www.python.org/[Python] 3.4
|
||||
* http://qt.io/[Qt] 5.2.0 or newer (5.4.2 recommended)
|
||||
* http://qt-project.org/[Qt] 5.2.0 or newer (5.4 recommended)
|
||||
* QtWebKit
|
||||
* http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.2.0 or newer
|
||||
(5.4.2 recommended) for Python 3
|
||||
(5.3.2 recommended) for Python 3
|
||||
* https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools]
|
||||
* http://fdik.org/pyPEG/[pyPEG2]
|
||||
* http://jinja.pocoo.org/[jinja2]
|
||||
* http://pygments.org/[pygments]
|
||||
* http://pyyaml.org/wiki/PyYAML[PyYAML]
|
||||
|
||||
To generate the documentation for the `:help` command, when using the git
|
||||
repository (rather than a release), http://asciidoc.org/[asciidoc] is needed.
|
||||
@@ -108,8 +99,8 @@ console:
|
||||
* https://pypi.python.org/pypi/colorlog/[colorlog]
|
||||
* On Windows: https://pypi.python.org/pypi/colorama/[colorama]
|
||||
|
||||
See link:INSTALL.asciidoc[INSTALL] for directions on how to install qutebrowser
|
||||
and its dependencies.
|
||||
See link:doc/INSTALL.asciidoc[INSTALL] for directions on how to install
|
||||
qutebrowser and its dependencies.
|
||||
|
||||
Donating
|
||||
--------
|
||||
@@ -134,49 +125,26 @@ Contributors, sorted by the number of commits in descending order:
|
||||
|
||||
// QUTE_AUTHORS_START
|
||||
* Florian Bruhin
|
||||
* Bruno Oliveira
|
||||
* Raphael Pierzina
|
||||
* Joel Torstensson
|
||||
* Martin Tournoij
|
||||
* Claude
|
||||
* Lamar Pavel
|
||||
* Austin Anderson
|
||||
* Artur Shaik
|
||||
* Antoni Boucher
|
||||
* ZDarian
|
||||
* Peter Vilim
|
||||
* John ShaggyTwoDope Jenkins
|
||||
* Jimmy
|
||||
* Zach-Button
|
||||
* rikn00
|
||||
* Patric Schmitz
|
||||
* Brian Jackson
|
||||
* Martin Zimmermann
|
||||
* Error 800
|
||||
* Brian Jackson
|
||||
* sbinix
|
||||
* Tobias Patzl
|
||||
* Johannes Altmanninger
|
||||
* Samir Benmendil
|
||||
* Regina Hug
|
||||
* Mathias Fussenegger
|
||||
* Larry Hynes
|
||||
* Fritz V155 Reichwald
|
||||
* Franz Fellner
|
||||
* error800
|
||||
* Thorsten Wißmann
|
||||
* Thiago Barroso Perrotta
|
||||
* Johannes Altmanninger
|
||||
* Joel Torstensson
|
||||
* Regina Hug
|
||||
* Peter Vilim
|
||||
* Matthias Lisin
|
||||
* Helen Sherwood-Taylor
|
||||
* HalosGhost
|
||||
* Gregor Pohl
|
||||
* Eivind Uggedal
|
||||
* Andreas Fischer
|
||||
// QUTE_AUTHORS_END
|
||||
|
||||
The following people have contributed graphics:
|
||||
|
||||
* WOFall (icon)
|
||||
* regines (key binding cheatsheet)
|
||||
* regines (keybinding cheatsheet)
|
||||
|
||||
Thanks / Similiar projects
|
||||
--------------------------
|
||||
@@ -222,7 +190,7 @@ Also, thanks to:
|
||||
|
||||
* Everyone who had the patience to test qutebrowser before v0.1.
|
||||
* Everyone triaging/fixing my bugs in the
|
||||
https://bugreports.qt.io/secure/Dashboard.jspa[Qt bugtracker]
|
||||
https://bugreports.qt-project.org/secure/Dashboard.jspa[Qt bugtracker]
|
||||
* Everyone answering my questions on http://stackoverflow.com/[Stack Overflow]
|
||||
and in IRC.
|
||||
* All the projects which were a great help while developing qutebrowser.
|
||||
|
||||
@@ -4,18 +4,18 @@ The Compiler <mail@qutebrowser.org>
|
||||
|
||||
[qanda]
|
||||
What is qutebrowser based on?::
|
||||
qutebrowser uses http://www.python.org/[Python], http://qt.io/[Qt] and
|
||||
http://www.riverbankcomputing.com/software/pyqt/intro[PyQt].
|
||||
qutebrowser uses http://www.python.org/[Python], http://qt-project.org/[Qt]
|
||||
and http://www.riverbankcomputing.com/software/pyqt/intro[PyQt].
|
||||
+
|
||||
The concept of it is largely inspired by http://portix.bitbucket.org/dwb/[dwb]
|
||||
and http://www.vimperator.org/vimperator[Vimperator]. Many actions and
|
||||
key bindings are similar to dwb.
|
||||
keybindings are similar to dwb.
|
||||
|
||||
Why another browser?::
|
||||
It might be hard to believe, but I didn't find any browser which I was
|
||||
happy with, so I started to write my own. Also, I needed a project to get
|
||||
into writing GUI applications with Python and
|
||||
link:http://qt.io/[Qt]/link:http://www.riverbankcomputing.com/software/pyqt/intro[PyQt].
|
||||
link:http://qt-project.org/[Qt]/link:http://www.riverbankcomputing.com/software/pyqt/intro[PyQt].
|
||||
+
|
||||
Read the next few questions to find out why I was unhappy with existing
|
||||
software.
|
||||
@@ -32,11 +32,12 @@ API] seems to lack basic features like proxy support, and almost no projects
|
||||
seem to have started porting to WebKit2 (I only know of
|
||||
http://www.uzbl.org/[uzbl]).
|
||||
+
|
||||
qutebrowser uses http://qt.io/[Qt] and http://wiki.qt.io/QtWebKit[QtWebKit]
|
||||
instead, which suffers from far less such crashes. It might switch to
|
||||
http://wiki.qt.io/QtWebEngine[QtWebEngine] in the future, which is based on
|
||||
Google's https://en.wikipedia.org/wiki/Blink_(layout_engine)[Blink] rendering
|
||||
engine.
|
||||
qutebrowser uses http://qt-project.org/[Qt] and
|
||||
http://qt-project.org/wiki/QtWebKit[QtWebKit] instead, which suffers from far
|
||||
less such crashes. It might switch to
|
||||
http://qt-project.org/wiki/QtWebEngine[QtWebEngine] in the future, which is
|
||||
based on Google's https://en.wikipedia.org/wiki/Blink_(layout_engine)[Blink]
|
||||
rendering engine.
|
||||
|
||||
What's wrong with https://www.mozilla.org/en-US/firefox/new/[Firefox] and link:http://5digits.org/pentadactyl/[Pentadactyl]/link:http://www.vimperator.org/vimperator[Vimperator]?::
|
||||
Firefox likes to break compatibility with addons on each upgrade, gets
|
||||
@@ -53,10 +54,10 @@ What's wrong with http://www.chromium.org/Home[Chromium] and https://vimium.gith
|
||||
|
||||
Why Python?::
|
||||
I enjoy writing Python since 2011, which made it one of the possible
|
||||
choices. I wanted to use http://qt.io/[Qt] because of
|
||||
http://wiki.qt.io/QtWebKit[QtWebKit] so I didn't have
|
||||
http://wiki.qt.io/Category:LanguageBindings[many other choices]. I don't
|
||||
like C++ and can't write it very well, so that wasn't an alternative.
|
||||
choices. I wanted to use http://qt-project.org/[Qt] because of
|
||||
http://qt-project.org/wiki/QtWebKit[QtWebKit] so I didn't have
|
||||
http://qt-project.org/wiki/Category:LanguageBindings[many other choices]. I
|
||||
don't like C++ and can't write it very well, so that wasn't an alternative.
|
||||
|
||||
But isn't Python too slow for a browser?::
|
||||
http://www.infoworld.com/d/application-development/van-rossum-python-not-too-slow-188715[No.]
|
||||
@@ -74,15 +75,6 @@ Is there an adblocker?::
|
||||
usage], so implementing it properly might take some time and won't be done
|
||||
for v0.1 if at all.
|
||||
|
||||
How do I play Youtube videos with mpv?::
|
||||
You can easily add a key binding to play youtube videos inside a real video
|
||||
player - optionally even with hinting for links:
|
||||
+
|
||||
----
|
||||
:bind x spawn mpv {url}
|
||||
:bind ;x hint links spawn mpv {hint-url}
|
||||
----
|
||||
|
||||
== Troubleshooting
|
||||
|
||||
Configuration not saved after modifying config.::
|
||||
@@ -111,17 +103,10 @@ Experiencing segfaults (crashes) on Debian systems.::
|
||||
|
||||
Segfaults on Facebook, Medium, Amazon, ...::
|
||||
If you are on a Debian or Ubuntu based system, you might experience some crashes
|
||||
visting these sites. This is caused by various bugs in Qt which have been
|
||||
visting these sites. This is caused by a known bug in Qt which has been
|
||||
fixed in Qt 5.4. However Debian and Ubuntu are slow to adopt or upgrade
|
||||
some packages. On Debian Jessie, it's recommended to use the experimental
|
||||
repos as described in https://github.com/The-Compiler/qutebrowser/blob/master/INSTALL.asciidoc#on-debian--ubuntu[INSTALL].
|
||||
+
|
||||
Since Ubuntu Trusty (using Qt 5.2.1),
|
||||
https://bugreports.qt.io/browse/QTBUG-42417?jql=component%20%3D%20WebKit%20and%20resolution%20%3D%20Done%20and%20fixVersion%20in%20(5.3.0%2C%20%225.3.0%20Alpha%22%2C%20%225.3.0%20Beta1%22%2C%20%225.3.0%20RC1%22%2C%205.3.1%2C%205.3.2%2C%205.4.0%2C%20%225.4.0%20Alpha%22%2C%20%225.4.0%20Beta%22%2C%20%225.4.0%20RC%22)%20and%20priority%20in%20(%22P2%3A%20Important%22%2C%20%22P1%3A%20Critical%22%2C%20%22P0%3A%20Blocker%22)[over
|
||||
70 important bugs] have been fixed in QtWebKit. For Debian Jessie (using Qt 5.3.2)
|
||||
it's still
|
||||
https://bugreports.qt.io/browse/QTBUG-42417?jql=component%20%3D%20WebKit%20and%20resolution%20%3D%20Done%20and%20fixVersion%20in%20(5.4.0%2C%20%225.4.0%20Alpha%22%2C%20%225.4.0%20Beta%22%2C%20%225.4.0%20RC%22)%20and%20priority%20in%20(%22P2%3A%20Important%22%2C%20%22P1%3A%20Critical%22%2C%20%22P0%3A%20Blocker%22)[nearly
|
||||
20 important bugs].
|
||||
some packages. There is currently no easy way to manually upgrade to Qt
|
||||
5.4 on those systems.
|
||||
|
||||
My issue is not listed.::
|
||||
If you experience any segfaults or crashes, you can report the issue in
|
||||
@@ -1,5 +1,5 @@
|
||||
Contributing to qutebrowser
|
||||
===========================
|
||||
qutebrowser HACKING
|
||||
===================
|
||||
The Compiler <mail@qutebrowser.org>
|
||||
:icons:
|
||||
:data-uri:
|
||||
@@ -37,6 +37,8 @@ If you want to find something useful to do, check the
|
||||
https://github.com/The-Compiler/qutebrowser/issues[issue tracker]. Some
|
||||
pointers:
|
||||
|
||||
* https://github.com/The-Compiler/qutebrowser/milestones/v0.1[Open issues for
|
||||
the v0.1 release]
|
||||
* https://github.com/The-Compiler/qutebrowser/labels/easy[Issues which should
|
||||
be easy to solve]
|
||||
* https://github.com/The-Compiler/qutebrowser/labels/not%20code[Issues which
|
||||
@@ -55,7 +57,7 @@ qutebrowser uses http://git-scm.com/[git] for its development. You can clone
|
||||
the repo like this:
|
||||
|
||||
----
|
||||
git clone https://github.com/The-Compiler/qutebrowser.git
|
||||
git clone git://the-compiler.org/qutebrowser
|
||||
----
|
||||
|
||||
If you don't know git, a http://git-scm.com/[git cheatsheet] might come in
|
||||
@@ -66,13 +68,8 @@ contributing, feel free to send normal patches instead, e.g. generated via
|
||||
Getting patches
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
The preferred way of submitting changes is to
|
||||
https://help.github.com/articles/fork-a-repo/[fork the repository] and to
|
||||
https://help.github.com/articles/creating-a-pull-request/[submit a pull
|
||||
request].
|
||||
|
||||
If you prefer to send a patch to the mailinglist, you can generate a patch
|
||||
based on your changes like this:
|
||||
After you finished your work and did `git commit`, you can get patches of your
|
||||
changes like this:
|
||||
|
||||
----
|
||||
git format-patch origin/master <1>
|
||||
@@ -86,26 +83,32 @@ Useful utilities
|
||||
Checkers
|
||||
~~~~~~~~
|
||||
|
||||
qutebrowser uses http://tox.readthedocs.org/en/latest/[tox] to run its
|
||||
unittests and several linters/checkers.
|
||||
In the _scripts/_ subfolder, there is a `run_checks.py` script.
|
||||
|
||||
Currently, the following tools will be invoked when you run `tox`:
|
||||
It runs a bunch of static checks on all source files, using the following
|
||||
checkers:
|
||||
|
||||
* Unit tests using https://www.pytest.org[pytest].
|
||||
* https://pypi.python.org/pypi/pyflakes[pyflakes] via https://pypi.python.org/pypi/pytest-flakes[pytest-flakes]
|
||||
* https://pypi.python.org/pypi/pep8[pep8] via https://pypi.python.org/pypi/pytest-pep8[pytest-pep8]
|
||||
* https://pypi.python.org/pypi/mccabe[mccabe] via https://pypi.python.org/pypi/pytest-mccabe[pytest-mccabe]
|
||||
* Unit tests using the Python
|
||||
https://docs.python.org/3.4/library/unittest.html[unittest] framework
|
||||
* https://pypi.python.org/pypi/flake8/1.3.1[flake8]
|
||||
* https://github.com/GreenSteam/pep257/[pep257]
|
||||
* http://pylint.org/[pylint]
|
||||
* https://pypi.python.org/pypi/pyroma/[pyroma]
|
||||
* https://github.com/mgedmin/check-manifest[check-manifest]
|
||||
* `scripts/misc_checks.py` which checks for the following things:
|
||||
* A custom checker for the following things:
|
||||
- untracked git files
|
||||
- VCS conflict markers
|
||||
|
||||
Please make sure the checks run without any warnings on your new contributions.
|
||||
There's of course the possibility of false-positives, and the following
|
||||
techniques are useful to handle these:
|
||||
If you changed `setup.py` or `MANIFEST.in`, add the `--setup` argument to run
|
||||
the following additional checkers:
|
||||
|
||||
* https://pypi.python.org/pypi/pyroma/0.9.3[pyroma]
|
||||
* https://github.com/mgedmin/check-manifest[check-manifest]
|
||||
|
||||
It needs all the checkers to be installed and also needs
|
||||
https://pypi.python.org/pypi/colorama/[colorama].
|
||||
|
||||
Please make sure this script runs without any warnings on your new
|
||||
contributions. There's of course the possibility of false-positives, and the
|
||||
following techniques are useful to handle these:
|
||||
|
||||
* Use `_foo` for unused parameters, with `foo` being a descriptive name. Using
|
||||
`_` is discouraged.
|
||||
@@ -153,7 +156,7 @@ Useful websites
|
||||
|
||||
Some resources which might be handy:
|
||||
|
||||
* http://doc.qt.io/qt-5/classes.html[The Qt5 reference]
|
||||
* http://qt-project.org/doc/qt-5/classes.html[The Qt5 reference]
|
||||
* https://docs.python.org/3/library/index.html[The Python reference]
|
||||
* http://httpbin.org/[httpbin, a test service for HTTP requests/responses]
|
||||
* http://requestb.in/[RequestBin, a service to inspect HTTP requests]
|
||||
@@ -211,7 +214,8 @@ Other
|
||||
Languages] (http://www.rfc-editor.org/errata_search.php?rfc=5646[Errata])
|
||||
* http://www.w3.org/TR/CSS2/[Cascading Style Sheets Level 2 Revision 1 (CSS
|
||||
2.1) Specification]
|
||||
* http://doc.qt.io/qt-5/stylesheet-reference.html[Qt Style Sheets Reference]
|
||||
* http://qt-project.org/doc/qt-4.8/stylesheet-reference.html[Qt Style Sheets
|
||||
Reference]
|
||||
* http://mimesniff.spec.whatwg.org/[MIME Sniffing Standard]
|
||||
* http://spec.whatwg.org/[WHATWG specifications]
|
||||
* http://www.w3.org/html/wg/drafts/html/master/Overview.html[HTML 5.1 Nightly]
|
||||
@@ -237,7 +241,9 @@ There are some exceptions to that:
|
||||
|
||||
* `QThread` is used instead of Python threads because it provides signals and
|
||||
slots.
|
||||
* `QProcess` is used instead of Python's `subprocess`
|
||||
* `QProcess` is used instead of Python's `subprocess` if certain actions (e.g.
|
||||
cleanup) when the process finished are desired, as it provides signals for
|
||||
that.
|
||||
* `QUrl` is used instead of storing URLs as string, see the
|
||||
<<handling-urls,handling URLs>> section for details.
|
||||
|
||||
@@ -276,7 +282,7 @@ There are currently these object registries, also called 'scopes':
|
||||
`cookie-jar`, etc.)
|
||||
* The `tab` scope with objects which are per-tab (`hintmanager`, `webview`,
|
||||
etc.). Passing this scope to `objreg.get()` selects the object in the currently
|
||||
focused tab by default. A tab can be explicitly selected by passing
|
||||
focused tab by default. A tab can be explicitely selected by passing
|
||||
+tab=_tab-id_, window=_win-id_+ to it.
|
||||
|
||||
A new object can be registered by using
|
||||
@@ -292,8 +298,8 @@ All objects can be printed by starting with the `--debug` flag and using the
|
||||
|
||||
The registry is mainly used for <<commands,command handlers>> but also can be
|
||||
useful in places where using Qt's
|
||||
http://doc.qt.io/qt-5/signalsandslots.html[signals and slots] mechanism would
|
||||
be difficult.
|
||||
http://qt-project.org/doc/qt-5/signalsandslots.html[signals and slots]
|
||||
mechanism would be difficult.
|
||||
|
||||
Logging
|
||||
~~~~~~~
|
||||
@@ -371,7 +377,7 @@ The types of the function arguments are inferred based on their default values,
|
||||
e.g. an argument `foo=True` will be converted to a flag `-f`/`--foo` in
|
||||
qutebrowser's commandline.
|
||||
|
||||
This behavior can be overridden using Python's
|
||||
This behaviour can be overridden using Python's
|
||||
http://legacy.python.org/dev/peps/pep-3107/[function annotations]. The
|
||||
annotation should always be a `dict`, like this:
|
||||
|
||||
@@ -395,12 +401,13 @@ then automatically checked. Possible values:
|
||||
e.g. `('foo', 'bar')` or `(int, 'foo')`.
|
||||
* `flag`: The flag to be used, as 1-char string (default: First char of the
|
||||
long name).
|
||||
* `name`: The long name to be used, as string (default: Name of the parameter).
|
||||
* `special`: The string `count` or `win_id` if the parameter should be
|
||||
auto-filled (with the count given by the user and the window ID the command was
|
||||
executed in, respectively).
|
||||
* `nargs`: Gets passed to argparse, see
|
||||
https://docs.python.org/dev/library/argparse.html#nargs[its documentation].
|
||||
|
||||
The name of an argument will always be the parameter name, with any trailing
|
||||
underscores stripped.
|
||||
|
||||
[[handling-urls]]
|
||||
Handling URLs
|
||||
~~~~~~~~~~~~~
|
||||
@@ -429,30 +436,6 @@ displaying it to the user.
|
||||
`QUrl` and take appropriate action if not. Note the URL of the current page
|
||||
always could be an invalid QUrl (if nothing is loaded yet).
|
||||
|
||||
Running valgrind on QtWebKit
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you want to run qutebrowser (and thus QtWebKit) with
|
||||
http://valgrind.org/[valgrind], you'll need to pass `--smc-check=all` to it or
|
||||
recompile QtWebKit with the Javascript JIT disabled.
|
||||
|
||||
This is needed so valgrind handles self-modifying code correctly:
|
||||
|
||||
[quote]
|
||||
____
|
||||
This option controls Valgrind's detection of self-modifying code. If no
|
||||
checking is done, if a program executes some code, then overwrites it with new
|
||||
code, and executes the new code, Valgrind will continue to execute the
|
||||
translations it made for the old code. This will likely lead to incorrect
|
||||
behavior and/or crashes.
|
||||
|
||||
...
|
||||
|
||||
Note that the default option will catch the vast majority of cases. The main
|
||||
case it will not catch is programs such as JIT compilers that dynamically
|
||||
generate code and subsequently overwrite part or all of it. Running with all
|
||||
will slow Valgrind down noticeably.
|
||||
____
|
||||
|
||||
Style conventions
|
||||
-----------------
|
||||
@@ -538,15 +521,18 @@ New Qt release
|
||||
|
||||
* Run all tests and check nothing is broken.
|
||||
* Check the
|
||||
https://bugreports.qt.io/issues/?jql=reporter%20%3D%20%22The%20Compiler%22%20ORDER%20BY%20fixVersion%20ASC[Qt bugtracker]
|
||||
https://bugreports.qt-project.org/issues/?jql=reporter%20%3D%20%22The%20Compiler%22%20ORDER%20BY%20fixVersion%20ASC[Qt bugtracker]
|
||||
and make sure all bugs marked as resolved are actually fixed.
|
||||
* Update own PKGBUILDs based on upstream Archlinux updates and rebuild.
|
||||
* Update own PKGBUILDs based on upstream Archlinux updates.
|
||||
* Build developer packages.
|
||||
* Build non-developer symbol packages.
|
||||
* Upload symbols patch to http://www.qutebrowser.org/qt-symbols.patch
|
||||
* Upload symbols packages to http://www.qutebrowser.org/qt-symbols-pkg/
|
||||
* Update recommended Qt version in `README`
|
||||
* Update OS X instructions in `README`
|
||||
* Make sure Gentoo instructions are up to date.
|
||||
* Grep for `WORKAROUND` in the code and test if fixed stuff works without the
|
||||
workaround.
|
||||
* Check relevant
|
||||
https://github.com/The-Compiler/qutebrowser/issues?q=is%3Aopen+is%3Aissue+label%3Aqt[qutebrowser
|
||||
bugs] and check if they're fixed.
|
||||
|
||||
qutebrowser release
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
@@ -560,15 +546,17 @@ qutebrowser release
|
||||
* Test an upgrade from the previous version (no manual intervention).
|
||||
* Test an upgrade from the first version (no manual intervention).
|
||||
|
||||
* Create annotated git tag (`git tag -s "v0.X.Y" -m "Release v0.X.Y"`)
|
||||
* If it's a new minor, create git branch `v0.X.x`
|
||||
* `git push`; `git push "v0.X.Y"`
|
||||
* Create annotated git tag (`git tag -s "v0.1" -m "Release v0.1"`)
|
||||
* Create git branch `v0.1.x`
|
||||
* Push including `--tags`
|
||||
* Create release on github
|
||||
* Mark the milestone at https://github.com/The-Compiler/qutebrowser/milestones
|
||||
as closed.
|
||||
|
||||
* Create standalone Windows package (32/64bit) in Windows VM
|
||||
* Upload to PyPI: `python setup.py register sdist upload --sign`
|
||||
* Upload to qutebrowser.org with checksum/GPG
|
||||
* Maybe upload to http://qt-apps.org/
|
||||
* Upload to webpage with checksum/GPG (when/if it exists)
|
||||
|
||||
* Announce to qutebrowser mailinglist
|
||||
* Maybe annouce at other places?
|
||||
@@ -12,41 +12,8 @@ qutebrowser should run on these systems:
|
||||
|
||||
Install the dependencies via apt-get:
|
||||
|
||||
[NOTE]
|
||||
==========================
|
||||
On Debian, it's recommended to install the Qt packages from the
|
||||
https://wiki.debian.org/DebianExperimental[experimental] repository as those
|
||||
are a much newer version of Qt which is more stable.
|
||||
|
||||
Add the following line to your `/etc/apt/sources.list`:
|
||||
|
||||
----
|
||||
deb http://ftp.debian.org/debian experimental main
|
||||
----
|
||||
|
||||
Then install the packages like this:
|
||||
|
||||
----
|
||||
# apt-get update
|
||||
# apt-get install -t experimental python3-pyqt5 python3-pyqt5.qtwebkit
|
||||
# apt-get install python-tox
|
||||
----
|
||||
|
||||
It's also recommended to pin those packages to receive updates by creating a
|
||||
file `/etc/apt/preferences.d/qutebrowser` with the following contents:
|
||||
|
||||
----
|
||||
Package: python3-pyqt5* libqt5*
|
||||
Pin: release a=experimental
|
||||
Pin-Priority: 800
|
||||
----
|
||||
==========================
|
||||
|
||||
For distributions other than Debian or if you prefer to not use the
|
||||
experimental repo:
|
||||
|
||||
----
|
||||
# apt-get install python3-pyqt5 python3-pyqt5.qtwebkit python-tox
|
||||
# apt-get install python3-pyqt5 python3-pyqt5.qtwebkit python-virtualenv
|
||||
----
|
||||
|
||||
To generate the documentation for the `:help` command, when using the git
|
||||
@@ -54,29 +21,27 @@ repository (rather than a release):
|
||||
|
||||
----
|
||||
# apt-get install asciidoc
|
||||
$ python3 scripts/asciidoc2html.py
|
||||
# python3 scripts/asciidoc2html.py
|
||||
----
|
||||
|
||||
Then run tox like this to set up a
|
||||
https://docs.python.org/3/library/venv.html[virtual environment]:
|
||||
Then run the supplied script to run qutebrowser inside a
|
||||
https://virtualenv.pypa.io/en/latest/virtualenv.html[virtualenv]:
|
||||
|
||||
----
|
||||
$ tox -e mkvenv
|
||||
# python3 scripts/init_venv.py
|
||||
----
|
||||
|
||||
This installs all needed Python dependencies in a `.venv` subfolder. The
|
||||
system-wide Qt5/PyQt5 installations are symlinked into the virtual environment.
|
||||
system-wide Qt5/PyQt5 installations are symlinked into the virtualenv.
|
||||
|
||||
You can then create a simple wrapper script to start qutebrowser somewhere in
|
||||
your `$PATH` (e.g. `/usr/local/bin/qutebrowser` or `~/bin/qutebrowser`):
|
||||
|
||||
----
|
||||
#!/bin/bash
|
||||
~/path/to/qutebrowser/.venv/bin/python3 -m qutebrowser "$@"
|
||||
~/path/to/qutebrowser/.venv/bin/python3 -m qutebrowser
|
||||
----
|
||||
|
||||
Please also read about <<updating,updating qutebrowser with tox>>.
|
||||
|
||||
On Archlinux
|
||||
------------
|
||||
|
||||
@@ -84,22 +49,13 @@ There are two Archlinux packages available in the AUR:
|
||||
https://aur.archlinux.org/packages/qutebrowser/[qutebrowser] and
|
||||
https://aur.archlinux.org/packages/qutebrowser-git/[qutebrowser-git].
|
||||
|
||||
You can install them (and the needed pypeg2 dependency) like this:
|
||||
You can install them like this:
|
||||
|
||||
----
|
||||
$ wget https://aur.archlinux.org/packages/py/python-pypeg2/python-pypeg2.tar.gz
|
||||
$ tar xzf python-pypeg2.tar.gz
|
||||
$ cd python-pypeg2
|
||||
$ makepkg -si
|
||||
$ cd ..
|
||||
$ rm -r python-pypeg2 python-pypeg2.tar.gz
|
||||
|
||||
$ wget https://aur.archlinux.org/packages/qu/qutebrowser/qutebrowser.tar.gz
|
||||
$ tar xzf qutebrowser.tar.gz
|
||||
$ mkdir qutebrowser
|
||||
$ cd qutebrowser
|
||||
$ wget https://aur.archlinux.org/packages/qu/qutebrowser-git/PKGBUILD
|
||||
$ makepkg -si
|
||||
$ cd ..
|
||||
$ rm -r qutebrowser qutebrowser.tar.gz
|
||||
----
|
||||
|
||||
or you could use an AUR helper, e.g. `yaourt -S qutebrowser-git`.
|
||||
@@ -112,6 +68,7 @@ https://github.com/posativ/qutebrowser-overlay[GitHub]. To install it, add the
|
||||
overlay with http://wiki.gentoo.org/wiki/Layman[layman]:
|
||||
|
||||
----
|
||||
# wget https://raw.githubusercontent.com/posativ/qutebrowser-overlay/master/overlays.xml -O /etc/layman/overlays/qutebrowser.xml
|
||||
# layman -a qutebrowser
|
||||
----
|
||||
|
||||
@@ -124,16 +81,6 @@ in your `PYTHON_TARGETS` (`/etc/portage/make.conf`) and rebuild your system
|
||||
# emerge -av qutebrowser
|
||||
----
|
||||
|
||||
On Void Linux
|
||||
-------------
|
||||
|
||||
qutebrowser is available in the official repositories and can be installed
|
||||
with:
|
||||
|
||||
----
|
||||
# xbps-install qutebrowser
|
||||
----
|
||||
|
||||
On Windows
|
||||
----------
|
||||
|
||||
@@ -146,24 +93,19 @@ Python 3 (be sure to install pip).
|
||||
* Use the installer from
|
||||
http://www.riverbankcomputing.com/software/pyqt/download5[Riverbank computing]
|
||||
to get Qt and PyQt5.
|
||||
* Install https://testrun.org/tox/latest/index.html[tox] via
|
||||
https://pip.pypa.io/en/latest/[pip]:
|
||||
* Run `pip install virtualenv` or
|
||||
http://www.lfd.uci.edu/~gohlke/pythonlibs/#virtualenv[the installer from here]
|
||||
to install virtualenv.
|
||||
|
||||
Then run the supplied script to run qutebrowser inside a
|
||||
https://virtualenv.pypa.io/en/latest/virtualenv.html[virtualenv]:
|
||||
|
||||
----
|
||||
$ pip install tox
|
||||
----
|
||||
|
||||
Then run tox like this to set up a
|
||||
https://docs.python.org/3/library/venv.html[virtual environment]:
|
||||
|
||||
----
|
||||
$ tox -e mkvenv
|
||||
# python3 scripts/init_venv.py
|
||||
----
|
||||
|
||||
This installs all needed Python dependencies in a `.venv` subfolder. The
|
||||
system-wide Qt5/PyQt5 installations are used in the virtual environment.
|
||||
|
||||
Please also read about <<updating,updating qutebrowser with tox>>.
|
||||
system-wide Qt5/PyQt5 installations are used in the virtualenv.
|
||||
|
||||
On OS X
|
||||
-------
|
||||
@@ -198,17 +140,3 @@ standard location for your distro (`/usr/share/applications` and
|
||||
|
||||
The normal `setup.py install` doesn't install these files, so you'll have to do
|
||||
it as part of the packaging process.
|
||||
|
||||
[[updating]]
|
||||
Updating qutebrowser with tox
|
||||
-----------------------------
|
||||
|
||||
When you updated your local copy of the code (e.g. by pulling the git repo, or
|
||||
extracting a new version), the virtualenv should automatically use the updated
|
||||
code. However, if dependencies got added, this won't be reflected in the
|
||||
virtualenv. Thus it's recommended to run the following command to recreate the
|
||||
virtualenv:
|
||||
|
||||
----
|
||||
$ tox -r -e mkvenv
|
||||
----
|
||||
@@ -8,19 +8,15 @@
|
||||
|<<adblock-update,adblock-update>>|Update the adblock block lists.
|
||||
|<<back,back>>|Go back in the history of the current tab.
|
||||
|<<bind,bind>>|Bind a key to a command.
|
||||
|<<cancel-download,cancel-download>>|Cancel the first/[count]th download.
|
||||
|<<close,close>>|Close the current window.
|
||||
|<<download,download>>|Download a given URL, or current page if no URL given.
|
||||
|<<download-cancel,download-cancel>>|Cancel the last/[count]th download.
|
||||
|<<download-delete,download-delete>>|Delete the last/[count]th download from disk.
|
||||
|<<download-open,download-open>>|Open the last/[count]th download.
|
||||
|<<download-remove,download-remove>>|Remove the last/[count]th download from the list.
|
||||
|<<download,download>>|Download a given URL, given as string.
|
||||
|<<download-page,download-page>>|Download the current page.
|
||||
|<<forward,forward>>|Go forward in the history of the current tab.
|
||||
|<<fullscreen,fullscreen>>|Toggle fullscreen mode.
|
||||
|<<help,help>>|Show help about a command or setting.
|
||||
|<<hint,hint>>|Start hinting.
|
||||
|<<home,home>>|Open main startpage in current tab.
|
||||
|<<inspector,inspector>>|Toggle the web inspector.
|
||||
|<<jseval,jseval>>|Evaluate a JavaScript string.
|
||||
|<<later,later>>|Execute a command after some time.
|
||||
|<<navigate,navigate>>|Open typical prev/next links or navigate using the URL path.
|
||||
|<<open,open>>|Open a URL in the current/[count]th tab.
|
||||
@@ -35,18 +31,14 @@
|
||||
|<<repeat,repeat>>|Repeat a given command.
|
||||
|<<report,report>>|Report a bug in qutebrowser.
|
||||
|<<restart,restart>>|Restart qutebrowser while keeping existing tabs open.
|
||||
|<<save,save>>|Save configs and state.
|
||||
|<<search,search>>|Search for a text on the current page. With no text, clear results.
|
||||
|<<session-delete,session-delete>>|Delete a session.
|
||||
|<<session-load,session-load>>|Load a session.
|
||||
|<<session-save,session-save>>|Save a session.
|
||||
|<<run-userscript,run-userscript>>|Run an userscript given as argument.
|
||||
|<<save,save>>|Save the config file.
|
||||
|<<set,set>>|Set an option.
|
||||
|<<set-cmd-text,set-cmd-text>>|Preset the statusbar to some text.
|
||||
|<<spawn,spawn>>|Spawn a command in a shell.
|
||||
|<<stop,stop>>|Stop loading in the current/[count]th tab.
|
||||
|<<tab-clone,tab-clone>>|Duplicate the current tab.
|
||||
|<<tab-close,tab-close>>|Close the current/[count]th tab.
|
||||
|<<tab-detach,tab-detach>>|Detach the current tab to its own window.
|
||||
|<<tab-focus,tab-focus>>|Select the tab given as argument/[count].
|
||||
|<<tab-move,tab-move>>|Move the current tab.
|
||||
|<<tab-next,tab-next>>|Switch to the next tab, or switch [count] tabs forward.
|
||||
@@ -55,7 +47,6 @@
|
||||
|<<unbind,unbind>>|Unbind a keychain.
|
||||
|<<undo,undo>>|Re-open a closed tab (optionally skipping [count] closed tabs).
|
||||
|<<view-source,view-source>>|Show the source of the current page.
|
||||
|<<wq,wq>>|Save open pages and quit.
|
||||
|<<yank,yank>>|Yank the current URL/title to the clipboard or primary selection.
|
||||
|<<zoom,zoom>>|Set the zoom level for the current tab.
|
||||
|<<zoom-in,zoom-in>>|Increase the zoom level for the current tab.
|
||||
@@ -81,7 +72,7 @@ How many pages to go back.
|
||||
|
||||
[[bind]]
|
||||
=== bind
|
||||
Syntax: +:bind [*--mode* 'MODE'] [*--force*] 'key' 'command'+
|
||||
Syntax: +:bind [*--mode* 'MODE'] 'key' 'command'+
|
||||
|
||||
Bind a key to a command.
|
||||
|
||||
@@ -92,11 +83,13 @@ Bind a key to a command.
|
||||
==== optional arguments
|
||||
* +*-m*+, +*--mode*+: A comma-separated list of modes to bind the key in (default: `normal`).
|
||||
|
||||
* +*-f*+, +*--force*+: Rebind the key if it is already bound.
|
||||
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
|
||||
[[cancel-download]]
|
||||
=== cancel-download
|
||||
Cancel the first/[count]th download.
|
||||
|
||||
==== count
|
||||
The index of the download to cancel.
|
||||
|
||||
[[close]]
|
||||
=== close
|
||||
@@ -104,46 +97,17 @@ Close the current window.
|
||||
|
||||
[[download]]
|
||||
=== download
|
||||
Syntax: +:download ['url'] ['dest']+
|
||||
Syntax: +:download 'url' ['dest']+
|
||||
|
||||
Download a given URL, or current page if no URL given.
|
||||
Download a given URL, given as string.
|
||||
|
||||
==== positional arguments
|
||||
* +'url'+: The URL to download. If not given, download the current page.
|
||||
* +'dest'+: The file path to write the download to, or not given to ask.
|
||||
* +'url'+: The URL to download
|
||||
* +'dest'+: The file path to write the download to to ask.
|
||||
|
||||
[[download-cancel]]
|
||||
=== download-cancel
|
||||
Cancel the last/[count]th download.
|
||||
|
||||
==== count
|
||||
The index of the download to cancel.
|
||||
|
||||
[[download-delete]]
|
||||
=== download-delete
|
||||
Delete the last/[count]th download from disk.
|
||||
|
||||
==== count
|
||||
The index of the download to cancel.
|
||||
|
||||
[[download-open]]
|
||||
=== download-open
|
||||
Open the last/[count]th download.
|
||||
|
||||
==== count
|
||||
The index of the download to cancel.
|
||||
|
||||
[[download-remove]]
|
||||
=== download-remove
|
||||
Syntax: +:download-remove [*--all*]+
|
||||
|
||||
Remove the last/[count]th download from the list.
|
||||
|
||||
==== optional arguments
|
||||
* +*-a*+, +*--all*+: If given removes all finished downloads.
|
||||
|
||||
==== count
|
||||
The index of the download to cancel.
|
||||
[[download-page]]
|
||||
=== download-page
|
||||
Download the current page.
|
||||
|
||||
[[forward]]
|
||||
=== forward
|
||||
@@ -159,10 +123,6 @@ Go forward in the history of the current tab.
|
||||
==== count
|
||||
How many pages to go forward.
|
||||
|
||||
[[fullscreen]]
|
||||
=== fullscreen
|
||||
Toggle fullscreen mode.
|
||||
|
||||
[[help]]
|
||||
=== help
|
||||
Syntax: +:help [*--tab*] [*--bg*] [*--window*] ['topic']+
|
||||
@@ -183,7 +143,7 @@ Show help about a command or setting.
|
||||
|
||||
[[hint]]
|
||||
=== hint
|
||||
Syntax: +:hint [*--rapid*] ['group'] ['target'] ['args' ['args' ...]]+
|
||||
Syntax: +:hint ['group'] ['target'] ['args' ['args' ...]]+
|
||||
|
||||
Start hinting.
|
||||
|
||||
@@ -199,9 +159,7 @@ Start hinting.
|
||||
* +'target'+: What to do with the selected element.
|
||||
|
||||
- `normal`: Open the link in the current tab.
|
||||
- `tab`: Open the link in a new tab (honoring the
|
||||
background-tabs setting).
|
||||
- `tab-fg`: Open the link in a new foreground tab.
|
||||
- `tab`: Open the link in a new tab.
|
||||
- `tab-bg`: Open the link in a new background tab.
|
||||
- `window`: Open the link in a new window.
|
||||
- `hover` : Hover over the link.
|
||||
@@ -210,6 +168,9 @@ Start hinting.
|
||||
- `run`: Run the argument as command.
|
||||
- `fill`: Fill the commandline with the command given as
|
||||
argument.
|
||||
- `rapid`: Open the link in a new tab and stay in hinting mode.
|
||||
- `rapid-win`: Open the link in a new window and stay in
|
||||
hinting mode.
|
||||
- `download`: Download the link.
|
||||
- `userscript`: Call an userscript with `$QUTE_URL` set to the
|
||||
link.
|
||||
@@ -229,11 +190,6 @@ Start hinting.
|
||||
- With `run`: Same as `fill`.
|
||||
|
||||
|
||||
==== optional arguments
|
||||
* +*-r*+, +*--rapid*+: Whether to do rapid hinting. This is only possible with targets `tab` (with background-tabs=true), `tab-bg`,
|
||||
`window`, `run`, `hover`, `userscript` and `spawn`.
|
||||
|
||||
|
||||
[[home]]
|
||||
=== home
|
||||
Open main startpage in current tab.
|
||||
@@ -242,22 +198,6 @@ Open main startpage in current tab.
|
||||
=== inspector
|
||||
Toggle the web inspector.
|
||||
|
||||
[[jseval]]
|
||||
=== jseval
|
||||
Syntax: +:jseval [*--quiet*] 'js-code'+
|
||||
|
||||
Evaluate a JavaScript string.
|
||||
|
||||
==== positional arguments
|
||||
* +'js-code'+: The string to evaluate.
|
||||
|
||||
==== optional arguments
|
||||
* +*-q*+, +*--quiet*+: Don't show resulting JS object.
|
||||
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
|
||||
|
||||
[[later]]
|
||||
=== later
|
||||
Syntax: +:later 'ms' 'command'+
|
||||
@@ -268,10 +208,6 @@ Execute a command after some time.
|
||||
* +'ms'+: How many milliseconds to wait.
|
||||
* +'command'+: The command to run, with optional args.
|
||||
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
|
||||
|
||||
[[navigate]]
|
||||
=== navigate
|
||||
Syntax: +:navigate [*--tab*] [*--bg*] [*--window*] 'where'+
|
||||
@@ -299,7 +235,7 @@ This tries to automatically click on typical _Previous Page_ or _Next Page_ link
|
||||
|
||||
[[open]]
|
||||
=== open
|
||||
Syntax: +:open [*--bg*] [*--tab*] [*--window*] ['url']+
|
||||
Syntax: +:open [*--bg*] [*--tab*] [*--window*] 'url'+
|
||||
|
||||
Open a URL in the current/[count]th tab.
|
||||
|
||||
@@ -314,10 +250,6 @@ Open a URL in the current/[count]th tab.
|
||||
==== count
|
||||
The tab index to open the URL in.
|
||||
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
|
||||
|
||||
[[paste]]
|
||||
=== paste
|
||||
Syntax: +:paste [*--sel*] [*--tab*] [*--bg*] [*--window*]+
|
||||
@@ -361,10 +293,6 @@ Delete a quickmark.
|
||||
==== positional arguments
|
||||
* +'name'+: The name of the quickmark to delete.
|
||||
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
|
||||
|
||||
[[quickmark-load]]
|
||||
=== quickmark-load
|
||||
Syntax: +:quickmark-load [*--tab*] [*--bg*] [*--window*] 'name'+
|
||||
@@ -379,10 +307,6 @@ Load a quickmark.
|
||||
* +*-b*+, +*--bg*+: Load the quickmark in a new background tab.
|
||||
* +*-w*+, +*--window*+: Load the quickmark in a new window.
|
||||
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
|
||||
|
||||
[[quickmark-save]]
|
||||
=== quickmark-save
|
||||
Save the current page as a quickmark.
|
||||
@@ -393,13 +317,8 @@ Quit qutebrowser.
|
||||
|
||||
[[reload]]
|
||||
=== reload
|
||||
Syntax: +:reload [*--force*]+
|
||||
|
||||
Reload the current/[count]th tab.
|
||||
|
||||
==== optional arguments
|
||||
* +*-f*+, +*--force*+: Bypass the page cache.
|
||||
|
||||
==== count
|
||||
The tab index to reload.
|
||||
|
||||
@@ -413,10 +332,6 @@ Repeat a given command.
|
||||
* +'times'+: How many times to repeat.
|
||||
* +'command'+: The command to run, with optional args.
|
||||
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
|
||||
|
||||
[[report]]
|
||||
=== report
|
||||
Report a bug in qutebrowser.
|
||||
@@ -425,82 +340,27 @@ Report a bug in qutebrowser.
|
||||
=== restart
|
||||
Restart qutebrowser while keeping existing tabs open.
|
||||
|
||||
[[run-userscript]]
|
||||
=== run-userscript
|
||||
Syntax: +:run-userscript 'cmd' ['args' ['args' ...]]+
|
||||
|
||||
Run an userscript given as argument.
|
||||
|
||||
==== positional arguments
|
||||
* +'cmd'+: The userscript to run.
|
||||
* +'args'+: Arguments to pass to the userscript.
|
||||
|
||||
[[save]]
|
||||
=== save
|
||||
Syntax: +:save ['what' ['what' ...]]+
|
||||
|
||||
Save configs and state.
|
||||
|
||||
==== positional arguments
|
||||
* +'what'+: What to save (`config`/`key-config`/`cookies`/...). If not given, everything is saved.
|
||||
|
||||
|
||||
[[search]]
|
||||
=== search
|
||||
Syntax: +:search [*--reverse*] ['text']+
|
||||
|
||||
Search for a text on the current page. With no text, clear results.
|
||||
|
||||
==== positional arguments
|
||||
* +'text'+: The text to search for.
|
||||
|
||||
==== optional arguments
|
||||
* +*-r*+, +*--reverse*+: Reverse search direction.
|
||||
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
|
||||
|
||||
[[session-delete]]
|
||||
=== session-delete
|
||||
Syntax: +:session-delete [*--force*] 'name'+
|
||||
|
||||
Delete a session.
|
||||
|
||||
==== positional arguments
|
||||
* +'name'+: The name of the session.
|
||||
|
||||
==== optional arguments
|
||||
* +*-f*+, +*--force*+: Force deleting internal sessions (starting with an underline).
|
||||
|
||||
|
||||
[[session-load]]
|
||||
=== session-load
|
||||
Syntax: +:session-load [*--clear*] [*--temp*] [*--force*] 'name'+
|
||||
|
||||
Load a session.
|
||||
|
||||
==== positional arguments
|
||||
* +'name'+: The name of the session.
|
||||
|
||||
==== optional arguments
|
||||
* +*-c*+, +*--clear*+: Close all existing windows.
|
||||
* +*-t*+, +*--temp*+: Don't set the current session for :session-save.
|
||||
* +*-f*+, +*--force*+: Force loading internal sessions (starting with an underline).
|
||||
|
||||
|
||||
[[session-save]]
|
||||
=== session-save
|
||||
Syntax: +:session-save [*--current*] [*--quiet*] [*--force*] ['name']+
|
||||
|
||||
Save a session.
|
||||
|
||||
==== positional arguments
|
||||
* +'name'+: The name of the session. If not given, the session configured in general -> session-default-name is saved.
|
||||
|
||||
|
||||
==== optional arguments
|
||||
* +*-c*+, +*--current*+: Save the current session instead of the default.
|
||||
* +*-q*+, +*--quiet*+: Don't show confirmation message.
|
||||
* +*-f*+, +*--force*+: Force saving internal sessions (starting with an underline).
|
||||
Save the config file.
|
||||
|
||||
[[set]]
|
||||
=== set
|
||||
Syntax: +:set [*--temp*] [*--print*] ['section'] ['option'] ['value']+
|
||||
Syntax: +:set [*--temp*] ['section'] ['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 '!' and it is a boolean value, toggle it.
|
||||
If the option name ends with '?', the value of the option is shown instead.
|
||||
|
||||
==== positional arguments
|
||||
* +'section'+: The section where the option is in.
|
||||
@@ -509,43 +369,26 @@ If the option name ends with '?', the value of the option is shown instead. If t
|
||||
|
||||
==== optional arguments
|
||||
* +*-t*+, +*--temp*+: Set value temporarily.
|
||||
* +*-p*+, +*--print*+: Print the value after setting.
|
||||
|
||||
[[set-cmd-text]]
|
||||
=== set-cmd-text
|
||||
Syntax: +:set-cmd-text [*--space*] 'text'+
|
||||
Syntax: +:set-cmd-text 'text'+
|
||||
|
||||
Preset the statusbar to some text.
|
||||
|
||||
==== positional arguments
|
||||
* +'text'+: The commandline to set.
|
||||
|
||||
==== optional arguments
|
||||
* +*-s*+, +*--space*+: If given, a space is added to the end.
|
||||
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
|
||||
|
||||
[[spawn]]
|
||||
=== spawn
|
||||
Syntax: +:spawn [*--userscript*] [*--verbose*] [*--detach*] 'cmdline'+
|
||||
Syntax: +:spawn 'args' ['args' ...]+
|
||||
|
||||
Spawn a command in a shell.
|
||||
|
||||
Note the {url} variable which gets replaced by the current URL might be useful here.
|
||||
|
||||
==== positional arguments
|
||||
* +'cmdline'+: The commandline to execute.
|
||||
|
||||
==== optional arguments
|
||||
* +*-u*+, +*--userscript*+: Run the command as an userscript.
|
||||
* +*-v*+, +*--verbose*+: Show notifications when the command started/exited.
|
||||
* +*-d*+, +*--detach*+: Whether the command should be detached from qutebrowser.
|
||||
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
|
||||
* +'args'+: The commandline to execute.
|
||||
|
||||
[[stop]]
|
||||
=== stop
|
||||
@@ -573,16 +416,12 @@ Close the current/[count]th tab.
|
||||
==== optional arguments
|
||||
* +*-l*+, +*--left*+: Force selecting the tab to the left of the current tab.
|
||||
* +*-r*+, +*--right*+: Force selecting the tab to the right of the current tab.
|
||||
* +*-o*+, +*--opposite*+: Force selecting the tab in the opposite direction of what's configured in 'tabs->select-on-remove'.
|
||||
* +*-o*+, +*--opposite*+: Force selecting the tab in the oppsite direction of what's configured in 'tabs->select-on-remove'.
|
||||
|
||||
|
||||
==== count
|
||||
The tab index to close
|
||||
|
||||
[[tab-detach]]
|
||||
=== tab-detach
|
||||
Detach the current tab to its own window.
|
||||
|
||||
[[tab-focus]]
|
||||
=== tab-focus
|
||||
Syntax: +:tab-focus ['index']+
|
||||
@@ -653,25 +492,15 @@ Re-open a closed tab (optionally skipping [count] closed tabs).
|
||||
=== view-source
|
||||
Show the source of the current page.
|
||||
|
||||
[[wq]]
|
||||
=== wq
|
||||
Syntax: +:wq ['name']+
|
||||
|
||||
Save open pages and quit.
|
||||
|
||||
==== positional arguments
|
||||
* +'name'+: The name of the session.
|
||||
|
||||
[[yank]]
|
||||
=== yank
|
||||
Syntax: +:yank [*--title*] [*--sel*] [*--domain*]+
|
||||
Syntax: +:yank [*--title*] [*--sel*]+
|
||||
|
||||
Yank the current URL/title to the clipboard or primary selection.
|
||||
|
||||
==== optional arguments
|
||||
* +*-t*+, +*--title*+: Yank the title instead of the URL.
|
||||
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
|
||||
* +*-d*+, +*--domain*+: Yank only the scheme, domain, and port number.
|
||||
|
||||
[[zoom]]
|
||||
=== zoom
|
||||
@@ -707,35 +536,14 @@ How many steps to zoom out.
|
||||
[options="header",width="75%",cols="25%,75%"]
|
||||
|==============
|
||||
|Command|Description
|
||||
|<<clear-keychain,clear-keychain>>|Clear the currently entered key chain.
|
||||
|<<command-accept,command-accept>>|Execute the command currently in the commandline.
|
||||
|<<command-history-next,command-history-next>>|Go forward in the commandline history.
|
||||
|<<command-history-prev,command-history-prev>>|Go back in the commandline history.
|
||||
|<<completion-item-next,completion-item-next>>|Select the next completion item.
|
||||
|<<completion-item-prev,completion-item-prev>>|Select the previous completion item.
|
||||
|<<drop-selection,drop-selection>>|Drop selection and keep selection mode enabled.
|
||||
|<<enter-mode,enter-mode>>|Enter a key mode.
|
||||
|<<follow-hint,follow-hint>>|Follow the currently selected hint.
|
||||
|<<follow-selected,follow-selected>>|Follow the selected text.
|
||||
|<<leave-mode,leave-mode>>|Leave the mode we're currently in.
|
||||
|<<message-error,message-error>>|Show an error message in the statusbar.
|
||||
|<<message-info,message-info>>|Show an info message in the statusbar.
|
||||
|<<message-warning,message-warning>>|Show a warning message in the statusbar.
|
||||
|<<move-to-end-of-document,move-to-end-of-document>>|Move the cursor or selection to the end of the document.
|
||||
|<<move-to-end-of-line,move-to-end-of-line>>|Move the cursor or selection to the end of line.
|
||||
|<<move-to-end-of-next-block,move-to-end-of-next-block>>|Move the cursor or selection to the end of next block.
|
||||
|<<move-to-end-of-prev-block,move-to-end-of-prev-block>>|Move the cursor or selection to the end of previous block.
|
||||
|<<move-to-end-of-word,move-to-end-of-word>>|Move the cursor or selection to the end of the word.
|
||||
|<<move-to-next-char,move-to-next-char>>|Move the cursor or selection to the next char.
|
||||
|<<move-to-next-line,move-to-next-line>>|Move the cursor or selection to the next line.
|
||||
|<<move-to-next-word,move-to-next-word>>|Move the cursor or selection to the next word.
|
||||
|<<move-to-prev-char,move-to-prev-char>>|Move the cursor or selection to the previous char.
|
||||
|<<move-to-prev-line,move-to-prev-line>>|Move the cursor or selection to the prev line.
|
||||
|<<move-to-prev-word,move-to-prev-word>>|Move the cursor or selection to the previous word.
|
||||
|<<move-to-start-of-document,move-to-start-of-document>>|Move the cursor or selection to the start of the document.
|
||||
|<<move-to-start-of-line,move-to-start-of-line>>|Move the cursor or selection to the start of the line.
|
||||
|<<move-to-start-of-next-block,move-to-start-of-next-block>>|Move the cursor or selection to the start of next block.
|
||||
|<<move-to-start-of-prev-block,move-to-start-of-prev-block>>|Move the cursor or selection to the start of previous block.
|
||||
|<<open-editor,open-editor>>|Open an external editor with the currently selected form field.
|
||||
|<<prompt-accept,prompt-accept>>|Accept the current prompt.
|
||||
|<<prompt-no,prompt-no>>|Answer no to a yes/no prompt.
|
||||
@@ -753,19 +561,12 @@ How many steps to zoom out.
|
||||
|<<rl-unix-line-discard,rl-unix-line-discard>>|Remove chars backward from the cursor to the beginning of the line.
|
||||
|<<rl-unix-word-rubout,rl-unix-word-rubout>>|Remove chars from the cursor to the beginning of the word.
|
||||
|<<rl-yank,rl-yank>>|Paste the most recently deleted text.
|
||||
|<<scroll,scroll>>|Scroll the current tab in the given direction.
|
||||
|<<scroll,scroll>>|Scroll the current tab by 'count * dx/dy'.
|
||||
|<<scroll-page,scroll-page>>|Scroll the frame page-wise.
|
||||
|<<scroll-perc,scroll-perc>>|Scroll to a specific percentage of the page.
|
||||
|<<scroll-px,scroll-px>>|Scroll the current tab by 'count * dx/dy' pixels.
|
||||
|<<search-next,search-next>>|Continue the search to the ([count]th) next term.
|
||||
|<<search-prev,search-prev>>|Continue the search to the ([count]th) previous term.
|
||||
|<<toggle-selection,toggle-selection>>|Toggle caret selection mode.
|
||||
|<<yank-selected,yank-selected>>|Yank the selected text to the clipboard or primary selection.
|
||||
|==============
|
||||
[[clear-keychain]]
|
||||
=== clear-keychain
|
||||
Clear the currently entered key chain.
|
||||
|
||||
[[command-accept]]
|
||||
=== command-accept
|
||||
Execute the command currently in the commandline.
|
||||
@@ -786,10 +587,6 @@ Select the next completion item.
|
||||
=== completion-item-prev
|
||||
Select the previous completion item.
|
||||
|
||||
[[drop-selection]]
|
||||
=== drop-selection
|
||||
Drop selection and keep selection mode enabled.
|
||||
|
||||
[[enter-mode]]
|
||||
=== enter-mode
|
||||
Syntax: +:enter-mode 'mode'+
|
||||
@@ -803,139 +600,10 @@ Enter a key mode.
|
||||
=== follow-hint
|
||||
Follow the currently selected hint.
|
||||
|
||||
[[follow-selected]]
|
||||
=== follow-selected
|
||||
Syntax: +:follow-selected [*--tab*]+
|
||||
|
||||
Follow the selected text.
|
||||
|
||||
==== optional arguments
|
||||
* +*-t*+, +*--tab*+: Load the selected link in a new tab.
|
||||
|
||||
[[leave-mode]]
|
||||
=== leave-mode
|
||||
Leave the mode we're currently in.
|
||||
|
||||
[[message-error]]
|
||||
=== message-error
|
||||
Syntax: +:message-error 'text'+
|
||||
|
||||
Show an error message in the statusbar.
|
||||
|
||||
==== positional arguments
|
||||
* +'text'+: The text to show.
|
||||
|
||||
[[message-info]]
|
||||
=== message-info
|
||||
Syntax: +:message-info 'text'+
|
||||
|
||||
Show an info message in the statusbar.
|
||||
|
||||
==== positional arguments
|
||||
* +'text'+: The text to show.
|
||||
|
||||
[[message-warning]]
|
||||
=== message-warning
|
||||
Syntax: +:message-warning 'text'+
|
||||
|
||||
Show a warning message in the statusbar.
|
||||
|
||||
==== positional arguments
|
||||
* +'text'+: The text to show.
|
||||
|
||||
[[move-to-end-of-document]]
|
||||
=== move-to-end-of-document
|
||||
Move the cursor or selection to the end of the document.
|
||||
|
||||
[[move-to-end-of-line]]
|
||||
=== move-to-end-of-line
|
||||
Move the cursor or selection to the end of line.
|
||||
|
||||
[[move-to-end-of-next-block]]
|
||||
=== move-to-end-of-next-block
|
||||
Move the cursor or selection to the end of next block.
|
||||
|
||||
==== count
|
||||
How many blocks to move.
|
||||
|
||||
[[move-to-end-of-prev-block]]
|
||||
=== move-to-end-of-prev-block
|
||||
Move the cursor or selection to the end of previous block.
|
||||
|
||||
==== count
|
||||
How many blocks to move.
|
||||
|
||||
[[move-to-end-of-word]]
|
||||
=== move-to-end-of-word
|
||||
Move the cursor or selection to the end of the word.
|
||||
|
||||
==== count
|
||||
How many words to move.
|
||||
|
||||
[[move-to-next-char]]
|
||||
=== move-to-next-char
|
||||
Move the cursor or selection to the next char.
|
||||
|
||||
==== count
|
||||
How many lines to move.
|
||||
|
||||
[[move-to-next-line]]
|
||||
=== move-to-next-line
|
||||
Move the cursor or selection to the next line.
|
||||
|
||||
==== count
|
||||
How many lines to move.
|
||||
|
||||
[[move-to-next-word]]
|
||||
=== move-to-next-word
|
||||
Move the cursor or selection to the next word.
|
||||
|
||||
==== count
|
||||
How many words to move.
|
||||
|
||||
[[move-to-prev-char]]
|
||||
=== move-to-prev-char
|
||||
Move the cursor or selection to the previous char.
|
||||
|
||||
==== count
|
||||
How many chars to move.
|
||||
|
||||
[[move-to-prev-line]]
|
||||
=== move-to-prev-line
|
||||
Move the cursor or selection to the prev line.
|
||||
|
||||
==== count
|
||||
How many lines to move.
|
||||
|
||||
[[move-to-prev-word]]
|
||||
=== move-to-prev-word
|
||||
Move the cursor or selection to the previous word.
|
||||
|
||||
==== count
|
||||
How many words to move.
|
||||
|
||||
[[move-to-start-of-document]]
|
||||
=== move-to-start-of-document
|
||||
Move the cursor or selection to the start of the document.
|
||||
|
||||
[[move-to-start-of-line]]
|
||||
=== move-to-start-of-line
|
||||
Move the cursor or selection to the start of the line.
|
||||
|
||||
[[move-to-start-of-next-block]]
|
||||
=== move-to-start-of-next-block
|
||||
Move the cursor or selection to the start of next block.
|
||||
|
||||
==== count
|
||||
How many blocks to move.
|
||||
|
||||
[[move-to-start-of-prev-block]]
|
||||
=== move-to-start-of-prev-block
|
||||
Move the cursor or selection to the start of previous block.
|
||||
|
||||
==== count
|
||||
How many blocks to move.
|
||||
|
||||
[[open-editor]]
|
||||
=== open-editor
|
||||
Open an external editor with the currently selected form field.
|
||||
@@ -1034,20 +702,20 @@ This acts like readline's yank.
|
||||
|
||||
[[scroll]]
|
||||
=== scroll
|
||||
Syntax: +:scroll 'direction' ['dy']+
|
||||
Syntax: +:scroll 'dx' 'dy'+
|
||||
|
||||
Scroll the current tab in the given direction.
|
||||
Scroll the current tab by 'count * dx/dy'.
|
||||
|
||||
==== positional arguments
|
||||
* +'direction'+: In which direction to scroll (up/down/left/right/top/bottom).
|
||||
|
||||
* +'dx'+: How much to scroll in x-direction.
|
||||
* +'dy'+: How much to scroll in x-direction.
|
||||
|
||||
==== count
|
||||
multiplier
|
||||
|
||||
[[scroll-page]]
|
||||
=== scroll-page
|
||||
Syntax: +:scroll-page [*--top-navigate* 'ACTION'] [*--bottom-navigate* 'ACTION'] 'x' 'y'+
|
||||
Syntax: +:scroll-page 'x' 'y'+
|
||||
|
||||
Scroll the frame page-wise.
|
||||
|
||||
@@ -1055,12 +723,6 @@ Scroll the frame page-wise.
|
||||
* +'x'+: How many pages to scroll to the right.
|
||||
* +'y'+: How many pages to scroll down.
|
||||
|
||||
==== optional arguments
|
||||
* +*-t*+, +*--top-navigate*+: :navigate action (prev, decrement) to run when scrolling up at the top of the page.
|
||||
|
||||
* +*-b*+, +*--bottom-navigate*+: :navigate action (next, increment) to run when scrolling down at the bottom of the page.
|
||||
|
||||
|
||||
==== count
|
||||
multiplier
|
||||
|
||||
@@ -1081,19 +743,6 @@ The percentage can be given either as argument or as count. If no percentage is
|
||||
==== count
|
||||
Percentage to scroll.
|
||||
|
||||
[[scroll-px]]
|
||||
=== scroll-px
|
||||
Syntax: +:scroll-px 'dx' 'dy'+
|
||||
|
||||
Scroll the current tab by 'count * dx/dy' pixels.
|
||||
|
||||
==== positional arguments
|
||||
* +'dx'+: How much to scroll in x-direction.
|
||||
* +'dy'+: How much to scroll in x-direction.
|
||||
|
||||
==== count
|
||||
multiplier
|
||||
|
||||
[[search-next]]
|
||||
=== search-next
|
||||
Continue the search to the ([count]th) next term.
|
||||
@@ -1108,20 +757,6 @@ Continue the search to the ([count]th) previous term.
|
||||
==== count
|
||||
How many elements to ignore.
|
||||
|
||||
[[toggle-selection]]
|
||||
=== toggle-selection
|
||||
Toggle caret selection mode.
|
||||
|
||||
[[yank-selected]]
|
||||
=== yank-selected
|
||||
Syntax: +:yank-selected [*--sel*] [*--keep*]+
|
||||
|
||||
Yank the selected text to the clipboard or primary selection.
|
||||
|
||||
==== optional arguments
|
||||
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
|
||||
* +*-k*+, +*--keep*+: If given, stay in visual mode after yanking.
|
||||
|
||||
|
||||
== Debugging commands
|
||||
These commands are mainly intended for debugging. They are hidden if qutebrowser was started without the `--debug`-flag.
|
||||
@@ -1134,9 +769,7 @@ These commands are mainly intended for debugging. They are hidden if qutebrowser
|
||||
|<<debug-cache-stats,debug-cache-stats>>|Print LRU cache stats.
|
||||
|<<debug-console,debug-console>>|Show the debugging console.
|
||||
|<<debug-crash,debug-crash>>|Crash for debugging purposes.
|
||||
|<<debug-pyeval,debug-pyeval>>|Evaluate a python string and display the results as a web page.
|
||||
|<<debug-trace,debug-trace>>|Trace executed code via hunter.
|
||||
|<<debug-webaction,debug-webaction>>|Execute a webaction.
|
||||
|<<debug-pyeval,debug-pyeval>>|Evaluate a python string and display the results as a webpage.
|
||||
|==============
|
||||
[[debug-all-objects]]
|
||||
=== debug-all-objects
|
||||
@@ -1163,39 +796,8 @@ Crash for debugging purposes.
|
||||
=== debug-pyeval
|
||||
Syntax: +:debug-pyeval 's'+
|
||||
|
||||
Evaluate a python string and display the results as a web page.
|
||||
Evaluate a python string and display the results as a webpage.
|
||||
|
||||
==== positional arguments
|
||||
* +'s'+: The string to evaluate.
|
||||
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
|
||||
|
||||
[[debug-trace]]
|
||||
=== debug-trace
|
||||
Syntax: +:debug-trace ['expr']+
|
||||
|
||||
Trace executed code via hunter.
|
||||
|
||||
==== positional arguments
|
||||
* +'expr'+: What to trace, passed to hunter.
|
||||
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
|
||||
|
||||
[[debug-webaction]]
|
||||
=== debug-webaction
|
||||
Syntax: +:debug-webaction 'action'+
|
||||
|
||||
Execute a webaction.
|
||||
|
||||
See http://doc.qt.io/qt-5/qwebpage.html#WebAction-enum for the available actions.
|
||||
|
||||
==== positional arguments
|
||||
* +'action'+: The action to execute, e.g. MoveToNextChar.
|
||||
|
||||
==== count
|
||||
How many times to repeat the action.
|
||||
|
||||
|
||||
@@ -8,10 +8,8 @@ The following help pages are currently available:
|
||||
|
||||
* link:quickstart.html[Quick start guide]
|
||||
* link:FAQ.html[Frequently asked questions]
|
||||
* link:CHANGELOG.html[Change Log]
|
||||
* link:commands.html[Documentation of commands]
|
||||
* link:settings.html[Documentation of settings]
|
||||
* link:userscripts.html[How to write userscripts]
|
||||
|
||||
Getting help
|
||||
------------
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
114
doc/notes
114
doc/notes
@@ -80,117 +80,3 @@ some cases passing the url to some cli program).
|
||||
I've also noticed the lack of completion. For example, on "o" pentadactyl will
|
||||
show sites (e.g. from history) that can be completed. I think I've been spoiled
|
||||
by pentadactyl having completion for just about everything.
|
||||
|
||||
|
||||
suckless surf ML post
|
||||
=====================
|
||||
|
||||
From: Ben Woolley <tautolog_AT_gmail.com>
|
||||
Date: Wed, 7 Jan 2015 18:29:25 -0800
|
||||
|
||||
Hi all,
|
||||
|
||||
This patch is a bit of a beast for surf. It is intended to be applied after
|
||||
the disk cache patch. It breaks some internal interfaces, so it could
|
||||
conflict with other patches.
|
||||
|
||||
I have been wanting a browser to implement a complete same-origin policy,
|
||||
and have been investigating how to do this in various browsers for many
|
||||
months. When I saw how surf opened new windows in a separate process, and
|
||||
was so simple, I knew I could do it quickly. Over the last two weeks, I
|
||||
have been developing this implementation on surf.
|
||||
|
||||
The basic idea is to prevent browser-based tracking as you browse from site
|
||||
to site, or origin to origin. By "origin" domain, I mean the "first-party"
|
||||
domain, the domain normally in the location bar (of the typical browser
|
||||
interface). Each origin domain effectively gets its own browser profile,
|
||||
and a browser process only ever deals with one origin domain at a time.
|
||||
This isolates origins vertically, preventing cookies, disk cache, memory
|
||||
cache, and window.name vulnerabilities. Basically, all known
|
||||
vulnerabilities that google and Mozilla cite as counter-examples when they
|
||||
explain why they haven't disabled third-party cookies yet.
|
||||
|
||||
When you are on msnbc.com, the tracking pixels will be stored in a cookie
|
||||
file for msnbc.com. When you go to cnn.com, the tracking pixels will be
|
||||
stored in a cookie file for cnn.com. You will not be tracked between them.
|
||||
However, third-party cookies, and the caching of third party resources will
|
||||
still work, but they will be isolated between origin domains. Instead of
|
||||
blocking cookies and cache entries, they are "double-keyed", or *also*
|
||||
keyed by origin.
|
||||
|
||||
There is a unidirectional communication channel, however, from one origin
|
||||
to the next, through navigation from one origin to the next. That is, the
|
||||
query string is passed from one origin to the next, and may embed
|
||||
identifiers. One example is an affiliate link that identifies where the
|
||||
lead came from. I have implemented what I call "horizontal isolation", in
|
||||
the form of an "Origin Crossing Gate".
|
||||
|
||||
Whenever you follow a link to a new domain, or even are just redirected to
|
||||
a new domain, a new window/tab is opened, and passed the referring origin
|
||||
via -R. The page passed to -O, for example -O originprompt.html, is an HTML
|
||||
page that is loaded in the new origin's context. That page tells you the
|
||||
origin you were on, the new origin, and the full link, and you can decide
|
||||
to go just to the new origin, or go to the full URL, after reviewing it for
|
||||
tracking data.
|
||||
|
||||
Also, you may click links that store your trust of that relationship with
|
||||
various expiration times, the same way you would trust geolocation requests
|
||||
for a particular origin for a period of time. The database used is actually
|
||||
the new origin's cookie file. Since the origin prompt is loaded in the new
|
||||
origin's context, I can set a cookie on behalf of the new origin. The
|
||||
expiration time of the trust is the expiration time of the cookie. The
|
||||
cookie implementation in webkit automatically expires the trust as part of
|
||||
how cookies work. Each time you cross an origin, the origin crossing page
|
||||
checks the cookie to see if trust is still established. If so, it will use
|
||||
window.location.replace() to continue on automatically. The initial page
|
||||
renders blank until the trust is invalidated, in which case the content of
|
||||
the gate is made visible.
|
||||
|
||||
However, the new origin is technically able to mess with those cookies, so
|
||||
a website could set trust for an origin crossing. I have addressed that by
|
||||
hashing the key with a salt, and setting the real expiration time as the
|
||||
value, along with an HMAC to verify the contents of the value. If the
|
||||
cookie is messed with in any way, the trust will be disabled, and the
|
||||
prompt will appear again. So it has a fail-safe function.
|
||||
|
||||
I know it seems a bit convoluted, but it just started out as a nice little
|
||||
rabbit hole, and I just wanted to get something workable. At first I
|
||||
thought using the cookie expiration time was convenient, but then when I
|
||||
realized that I needed to protect the cookie, things got a bit hairy. But
|
||||
it works.
|
||||
|
||||
Each profile is, by default, stored in ~/.surf/origins/$origin/
|
||||
The interesting side effect is that if there is a problem where a website
|
||||
relies on the cross-site cookie vulnerability to make a connection, you can
|
||||
simply make a symbolic link from one origin folder to another, and they
|
||||
will share the same profile. And if you want to delete cookies and/or cache
|
||||
for a particular origin, you just rm -rf the origin's profile folder, and
|
||||
don't have to interfere with your other sites that are working just fine.
|
||||
|
||||
One thing I don't handle are cross-origins POSTs. They just end up as GET
|
||||
requests right now. I intend to do something about that, but I haven't
|
||||
figured that out yet.
|
||||
|
||||
I have only been using this functionality for a few days myself, so I have
|
||||
absolutely no feedback yet. I wanted to provide the first implementation of
|
||||
the management of identity as a system resource the same way that things
|
||||
like geolocation, camera, and microphone resources are managed in browsers
|
||||
and mobile apps.
|
||||
|
||||
Currently, Mozilla and Tor have are working on third-party tracking issues
|
||||
in Firefox.
|
||||
https://blog.mozilla.org/privacy/2014/11/10/introducing-polaris-privacy-initiative-to-accelerate-user-focused-privacy-online/
|
||||
|
||||
Up to this point, Tor has provided a patch that double-keys cookies with
|
||||
the origin domain, but no other progress is visible. I have seen no
|
||||
discussion of how horizontal isolation is supposed to happen, and I wanted
|
||||
to show people that it can be done, and this is one way it can be done, and
|
||||
to compel the other browser makers to catch up, and hopefully the community
|
||||
can work toward a standard *without* the tracking loopholes, by showing
|
||||
people what a *complete* solution looks like.
|
||||
|
||||
Thank you,
|
||||
|
||||
Ben Woolley
|
||||
|
||||
Patch: http://lists.suckless.org/dev/att-25070/0005-same-origin-policy.patch
|
||||
|
||||
@@ -8,10 +8,9 @@ time, use the `:help` command.
|
||||
What to do now
|
||||
--------------
|
||||
|
||||
* View the http://qutebrowser.org/img/cheatsheet-big.png[key binding cheatsheet]
|
||||
to make yourself familiar with the key bindings: +
|
||||
image:http://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding cheatsheet",link="http://qutebrowser.org/img/cheatsheet-big.png"]
|
||||
* Run `:adblock-update` to download adblock lists and activate adblocking.
|
||||
* View the http://qutebrowser.org/img/cheatsheet-big.png[keybinding cheatsheet]
|
||||
to make yourself familiar with the keybindings: +
|
||||
image:http://qutebrowser.org/img/cheatsheet-small.png["qutebrowser keybinding cheatsheet",link="http://qutebrowser.org/img/cheatsheet-big.png"]
|
||||
* If you just cloned the repository, you'll need to run
|
||||
`scripts/asciidoc2html.py` to generate the documentation.
|
||||
* Go to the link:qute://settings[settings page] to set up qutebrowser the way you want it.
|
||||
|
||||
@@ -21,10 +21,6 @@ on Python, PyQt5 and QtWebKit and free software, licensed under the GPL.
|
||||
|
||||
It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl.
|
||||
|
||||
Note the commands and settings of qutebrowser are not described in this
|
||||
manpage, but in the help integrated in qutebrowser - use the ":help" command to
|
||||
show it.
|
||||
|
||||
== OPTIONS
|
||||
// QUTE_OPTIONS_START
|
||||
=== positional arguments
|
||||
@@ -39,29 +35,11 @@ show it.
|
||||
show this help message and exit
|
||||
|
||||
*-c* 'CONFDIR', *--confdir* 'CONFDIR'::
|
||||
Set config directory (empty for no config storage).
|
||||
|
||||
*--datadir* 'DATADIR'::
|
||||
Set data directory (empty for no data storage).
|
||||
|
||||
*--cachedir* 'CACHEDIR'::
|
||||
Set cache directory (empty for no cache storage).
|
||||
|
||||
*--basedir* 'BASEDIR'::
|
||||
Base directory for all storage. Other --*dir arguments are ignored if this is given.
|
||||
Set config directory (empty for no config storage)
|
||||
|
||||
*-V*, *--version*::
|
||||
Show version and quit.
|
||||
|
||||
*-s* 'SECTION' 'OPTION' 'VALUE', *--set* 'SECTION' 'OPTION' 'VALUE'::
|
||||
Set a temporary setting for this session.
|
||||
|
||||
*-r* 'SESSION', *--restore* 'SESSION'::
|
||||
Restore a named session.
|
||||
|
||||
*-R*, *--override-restore*::
|
||||
Don't restore a session even if one would be restored.
|
||||
|
||||
=== debug arguments
|
||||
*-l* 'LOGLEVEL', *--loglevel* 'LOGLEVEL'::
|
||||
Set loglevel
|
||||
@@ -81,26 +59,14 @@ show it.
|
||||
*--harfbuzz* '{old,new,system,auto}'::
|
||||
HarfBuzz engine version to use. Default: auto.
|
||||
|
||||
*--relaxed-config*::
|
||||
Silently remove unknown config options.
|
||||
|
||||
*--nowindow*::
|
||||
Don't show the main window.
|
||||
|
||||
*--debug-exit*::
|
||||
Turn on debugging of late exit.
|
||||
|
||||
*--pdb-postmortem*::
|
||||
Drop into pdb on exceptions.
|
||||
|
||||
*--temp-basedir*::
|
||||
Use a temporary basedir.
|
||||
|
||||
*--no-err-windows*::
|
||||
Don't show any error windows (used for tests/smoke.py).
|
||||
|
||||
*--qt-name* 'NAME'::
|
||||
Set the window name.
|
||||
*--no-crash-dialog*::
|
||||
Don't show a crash dialog.
|
||||
|
||||
*--qt-style* 'STYLE'::
|
||||
Set the Qt GUI style to use.
|
||||
@@ -122,14 +88,8 @@ show it.
|
||||
|
||||
- '~/.config/qutebrowser/qutebrowser.conf': Main config file.
|
||||
- '~/.config/qutebrowser/quickmarks': Saved quickmarks.
|
||||
- '~/.config/qutebrowser/keys.conf': Defined key bindings.
|
||||
- '~/.local/share/qutebrowser/': Various state information.
|
||||
- '~/.cache/qutebrowser/': Temporary data.
|
||||
|
||||
Note qutebrowser conforms to the XDG basedir specification - if
|
||||
'XDG_CONFIG_HOME', 'XDG_DATA_HOME' or 'XDG_CACHE_HOME' are set in the
|
||||
environment, the directories configured there are used instead of the above
|
||||
defaults.
|
||||
- '~/.config/qutebrowser/keys.conf': Defined keybindings.
|
||||
- '~/.local/share/qutebrowser/': Various state information
|
||||
|
||||
== BUGS
|
||||
Bugs are tracked in the Github issue tracker at
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
Getting stacktraces on crashes
|
||||
==============================
|
||||
:toc:
|
||||
The Compiler <mail@qutebrowser.org>
|
||||
|
||||
When there is a fatal crash in qutebrowser - most of the times a
|
||||
@@ -15,17 +14,10 @@ https://en.wikipedia.org/wiki/Debug_symbol[debugging symbols] is required.
|
||||
The rest of this guide is quite Linux specific, though there is a
|
||||
<<windows,section for Windows>> at the end.
|
||||
|
||||
Crashes which can be reproduced
|
||||
-------------------------------
|
||||
|
||||
If a crash can be reproduced, packages with debugging symbols should be
|
||||
installed, and the crash should be reproduced under gdb.
|
||||
|
||||
Getting debugging symbols
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
-------------------------
|
||||
|
||||
Debian/Ubuntu/...
|
||||
^^^^^^^^^^^^^^^^^
|
||||
.Debian/Ubuntu/...
|
||||
|
||||
For Debian based systems (Debian, Ubuntu, Linux Mint, ...), debug information
|
||||
is available in the repositories:
|
||||
@@ -34,66 +26,76 @@ is available in the repositories:
|
||||
# apt-get install python3-pyqt5-dbg python3-pyqt5.qtwebkit-dbg python3-dbg libqt5webkit5-dbg
|
||||
----
|
||||
|
||||
Archlinux
|
||||
^^^^^^^^^
|
||||
.Archlinux
|
||||
|
||||
For Archlinux, no debug informations are 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).
|
||||
debugging symbols compiled by me (x86_64 only).
|
||||
|
||||
.To compile by yourself
|
||||
To compile by yourself:
|
||||
|
||||
----
|
||||
$ git clone https://github.com/The-Compiler/qt-debug-pkgbuild.git
|
||||
$ cd qt-debug-pkgbuild
|
||||
$ git checkout symbols
|
||||
$ export DEBUG_CFLAGS='-ggdb3 -fvar-tracking-assignments -Og'
|
||||
$ export DEBUG_CXXFLAGS='-ggdb3 -fvar-tracking-assignments -Og'
|
||||
$ cd qt5
|
||||
$ makepkg -si --pkg qt5-base-debug,qt5-webkit-debug
|
||||
$ makepkg -si
|
||||
$ cd ../pyqt5
|
||||
$ makepkg -si --pkg pyqt5-common-debug,python-pyqt5-debug
|
||||
$ makepkg -si
|
||||
----
|
||||
|
||||
.To install my pre-built packages
|
||||
|
||||
First download and sign the key:
|
||||
To install my pre-built packages:
|
||||
|
||||
----
|
||||
# pacman-key -r 0xD6A1C70FE80A0C82
|
||||
$ pacman-key -f 0xD6A1C70FE80A0C82
|
||||
Key fingerprint = 14AF EC28 70C6 4863 C5C7 ACCB D6A1 C70F E80A 0C82
|
||||
# pacman-key --lsign-key 0xD6A1C70FE80A0C82
|
||||
$ mkdir qt-debug
|
||||
$ cd qt-debug
|
||||
$ wget -r -l1 -A '*.tar.xz' -L -np -nd http://www.qutebrowser.org/qt-symbols-pkg/
|
||||
# pacman -U *.pkg.tar.xz
|
||||
----
|
||||
|
||||
Then edit your `/etc/pacman.conf` to add the repository to the bottom:
|
||||
After you are done debugging, make sure to install the system packages again so
|
||||
you get updates. This can be done with this command:
|
||||
|
||||
----
|
||||
[qt-debug]
|
||||
Server = http://qutebrowser.org/qt-debug/$arch
|
||||
# pacman -S qt5
|
||||
----
|
||||
|
||||
Then install the packages:
|
||||
Getting a core dump
|
||||
-------------------
|
||||
|
||||
The next step is finding the core dump so we can get a stacktrace from it.
|
||||
|
||||
First of all, try to reproduce your problem. If you can, run qutebrowser
|
||||
directly inside gdb like this:
|
||||
|
||||
----
|
||||
# pacman -Sy pyqt5-common-debug python-pyqt5-debug qt5-base-debug qt5-webkit-debug
|
||||
$ gdb $(which python3) -ex 'run -m qutebrowser --debug'
|
||||
----
|
||||
|
||||
The `-debug` packages conflict with the non-debug variants - it's safe to
|
||||
remove them.
|
||||
If you cannot reproduce the problem, you need to check if a coredump got
|
||||
written somewhere.
|
||||
|
||||
Getting the stack trace
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Check the file `/proc/sys/kernel/core_pattern` on your system. If it does not
|
||||
start with a `|` character (pipe), check if there is a file named `core` or
|
||||
`core.NNNN` in the directory from that file, or in the current directory.
|
||||
|
||||
First install `gdb` on your system if it's not installed already.
|
||||
|
||||
Then run qutebrowser directly inside gdb like this:
|
||||
If so, execute gdb like this:
|
||||
|
||||
----
|
||||
$ gdb $(readlink -f $(which python3)) -ex 'run -m qutebrowser --debug'
|
||||
$ gdb $(which python3) /path/to/core
|
||||
----
|
||||
|
||||
After you reproduce the crash, you should now see something like:
|
||||
If your `/proc/sys/kernel/core_pattern` contains something like
|
||||
`|/usr/lib/systemd/systemd-coredump`, use `coredumpctl` as root to run gdb:
|
||||
|
||||
----
|
||||
# coredumpctl gdb $(which python3)
|
||||
----
|
||||
|
||||
Getting a stack trace
|
||||
---------------------
|
||||
|
||||
Regardless of the way you used to open gdb, you should now see something like:
|
||||
|
||||
----
|
||||
Program received signal SIGSEGV, Segmentation fault.
|
||||
@@ -105,58 +107,16 @@ Now enter these commands at the gdb prompt:
|
||||
|
||||
----
|
||||
(gdb) set logging on
|
||||
(gdb) bt full
|
||||
# you might have to press enter a few times until you get the prompt back
|
||||
(gdb) quit
|
||||
----
|
||||
|
||||
This will create a `gdb.txt` in your current directory.
|
||||
|
||||
Copy the last few lines of the debug log (before you got the gdb prompt) and
|
||||
the full content of `gdb.txt` into the bug report. Please also add some words
|
||||
about what you were doing (or what pages you visited) before the crash
|
||||
happened.
|
||||
|
||||
Crashes which can NOT be reproduced
|
||||
-----------------------------------
|
||||
|
||||
If you cannot reproduce the problem, you need to check if a coredump got
|
||||
written somewhere. You should not install debug symbols as they won't match the
|
||||
generated coredump.
|
||||
|
||||
First install `gdb` on your system if it's not installed already.
|
||||
|
||||
Then check the file `/proc/sys/kernel/core_pattern` on your system. If it does
|
||||
not start with a `|` character (pipe), check if there is a file named `core` or
|
||||
`core.NNNN` in the directory from that file, or in the current directory.
|
||||
|
||||
If so, execute gdb like this:
|
||||
|
||||
----
|
||||
$ gdb $(readlink -f $(which python3)) /path/to/core
|
||||
----
|
||||
|
||||
If your `/proc/sys/kernel/core_pattern` contains something like
|
||||
`|/usr/lib/systemd/systemd-coredump`, use `coredumpctl` to run gdb:
|
||||
|
||||
----
|
||||
$ coredumpctl gdb $(readlink -f $(which python3))
|
||||
----
|
||||
|
||||
Getting the stack trace
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Now enter these commands at the gdb prompt:
|
||||
|
||||
----
|
||||
(gdb) set logging on
|
||||
(gdb) set logging redirect on
|
||||
(gdb) bt
|
||||
# you might have to press enter a few times until you get the prompt back
|
||||
(gdb) set logging redirect off
|
||||
(gdb) quit
|
||||
----
|
||||
|
||||
Copy the content of `gdb.txt` into the bug report. Please also add some words
|
||||
about what you were doing (or what pages you visited) before the crash
|
||||
Now copy the last few lines of the debug log (before you got the gdb prompt)
|
||||
and the full content of `gdb.txt` into the bug report. Please also add some
|
||||
words about what you were doing (or what pages you visited) before the crash
|
||||
happened.
|
||||
|
||||
[[windows]]
|
||||
@@ -170,9 +130,9 @@ file displayed there.
|
||||
|
||||
Now install
|
||||
http://www.microsoft.com/en-us/download/details.aspx?id=42933[DebugDiag] from
|
||||
Microsoft, then run the *DebugDiag 2 Analysis* tool. There, check
|
||||
*CrashHangAnalysis* and add your crash dump via *Add Data files*. Then click
|
||||
*Start analysis*.
|
||||
Microsoft, then run the "DebugDiag 2 Analysis" tool. There, check
|
||||
"CrashHangAnalysis" and add your crash dump via "Add Data files". Then click
|
||||
"Start analysis".
|
||||
|
||||
Close the Internet Explorer which opens when it's done and use the
|
||||
folder-button at the top left to get to the reports. There find the report file
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
Writing qutebrowser userscripts
|
||||
===============================
|
||||
The Compiler <mail@qutebrowser.org>
|
||||
|
||||
qutebrowser is extensible by writing userscripts which can be called via the
|
||||
`:spawn --userscript` command, or via a key binding.
|
||||
|
||||
These userscripts are similiar to the (non-javascript) dwb userscripts. They
|
||||
can be written in any language which can read environment variables and write
|
||||
to a FIFO. Note they are *not* related to Greasemonkey userscripts.
|
||||
|
||||
Note for simple things such as opening the current page with another browser or
|
||||
mpv, a simple key binding to something like `:spawn mpv {url}` should suffice.
|
||||
|
||||
Also note userscripts need to have the executable bit set (`chmod +x`) for
|
||||
qutebrowser to run them.
|
||||
|
||||
Getting information
|
||||
-------------------
|
||||
|
||||
The following environment variables will be set when an userscript is launched:
|
||||
|
||||
- `QUTE_MODE`: Either `hints` (started via hints) or `command` (started via
|
||||
command or key binding).
|
||||
- `QUTE_USER_AGENT`: The currently set user agent.
|
||||
- `QUTE_FIFO`: The FIFO or file to write commands to.
|
||||
- `QUTE_HTML`: Path of a file containing the HTML source of the current page.
|
||||
- `QUTE_TEXT`: Path of a file containing the plaintext of the current page.
|
||||
|
||||
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.
|
||||
|
||||
In `hints` mode:
|
||||
|
||||
- `QUTE_URL`: The URL selected via hints.
|
||||
- `QUTE_SELECTED_TEXT`: The plain text of the element selected via hints.
|
||||
- `QUTE_SELECTED_HTML` The HTML of the element selected via hints.
|
||||
|
||||
Sending commands
|
||||
----------------
|
||||
|
||||
Normal qutebrowser commands can be written to `$QUTE_FIFO` and will be
|
||||
executed.
|
||||
|
||||
On Unix/OS X, this is a named pipe and commands written to it will get executed
|
||||
immediately.
|
||||
|
||||
On Windows, this is a regular file, and the commands in it will be executed as
|
||||
soon as your userscript terminates. This means when writing multiple commands,
|
||||
you should append to the file (`>>` in bash) rather than overwrite it (`>`).
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
Opening the currently selected word on http://www.dict.cc/[dict.cc]:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
#!/bin/bash
|
||||
|
||||
echo "open -t http://www.dict.cc/?s=$QUTE_SELECTED_TEXT" >> "$QUTE_FIFO"
|
||||
----
|
||||
Binary file not shown.
@@ -32,22 +32,21 @@
|
||||
objecttolerance="10"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.8791156"
|
||||
inkscape:cx="768.67127"
|
||||
inkscape:cy="133.80749"
|
||||
inkscape:zoom="1.2432572"
|
||||
inkscape:cx="510.06077"
|
||||
inkscape:cy="315.85317"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
width="1024px"
|
||||
height="640px"
|
||||
showgrid="false"
|
||||
inkscape:window-width="636"
|
||||
inkscape:window-height="536"
|
||||
inkscape:window-x="2560"
|
||||
inkscape:window-width="1024"
|
||||
inkscape:window-height="723"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:snap-text-baseline="true">
|
||||
inkscape:window-maximized="1">
|
||||
<inkscape:grid
|
||||
id="GridFromPre046Settings"
|
||||
type="xygrid"
|
||||
@@ -1455,27 +1454,23 @@
|
||||
x="714.29938"
|
||||
y="108.87096">)</tspan></text>
|
||||
<rect
|
||||
ry="3.3457608"
|
||||
y="363.19348"
|
||||
ry="4.3646927"
|
||||
y="363.55695"
|
||||
x="238.30771"
|
||||
height="44.799603"
|
||||
height="58.443066"
|
||||
width="361.69229"
|
||||
id="rect5017"
|
||||
style="font-size:18px;fill:#babdb6;fill-opacity:1;stroke:none" />
|
||||
<g
|
||||
id="g4061"
|
||||
transform="translate(0,-6.7232151)">
|
||||
<text
|
||||
id="text5021"
|
||||
y="395.78867"
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:13px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:DejaVu Sans Mono"
|
||||
x="245.32532"
|
||||
y="395.78867"
|
||||
id="text5021"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan5023"
|
||||
x="245.32532"
|
||||
style="font-style:normal;font-weight:normal;font-size:13px;font-family:'DejaVu Sans Mono';fill:#000000;fill-opacity:1;stroke:none"
|
||||
xml:space="preserve"><tspan
|
||||
y="395.78867"
|
||||
x="245.32532"
|
||||
id="tspan5023"
|
||||
sodipodi:role="line">Space</tspan></text>
|
||||
</g>
|
||||
y="395.78867">Space</tspan></text>
|
||||
<text
|
||||
id="text6971"
|
||||
y="317.98907"
|
||||
@@ -1870,16 +1865,16 @@
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:9px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:89.99999762%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:TlwgTypewriter"
|
||||
x="317.63174"
|
||||
x="320.22501"
|
||||
y="195.40761"
|
||||
id="text7245"
|
||||
sodipodi:linespacing="89.999998%"><tspan
|
||||
sodipodi:role="line"
|
||||
x="317.63174"
|
||||
x="320.22501"
|
||||
y="195.40761"
|
||||
id="tspan7366" /><tspan
|
||||
sodipodi:role="line"
|
||||
x="317.63174"
|
||||
x="320.22501"
|
||||
y="202.78995"
|
||||
id="tspan7249"
|
||||
style="font-size:8px">reload</tspan></text>
|
||||
@@ -1939,7 +1934,7 @@
|
||||
x="542.06946"
|
||||
sodipodi:role="line"
|
||||
id="tspan4938"
|
||||
style="font-size:8px">scroll</tspan><tspan
|
||||
style="font-size:8px">scoll</tspan><tspan
|
||||
y="276.1955"
|
||||
x="542.06946"
|
||||
sodipodi:role="line"
|
||||
@@ -2094,7 +2089,7 @@
|
||||
id="tspan4998"
|
||||
style="font-size:8px">new tab<tspan
|
||||
style="fill:#ff0000"
|
||||
id="tspan3699" /></tspan><tspan
|
||||
id="tspan3699"></tspan></tspan><tspan
|
||||
y="177.83009"
|
||||
x="670.26074"
|
||||
sodipodi:role="line"
|
||||
@@ -2629,8 +2624,8 @@
|
||||
<flowRoot
|
||||
xml:space="preserve"
|
||||
id="flowRoot5691"
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
|
||||
transform="translate(0,-38.539167)"><flowRegion
|
||||
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
|
||||
transform="translate(0,-14.539167)"><flowRegion
|
||||
id="flowRegion5693"><rect
|
||||
id="rect5695"
|
||||
width="322.5"
|
||||
@@ -2639,8 +2634,8 @@
|
||||
y="448.75"
|
||||
style="fill:#000000" /></flowRegion><flowPara
|
||||
id="flowPara5697"
|
||||
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"><flowSpan
|
||||
style="font-weight:bold;-inkscape-font-specification:'Sans Bold';fill:#ff0000"
|
||||
style="font-size:10px;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"><flowSpan
|
||||
style="font-weight:bold;fill:#ff0000;-inkscape-font-specification:Sans Bold"
|
||||
id="flowSpan5705">(1)</flowSpan> copying/yanking:</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara5701">yy - copy/yank URL</flowPara><flowPara
|
||||
@@ -2652,10 +2647,10 @@
|
||||
id="flowPara5709">yT - copy title to selection</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara5711" /></flowRoot> <flowRoot
|
||||
transform="translate(0.713591,38.823906)"
|
||||
transform="translate(0.713591,62.823906)"
|
||||
xml:space="preserve"
|
||||
id="flowRoot5691-0"
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
|
||||
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><flowRegion
|
||||
id="flowRegion5693-7"><rect
|
||||
id="rect5695-0"
|
||||
width="322.5"
|
||||
@@ -2664,8 +2659,8 @@
|
||||
y="448.75"
|
||||
style="fill:#000000" /></flowRegion><flowPara
|
||||
id="flowPara5697-9"
|
||||
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"><flowSpan
|
||||
style="font-weight:bold;-inkscape-font-specification:'Sans Bold';fill:#ff0000"
|
||||
style="font-size:10px;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"><flowSpan
|
||||
style="font-weight:bold;fill:#ff0000;-inkscape-font-specification:Sans Bold"
|
||||
id="flowSpan5705-5">(2)</flowSpan> pasting:</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara5701-9">pp - open URL from clipboard</flowPara><flowPara
|
||||
@@ -2673,26 +2668,26 @@
|
||||
id="flowPara5703-8">pP - open URL from selection</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara5707-0">Pp - like <flowSpan
|
||||
style="font-style:italic;-inkscape-font-specification:'Sans Italic'"
|
||||
style="font-style:italic;-inkscape-font-specification:Sans Italic"
|
||||
id="flowSpan6101">pp</flowSpan>, in new tab</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara5709-3">PP - like <flowSpan
|
||||
style="font-style:italic;-inkscape-font-specification:'Sans Italic'"
|
||||
style="font-style:italic;-inkscape-font-specification:Sans Italic"
|
||||
id="flowSpan6103">pP</flowSpan>, in new tab</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara5763">wp - like <flowSpan
|
||||
style="font-style:italic;-inkscape-font-specification:'Sans Italic'"
|
||||
style="font-style:italic;-inkscape-font-specification:Sans Italic"
|
||||
id="flowSpan6105">pp</flowSpan>, in new window</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara5765">wP - like <flowSpan
|
||||
style="font-style:italic;-inkscape-font-specification:'Sans Italic'"
|
||||
style="font-style:italic;-inkscape-font-specification:Sans Italic"
|
||||
id="flowSpan6107">pP</flowSpan>, in new window</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara5711-1" /></flowRoot> <flowRoot
|
||||
transform="translate(171.2479,-38.539167)"
|
||||
transform="translate(171.2479,-14.539167)"
|
||||
xml:space="preserve"
|
||||
id="flowRoot5691-0-9"
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
|
||||
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><flowRegion
|
||||
id="flowRegion5693-7-0"><rect
|
||||
id="rect5695-0-5"
|
||||
width="322.5"
|
||||
@@ -2700,9 +2695,9 @@
|
||||
x="17.5"
|
||||
y="448.75"
|
||||
style="fill:#000000" /></flowRegion><flowPara
|
||||
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"
|
||||
style="font-size:10px;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"
|
||||
id="flowPara5701-9-6"><flowSpan
|
||||
style="font-weight:bold;-inkscape-font-specification:'Sans Bold';fill:#ff0000"
|
||||
style="font-weight:bold;fill:#ff0000;-inkscape-font-specification:Sans Bold"
|
||||
id="flowSpan5705-5-8">(3)</flowSpan> navigation:</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara5829">[[ - click "previous"-link on page</flowPara><flowPara
|
||||
@@ -2710,11 +2705,11 @@
|
||||
id="flowPara5703-8-2">]] - click "next"-link on page</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara5707-0-7">{{ - like <flowSpan
|
||||
style="font-style:italic;-inkscape-font-specification:'Sans Italic'"
|
||||
style="font-style:italic;-inkscape-font-specification:Sans Italic"
|
||||
id="flowSpan6111">[[</flowSpan>, in new tab</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara5709-3-1">}} - like <flowSpan
|
||||
style="font-style:italic;-inkscape-font-specification:'Sans Italic'"
|
||||
style="font-style:italic;-inkscape-font-specification:Sans Italic"
|
||||
id="flowSpan6109">]]</flowSpan>, in new tab</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara5835"><Ctrl-A> - increment no. in URL</flowPara><flowPara
|
||||
@@ -2774,10 +2769,10 @@
|
||||
id="tspan4936-1-1-9-2"
|
||||
style="font-size:8px;fill:#ff0000">(3)</tspan></text>
|
||||
<flowRoot
|
||||
transform="translate(169.83695,63.823906)"
|
||||
transform="translate(169.83695,87.823906)"
|
||||
xml:space="preserve"
|
||||
id="flowRoot5691-4"
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
|
||||
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><flowRegion
|
||||
id="flowRegion5693-9"><rect
|
||||
id="rect5695-9"
|
||||
width="322.5"
|
||||
@@ -2786,8 +2781,8 @@
|
||||
y="448.75"
|
||||
style="fill:#000000" /></flowRegion><flowPara
|
||||
id="flowPara5697-3"
|
||||
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"><flowSpan
|
||||
style="font-weight:bold;-inkscape-font-specification:'Sans Bold';fill:#ff0000"
|
||||
style="font-size:10px;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"><flowSpan
|
||||
style="font-weight:bold;fill:#ff0000;-inkscape-font-specification:Sans Bold"
|
||||
id="flowSpan5705-0">(4)</flowSpan> scrolling:</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara5701-8"><Ctrl-F> - page down</flowPara><flowPara
|
||||
@@ -2797,59 +2792,59 @@
|
||||
id="flowPara5962"><Ctrl-D> - half page down</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara5711-7"><Ctrl-U> - half page up</flowPara></flowRoot> <flowRoot
|
||||
transform="translate(360.81663,-38.539167)"
|
||||
transform="translate(360.81663,-14.539167)"
|
||||
xml:space="preserve"
|
||||
id="flowRoot5691-4-9"
|
||||
style="font-style:normal;font-weight:bold;font-size:40px;line-height:125%;font-family:Sans;-inkscape-font-specification:'Sans Bold';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
|
||||
style="font-size:40px;font-style:normal;font-weight:bold;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans Bold"><flowRegion
|
||||
id="flowRegion5693-9-1"><rect
|
||||
id="rect5695-9-8"
|
||||
width="322.5"
|
||||
height="162.5"
|
||||
x="17.5"
|
||||
y="448.75"
|
||||
style="font-weight:bold;-inkscape-font-specification:'Sans Bold';fill:#000000" /></flowRegion><flowPara
|
||||
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"
|
||||
style="font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold" /></flowRegion><flowPara
|
||||
style="font-size:10px;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"
|
||||
id="flowPara4171">in prompt mode:</flowPara><flowPara
|
||||
style="font-weight:normal;font-size:10px;-inkscape-font-specification:Sans;fill:#000000"
|
||||
style="font-size:10px;font-weight:normal;fill:#000000;-inkscape-font-specification:Sans"
|
||||
id="flowPara4175">Enter - accept prompt</flowPara><flowPara
|
||||
style="font-weight:normal;font-size:10px;-inkscape-font-specification:Sans;fill:#000000"
|
||||
style="font-size:10px;font-weight:normal;fill:#000000;-inkscape-font-specification:Sans"
|
||||
id="flowPara4177">y - answer yes to prompt</flowPara><flowPara
|
||||
style="font-weight:normal;font-size:10px;-inkscape-font-specification:Sans;fill:#000000"
|
||||
style="font-size:10px;font-weight:normal;fill:#000000;-inkscape-font-specification:Sans"
|
||||
id="flowPara4179">n - answer no to prompt</flowPara><flowPara
|
||||
style="font-weight:normal;font-size:10px;-inkscape-font-specification:Sans;fill:#000000"
|
||||
style="font-size:10px;font-weight:normal;fill:#000000;-inkscape-font-specification:Sans"
|
||||
id="flowPara6016" /></flowRoot> <flowRoot
|
||||
transform="translate(360.8264,16.645949)"
|
||||
transform="translate(360.8264,40.645949)"
|
||||
xml:space="preserve"
|
||||
id="flowRoot5691-0-9-9"
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;-inkscape-font-specification:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
|
||||
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans"><flowRegion
|
||||
id="flowRegion5693-7-0-2"><rect
|
||||
id="rect5695-0-5-6"
|
||||
width="322.5"
|
||||
height="162.5"
|
||||
x="17.5"
|
||||
y="448.75"
|
||||
style="font-style:normal;-inkscape-font-specification:Sans;fill:#000000" /></flowRegion><flowPara
|
||||
style="font-style:normal;font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"
|
||||
style="font-style:normal;fill:#000000;-inkscape-font-specification:Sans" /></flowRegion><flowPara
|
||||
style="font-size:10px;font-style:normal;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"
|
||||
id="flowPara5701-9-6-8"><flowSpan
|
||||
style="font-style:normal;font-weight:bold;-inkscape-font-specification:'Sans Bold';fill:#ff0000"
|
||||
style="font-style:normal;font-weight:bold;fill:#ff0000;-inkscape-font-specification:Sans Bold"
|
||||
id="flowSpan5705-5-8-3">(6)</flowSpan> opening:</flowPara><flowPara
|
||||
style="font-style:normal;font-size:10px;-inkscape-font-specification:Sans;fill:#000000"
|
||||
style="font-size:10px;font-style:normal;fill:#000000;-inkscape-font-specification:Sans"
|
||||
id="flowPara5829-1">go - open based on cur. URL</flowPara><flowPara
|
||||
style="font-style:normal;font-size:10px;-inkscape-font-specification:Sans;fill:#000000"
|
||||
style="font-size:10px;font-style:normal;fill:#000000;-inkscape-font-specification:Sans"
|
||||
id="flowPara5703-8-2-8">gO - like <flowSpan
|
||||
style="font-style:italic;-inkscape-font-specification:'Sans Italic'"
|
||||
style="font-style:italic;-inkscape-font-specification:Sans Italic"
|
||||
id="flowSpan6132">go</flowSpan>, in new tab</flowPara><flowPara
|
||||
style="font-style:normal;font-size:10px;-inkscape-font-specification:Sans;fill:#000000"
|
||||
style="font-size:10px;font-style:normal;fill:#000000;-inkscape-font-specification:Sans"
|
||||
id="flowPara3581">xO - like <flowSpan
|
||||
style="font-style:italic;-inkscape-font-specification:'Sans Italic'"
|
||||
style="font-style:italic;-inkscape-font-specification:Sans Italic"
|
||||
id="flowSpan6134">go</flowSpan>, in bg. tab</flowPara><flowPara
|
||||
style="font-style:normal;font-size:10px;-inkscape-font-specification:Sans;fill:#000000"
|
||||
style="font-size:10px;font-style:normal;fill:#000000;-inkscape-font-specification:Sans"
|
||||
id="flowPara5709-3-1-6">xo - open in background tab</flowPara><flowPara
|
||||
style="font-style:normal;font-size:10px;-inkscape-font-specification:Sans;fill:#000000"
|
||||
style="font-size:10px;font-style:normal;fill:#000000;-inkscape-font-specification:Sans"
|
||||
id="flowPara5841-1">wo - open in new window</flowPara><flowPara
|
||||
style="font-style:normal;font-size:10px;-inkscape-font-specification:Sans;fill:#000000"
|
||||
style="font-size:10px;font-style:normal;fill:#000000;-inkscape-font-specification:Sans"
|
||||
id="flowPara5839-8" /><flowPara
|
||||
style="font-style:normal;font-size:10px;-inkscape-font-specification:Sans;fill:#000000"
|
||||
style="font-size:10px;font-style:normal;fill:#000000;-inkscape-font-specification:Sans"
|
||||
id="flowPara5711-1-8-7" /></flowRoot> <text
|
||||
xml:space="preserve"
|
||||
style="font-size:9px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:89.99999762%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:TlwgTypewriter"
|
||||
@@ -2904,10 +2899,10 @@
|
||||
id="tspan6219"
|
||||
style="font-size:8px">mode</tspan></text>
|
||||
<flowRoot
|
||||
transform="translate(361.29883,97.78408)"
|
||||
transform="translate(361.29883,121.78408)"
|
||||
xml:space="preserve"
|
||||
id="flowRoot5691-4-9-3"
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
|
||||
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><flowRegion
|
||||
id="flowRegion5693-9-1-7"><rect
|
||||
id="rect5695-9-8-7"
|
||||
width="322.5"
|
||||
@@ -2916,8 +2911,8 @@
|
||||
y="448.75"
|
||||
style="fill:#000000" /></flowRegion><flowPara
|
||||
id="flowPara5697-3-7-6"
|
||||
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"><flowSpan
|
||||
style="font-weight:bold;-inkscape-font-specification:'Sans Bold';fill:#ff0000"
|
||||
style="font-size:10px;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"><flowSpan
|
||||
style="font-weight:bold;fill:#ff0000;-inkscape-font-specification:Sans Bold"
|
||||
id="flowSpan5705-0-4-7">(7)</flowSpan> back/forward:</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara5701-8-5-8"><flowSpan
|
||||
@@ -2964,10 +2959,10 @@
|
||||
style="font-size:8px;fill:#ff0000"
|
||||
id="tspan3662">(9)</tspan></tspan></text>
|
||||
<flowRoot
|
||||
transform="translate(526.15723,-38.548933)"
|
||||
transform="translate(526.15723,-14.548933)"
|
||||
xml:space="preserve"
|
||||
id="flowRoot5691-4-9-3-6"
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
|
||||
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><flowRegion
|
||||
id="flowRegion5693-9-1-7-3"><rect
|
||||
id="rect5695-9-8-7-7"
|
||||
width="322.5"
|
||||
@@ -2976,15 +2971,15 @@
|
||||
y="448.75"
|
||||
style="fill:#000000" /></flowRegion><flowPara
|
||||
id="flowPara5697-3-7-6-8"
|
||||
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#ff0000">(8)</flowPara><flowPara
|
||||
style="font-size:10px;font-weight:bold;fill:#ff0000;-inkscape-font-specification:Sans Bold">(8)</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3626-7">prefix with w - in new window</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3725" /></flowRoot> <flowRoot
|
||||
transform="translate(525.65723,10.440325)"
|
||||
transform="translate(525.65723,34.440325)"
|
||||
xml:space="preserve"
|
||||
id="flowRoot5691-4-9-3-1"
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
|
||||
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><flowRegion
|
||||
id="flowRegion5693-9-1-7-1"><rect
|
||||
id="rect5695-9-8-7-5"
|
||||
width="322.5"
|
||||
@@ -2993,14 +2988,12 @@
|
||||
y="448.75"
|
||||
style="fill:#000000" /></flowRegion><flowPara
|
||||
id="flowPara5697-3-7-6-1"
|
||||
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"><flowSpan
|
||||
style="font-weight:bold;-inkscape-font-specification:'Sans Bold';fill:#ff0000"
|
||||
style="font-size:10px;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"><flowSpan
|
||||
style="font-weight:bold;fill:#ff0000;-inkscape-font-specification:Sans Bold"
|
||||
id="flowSpan5705-0-4-7-6">(9)</flowSpan> extended hint mode:</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3626-73">;b - open hint in background tab</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara4051">;f - open hint in foreground tab</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3788">;h - hover over hint (mouse-over)</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3790">;i - hint images</flowPara><flowPara
|
||||
@@ -3010,7 +3003,7 @@
|
||||
id="flowPara3794">;o - put hinted URL in cmd. line</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3796">;O - like <flowSpan
|
||||
style="font-style:italic;-inkscape-font-specification:'Sans Italic'"
|
||||
style="font-style:italic;-inkscape-font-specification:Sans Italic"
|
||||
id="flowSpan3798">;o</flowSpan>, in new tab</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3800">;y - yank hinted URL to clipboard</flowPara><flowPara
|
||||
@@ -3020,24 +3013,24 @@
|
||||
id="flowPara3804">;r - rapid hinting</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3806">;R - like <flowSpan
|
||||
style="font-style:italic;-inkscape-font-specification:'Sans Italic'"
|
||||
style="font-style:italic;-inkscape-font-specification:Sans Italic"
|
||||
id="flowSpan3810">;r</flowSpan>, in new window</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3808">;d - download hinted URL</flowPara></flowRoot> <flowRoot
|
||||
transform="translate(706.84131,-38.539167)"
|
||||
transform="translate(706.84131,-14.539167)"
|
||||
xml:space="preserve"
|
||||
id="flowRoot5691-4-9-3-6-1"
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
|
||||
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><flowRegion
|
||||
id="flowRegion5693-9-1-7-3-5"><rect
|
||||
id="rect5695-9-8-7-7-0"
|
||||
width="154.90645"
|
||||
height="240.73535"
|
||||
width="148.08141"
|
||||
height="203.19766"
|
||||
x="17.5"
|
||||
y="448.75"
|
||||
style="fill:#000000" /></flowRegion><flowPara
|
||||
id="flowPara5697-3-7-6-8-2"
|
||||
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"><flowSpan
|
||||
style="font-weight:bold;-inkscape-font-specification:'Sans Bold';fill:#ff0000"
|
||||
style="font-size:10px;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"><flowSpan
|
||||
style="font-weight:bold;fill:#ff0000;-inkscape-font-specification:Sans Bold"
|
||||
id="flowSpan3852">(10)</flowSpan> misc. commands:</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3725-0"><flowSpan
|
||||
@@ -3059,7 +3052,7 @@
|
||||
id="flowPara3915">gu - navigate up in URL</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3917">gU - like <flowSpan
|
||||
style="font-style:italic;-inkscape-font-specification:'Sans Italic'"
|
||||
style="font-style:italic;-inkscape-font-specification:Sans Italic"
|
||||
id="flowSpan3923">gu</flowSpan>, in new tab</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3921">sf - save config</flowPara><flowPara
|
||||
@@ -3079,16 +3072,10 @@
|
||||
id="flowPara4169"><flowSpan
|
||||
style="fill:#0000ff"
|
||||
id="flowSpan5438">ad</flowSpan> - cancel download</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara4077">co - close other tabs</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara4081">cd - clear downloads</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3933" /><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3935" /><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara4079" /></flowRoot> <text
|
||||
id="flowPara3935" /></flowRoot> <text
|
||||
sodipodi:linespacing="89.999998%"
|
||||
id="text9514-8-9-0-8"
|
||||
y="204.26315"
|
||||
@@ -3125,10 +3112,10 @@
|
||||
id="tspan4936-1-1-9-59-5"
|
||||
style="font-size:8px;fill:#ff0000">(10)</tspan></text>
|
||||
<flowRoot
|
||||
transform="translate(841.04351,-38.539167)"
|
||||
transform="translate(841.04351,-14.539167)"
|
||||
xml:space="preserve"
|
||||
id="flowRoot5691-4-9-3-6-1-2"
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
|
||||
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><flowRegion
|
||||
id="flowRegion5693-9-1-7-3-5-2"><rect
|
||||
id="rect5695-9-8-7-7-0-9"
|
||||
width="328.31396"
|
||||
@@ -3137,8 +3124,8 @@
|
||||
y="448.75"
|
||||
style="fill:#000000" /></flowRegion><flowPara
|
||||
id="flowPara5697-3-7-6-8-2-0"
|
||||
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"><flowSpan
|
||||
style="font-weight:bold;-inkscape-font-specification:'Sans Bold';fill:#ff0000"
|
||||
style="font-size:10px;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"><flowSpan
|
||||
style="font-weight:bold;fill:#ff0000;-inkscape-font-specification:Sans Bold"
|
||||
id="flowSpan3852-6">(11)</flowSpan> modifier commands:</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3933-6"><Alt-num> - select tab</flowPara><flowPara
|
||||
@@ -3154,11 +3141,11 @@
|
||||
id="flowPara4138"><Ctrl-S> - stop loading</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara4140"><Ctrl-Alt-P> - print</flowPara><flowPara
|
||||
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"
|
||||
style="font-size:10px;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"
|
||||
id="flowPara4142">in insert mode:</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara4144"><Ctrl-E> - open editor</flowPara><flowPara
|
||||
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#000000"
|
||||
style="font-size:10px;font-weight:bold;fill:#000000;-inkscape-font-specification:Sans Bold"
|
||||
id="flowPara4146">in command mode:</flowPara><flowPara
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara4148"><Ctrl-P> - prev. history item</flowPara><flowPara
|
||||
@@ -3167,142 +3154,126 @@
|
||||
style="font-size:18px;fill:#eeeeec;fill-opacity:1;stroke:none"
|
||||
id="rect3764-9"
|
||||
width="60"
|
||||
height="45.993073"
|
||||
height="60"
|
||||
x="168.32558"
|
||||
y="362"
|
||||
ry="3.4348924" />
|
||||
ry="4.480969" />
|
||||
<rect
|
||||
style="font-size:18px;fill:#eeeeec;fill-opacity:1;stroke:none"
|
||||
id="rect3764-9-3"
|
||||
width="60"
|
||||
height="45.993073"
|
||||
height="60"
|
||||
x="47.906979"
|
||||
y="362"
|
||||
ry="3.4348924" />
|
||||
ry="4.480969" />
|
||||
<rect
|
||||
style="font-size:18px;fill:#eeeeec;fill-opacity:1;stroke:none"
|
||||
id="rect3764-9-1"
|
||||
width="60"
|
||||
height="45.993073"
|
||||
height="60"
|
||||
x="613.81396"
|
||||
y="362"
|
||||
ry="3.4348924" />
|
||||
ry="4.480969" />
|
||||
<rect
|
||||
style="font-size:18px;fill:#eeeeec;fill-opacity:1;stroke:none"
|
||||
id="rect3764-9-7"
|
||||
width="60"
|
||||
height="45.993073"
|
||||
height="60"
|
||||
x="730.46509"
|
||||
y="362"
|
||||
ry="3.4348924" />
|
||||
<g
|
||||
id="g4049"
|
||||
transform="translate(1.3728676,-1.9658966)">
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-weight:normal;font-size:12px;font-family:'DejaVu Sans Mono';fill:#000000;fill-opacity:1;stroke:none"
|
||||
ry="4.480969" />
|
||||
<text
|
||||
id="text7358-8"
|
||||
y="395.78867"
|
||||
x="62.269463"
|
||||
style="font-size:12px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:DejaVu Sans Mono"
|
||||
xml:space="preserve"><tspan
|
||||
y="395.78867"
|
||||
x="62.269463"
|
||||
y="385.78867"
|
||||
id="text7358-8"><tspan
|
||||
style="font-size:12px;font-family:'DejaVu Sans Mono'"
|
||||
sodipodi:role="line"
|
||||
id="tspan7360-1"
|
||||
x="62.269463"
|
||||
y="385.78867">Ctrl</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:8px;line-height:89.99999762%;font-family:TlwgTypewriter;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
|
||||
x="67.315361"
|
||||
y="400.26315"
|
||||
id="text9514-8-9-0-8-4-0"
|
||||
sodipodi:linespacing="89.999998%"><tspan
|
||||
style="font-size:8px;fill:#ff0000"
|
||||
id="tspan4936-1-1-9-59-8-3"
|
||||
sodipodi:role="line"
|
||||
x="67.315361"
|
||||
y="400.26315">(11)</tspan></text>
|
||||
</g>
|
||||
<g
|
||||
id="g4055"
|
||||
transform="translate(1.6278992,-11.965897)">
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-weight:normal;font-size:12px;font-family:'DejaVu Sans Mono';fill:#000000;fill-opacity:1;stroke:none"
|
||||
x="186.34709"
|
||||
id="tspan7360-1"
|
||||
sodipodi:role="line"
|
||||
style="font-size:12px;font-family:DejaVu Sans Mono">Ctrl</tspan></text>
|
||||
<text
|
||||
id="text7358-8-3"
|
||||
y="395.78867"
|
||||
x="745.17719"
|
||||
style="font-size:12px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:DejaVu Sans Mono"
|
||||
xml:space="preserve"><tspan
|
||||
y="395.78867"
|
||||
id="text7358-8-3-8-1"><tspan
|
||||
style="font-size:12px;font-family:'DejaVu Sans Mono'"
|
||||
sodipodi:role="line"
|
||||
id="tspan7360-1-7-0-2"
|
||||
x="186.34709"
|
||||
y="395.78867">Alt</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:8px;line-height:89.99999762%;font-family:TlwgTypewriter;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
|
||||
x="187.47893"
|
||||
y="410.26315"
|
||||
id="text9514-8-9-0-8-4-0-8"
|
||||
sodipodi:linespacing="89.999998%"><tspan
|
||||
style="font-size:8px;fill:#ff0000"
|
||||
id="tspan4936-1-1-9-59-8-3-8"
|
||||
sodipodi:role="line"
|
||||
x="187.47893"
|
||||
y="410.26315">(11)</tspan></text>
|
||||
</g>
|
||||
<g
|
||||
id="g4065"
|
||||
transform="translate(5.706604,-11.965897)">
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-weight:normal;font-size:12px;font-family:'DejaVu Sans Mono';fill:#000000;fill-opacity:1;stroke:none"
|
||||
x="627.75677"
|
||||
y="395.78867"
|
||||
id="text7358-8-3-8"><tspan
|
||||
style="font-size:12px;font-family:'DejaVu Sans Mono'"
|
||||
sodipodi:role="line"
|
||||
id="tspan7360-1-7-0"
|
||||
x="627.75677"
|
||||
y="395.78867">Alt</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:8px;line-height:89.99999762%;font-family:TlwgTypewriter;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
|
||||
x="628.88861"
|
||||
y="410.26315"
|
||||
id="text9514-8-9-0-8-4-0-7"
|
||||
sodipodi:linespacing="89.999998%"><tspan
|
||||
style="font-size:8px;fill:#ff0000"
|
||||
id="tspan4936-1-1-9-59-8-3-82"
|
||||
sodipodi:role="line"
|
||||
x="628.88861"
|
||||
y="410.26315">(11)</tspan></text>
|
||||
</g>
|
||||
<g
|
||||
id="g4071"
|
||||
transform="translate(1.0232544,-11.965897)">
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-weight:normal;font-size:12px;font-family:'DejaVu Sans Mono';fill:#000000;fill-opacity:1;stroke:none"
|
||||
x="745.17719"
|
||||
id="tspan7360-1-7"
|
||||
sodipodi:role="line"
|
||||
style="font-size:12px;font-family:DejaVu Sans Mono">Ctrl</tspan></text>
|
||||
<text
|
||||
id="text7358-8-3-8"
|
||||
y="395.78867"
|
||||
x="627.75677"
|
||||
style="font-size:12px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:DejaVu Sans Mono"
|
||||
xml:space="preserve"><tspan
|
||||
y="395.78867"
|
||||
id="text7358-8-3"><tspan
|
||||
style="font-size:12px;font-family:'DejaVu Sans Mono'"
|
||||
sodipodi:role="line"
|
||||
id="tspan7360-1-7"
|
||||
x="745.17719"
|
||||
y="395.78867">Ctrl</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:8px;line-height:89.99999762%;font-family:TlwgTypewriter;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
|
||||
x="750.22308"
|
||||
x="627.75677"
|
||||
id="tspan7360-1-7-0"
|
||||
sodipodi:role="line"
|
||||
style="font-size:12px;font-family:DejaVu Sans Mono">Alt</tspan></text>
|
||||
<text
|
||||
id="text7358-8-3-8-1"
|
||||
y="395.78867"
|
||||
x="186.34709"
|
||||
style="font-size:12px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:DejaVu Sans Mono"
|
||||
xml:space="preserve"><tspan
|
||||
y="395.78867"
|
||||
x="186.34709"
|
||||
id="tspan7360-1-7-0-2"
|
||||
sodipodi:role="line"
|
||||
style="font-size:12px;font-family:DejaVu Sans Mono">Alt</tspan></text>
|
||||
<text
|
||||
sodipodi:linespacing="89.999998%"
|
||||
id="text9514-8-9-0-8-4-0"
|
||||
y="410.26315"
|
||||
x="67.315361"
|
||||
style="font-size:8px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:89.99999762%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:TlwgTypewriter"
|
||||
xml:space="preserve"><tspan
|
||||
y="410.26315"
|
||||
id="text9514-8-9-0-8-4-0-3"
|
||||
sodipodi:linespacing="89.999998%"><tspan
|
||||
style="font-size:8px;fill:#ff0000"
|
||||
id="tspan4936-1-1-9-59-8-3-4"
|
||||
sodipodi:role="line"
|
||||
x="750.22308"
|
||||
y="410.26315">(11)</tspan></text>
|
||||
</g>
|
||||
x="67.315361"
|
||||
sodipodi:role="line"
|
||||
id="tspan4936-1-1-9-59-8-3"
|
||||
style="font-size:8px;fill:#ff0000">(11)</tspan></text>
|
||||
<text
|
||||
sodipodi:linespacing="89.999998%"
|
||||
id="text9514-8-9-0-8-4-0-8"
|
||||
y="410.26315"
|
||||
x="187.47893"
|
||||
style="font-size:8px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:89.99999762%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:TlwgTypewriter"
|
||||
xml:space="preserve"><tspan
|
||||
y="410.26315"
|
||||
x="187.47893"
|
||||
sodipodi:role="line"
|
||||
id="tspan4936-1-1-9-59-8-3-8"
|
||||
style="font-size:8px;fill:#ff0000">(11)</tspan></text>
|
||||
<text
|
||||
sodipodi:linespacing="89.999998%"
|
||||
id="text9514-8-9-0-8-4-0-7"
|
||||
y="410.26315"
|
||||
x="628.88861"
|
||||
style="font-size:8px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:89.99999762%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:TlwgTypewriter"
|
||||
xml:space="preserve"><tspan
|
||||
y="410.26315"
|
||||
x="628.88861"
|
||||
sodipodi:role="line"
|
||||
id="tspan4936-1-1-9-59-8-3-82"
|
||||
style="font-size:8px;fill:#ff0000">(11)</tspan></text>
|
||||
<text
|
||||
sodipodi:linespacing="89.999998%"
|
||||
id="text9514-8-9-0-8-4-0-3"
|
||||
y="410.26315"
|
||||
x="750.22308"
|
||||
style="font-size:8px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:89.99999762%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:TlwgTypewriter"
|
||||
xml:space="preserve"><tspan
|
||||
y="410.26315"
|
||||
x="750.22308"
|
||||
sodipodi:role="line"
|
||||
id="tspan4936-1-1-9-59-8-3-4"
|
||||
style="font-size:8px;fill:#ff0000">(11)</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:9px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:89.99999762%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:TlwgTypewriter"
|
||||
@@ -3326,15 +3297,27 @@
|
||||
style="font-size:8px">tab</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:8px;line-height:89.99999762%;font-family:TlwgTypewriter;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#ff0000;fill-opacity:1;stroke:none"
|
||||
x="274.21381"
|
||||
y="343.17578"
|
||||
style="font-size:8px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:89.99999762%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:TlwgTypewriter"
|
||||
x="267.67316"
|
||||
y="326.20523"
|
||||
id="text10547-23-6-7"
|
||||
sodipodi:linespacing="89.999998%"><tspan
|
||||
sodipodi:role="line"
|
||||
x="274.21381"
|
||||
y="343.17578"
|
||||
id="tspan4052">(10)</tspan></text>
|
||||
x="267.67316"
|
||||
y="326.20523"
|
||||
id="tspan10560-1-3-1" /><tspan
|
||||
sodipodi:role="line"
|
||||
x="267.67316"
|
||||
y="333.40524"
|
||||
id="tspan5325">co: close</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="267.67316"
|
||||
y="340.60522"
|
||||
id="tspan5327">other</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="267.67316"
|
||||
y="347.80524"
|
||||
id="tspan10562-12-5-98">tabs</tspan></text>
|
||||
<text
|
||||
sodipodi:linespacing="89.999998%"
|
||||
id="text10564-6-7-8-0"
|
||||
@@ -3415,10 +3398,10 @@
|
||||
id="tspan4936-1-1-9-59-5-6"
|
||||
style="font-size:8px;fill:#ff0000">(10)</tspan></text>
|
||||
<flowRoot
|
||||
transform="translate(838.55559,134.52236)"
|
||||
transform="translate(838.55559,158.52236)"
|
||||
xml:space="preserve"
|
||||
id="flowRoot5691-4-9-3-6-6"
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
|
||||
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"><flowRegion
|
||||
id="flowRegion5693-9-1-7-3-8"><rect
|
||||
id="rect5695-9-8-7-7-6"
|
||||
width="322.5"
|
||||
@@ -3429,50 +3412,9 @@
|
||||
style="font-size:10px;fill:#000000"
|
||||
id="flowPara3626-7-0"><flowSpan
|
||||
id="flowSpan5520"
|
||||
style="font-weight:bold;font-size:10px;-inkscape-font-specification:'Sans Bold';fill:#0000ff">blue keys </flowSpan><flowSpan
|
||||
style="font-size:10px;font-weight:bold;fill:#0000ff;-inkscape-font-specification:Sans Bold">blue keys </flowSpan><flowSpan
|
||||
style="fill:#0000ff"
|
||||
id="flowSpan5528">can be</flowSpan></flowPara><flowPara
|
||||
style="font-size:10px;fill:#0000ff"
|
||||
id="flowPara3725-9">prefixed by a count</flowPara></flowRoot> <text
|
||||
xml:space="preserve"
|
||||
style="font-size:9px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:89.99999762%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:TlwgTypewriter"
|
||||
x="317.95987"
|
||||
y="155.85321"
|
||||
id="text7245-1-7"
|
||||
sodipodi:linespacing="89.999998%"><tspan
|
||||
sodipodi:role="line"
|
||||
x="317.95987"
|
||||
y="155.85321"
|
||||
id="tspan7366-3-3" /><tspan
|
||||
sodipodi:role="line"
|
||||
x="317.95987"
|
||||
y="163.23555"
|
||||
id="tspan5293-5"
|
||||
style="font-size:8px">reload </tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="317.95987"
|
||||
y="170.43555"
|
||||
style="font-size:8px"
|
||||
id="tspan3716">(bypass </tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="317.95987"
|
||||
y="177.63554"
|
||||
style="font-size:8px"
|
||||
id="tspan3719">cache)</tspan></text>
|
||||
<text
|
||||
sodipodi:linespacing="89.999998%"
|
||||
id="text9514-60-7-7-0-8"
|
||||
y="338.04874"
|
||||
x="342.42523"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:8px;line-height:89.99999762%;font-family:TlwgTypewriter;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
|
||||
xml:space="preserve"><tspan
|
||||
y="338.04874"
|
||||
x="342.42523"
|
||||
sodipodi:role="line"
|
||||
id="tspan5689-6">visual</tspan><tspan
|
||||
y="345.24875"
|
||||
x="342.42523"
|
||||
sodipodi:role="line"
|
||||
id="tspan4112">mode</tspan></text>
|
||||
</g>
|
||||
id="flowPara3725-9">prefixed by a count</flowPara></flowRoot> </g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 135 KiB |
@@ -1,7 +0,0 @@
|
||||
These files are copied from Qt's source tree in
|
||||
src/plugins/platforms/cocoa/qt_menu.nib at revision
|
||||
b8246f08e49eb672974fd3d3d972a5ff13c1524d.
|
||||
|
||||
http://code.qt.io/cgit/qt/qtbase.git/tree/src/plugins/platforms/cocoa/qt_menu.nib
|
||||
|
||||
They are needed for cx_Freeze and don't seem to be bundled with Qt anymore.
|
||||
59
misc/qt_menu.nib/classes.nib
generated
59
misc/qt_menu.nib/classes.nib
generated
@@ -1,59 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IBClasses</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>ACTIONS</key>
|
||||
<dict>
|
||||
<key>hide</key>
|
||||
<string>id</string>
|
||||
<key>hideOtherApplications</key>
|
||||
<string>id</string>
|
||||
<key>orderFrontStandardAboutPanel</key>
|
||||
<string>id</string>
|
||||
<key>qtDispatcherToQPAMenuItem</key>
|
||||
<string>id</string>
|
||||
<key>terminate</key>
|
||||
<string>id</string>
|
||||
<key>unhideAllApplications</key>
|
||||
<string>id</string>
|
||||
</dict>
|
||||
<key>CLASS</key>
|
||||
<string>QCocoaMenuLoader</string>
|
||||
<key>LANGUAGE</key>
|
||||
<string>ObjC</string>
|
||||
<key>OUTLETS</key>
|
||||
<dict>
|
||||
<key>aboutItem</key>
|
||||
<string>NSMenuItem</string>
|
||||
<key>aboutQtItem</key>
|
||||
<string>NSMenuItem</string>
|
||||
<key>appMenu</key>
|
||||
<string>NSMenu</string>
|
||||
<key>hideItem</key>
|
||||
<string>NSMenuItem</string>
|
||||
<key>preferencesItem</key>
|
||||
<string>NSMenuItem</string>
|
||||
<key>quitItem</key>
|
||||
<string>NSMenuItem</string>
|
||||
<key>theMenu</key>
|
||||
<string>NSMenu</string>
|
||||
</dict>
|
||||
<key>SUPERCLASS</key>
|
||||
<string>NSResponder</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CLASS</key>
|
||||
<string>FirstResponder</string>
|
||||
<key>LANGUAGE</key>
|
||||
<string>ObjC</string>
|
||||
<key>SUPERCLASS</key>
|
||||
<string>NSObject</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>IBVersion</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
</plist>
|
||||
18
misc/qt_menu.nib/info.nib
generated
18
misc/qt_menu.nib/info.nib
generated
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IBFramework Version</key>
|
||||
<string>672</string>
|
||||
<key>IBOldestOS</key>
|
||||
<integer>5</integer>
|
||||
<key>IBOpenObjects</key>
|
||||
<array>
|
||||
<integer>57</integer>
|
||||
</array>
|
||||
<key>IBSystem Version</key>
|
||||
<string>9L31a</string>
|
||||
<key>targetFramework</key>
|
||||
<string>IBCocoaFramework</string>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
misc/qt_menu.nib/keyedobjects.nib
generated
BIN
misc/qt_menu.nib/keyedobjects.nib
generated
Binary file not shown.
@@ -1,48 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Copyright 2015 Zach-Button <zachrey.button@gmail.com>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Pipes history, quickmarks, and URL into dmenu.
|
||||
#
|
||||
# If run from qutebrowser as a userscript, it runs :open on the URL
|
||||
# If not, it opens a new qutebrowser window at the URL
|
||||
#
|
||||
# Ideal for use with tabs-are-windows. Set a hotkey to launch this script, then:
|
||||
# :bind o spawn --userscript dmenu_qutebrowser
|
||||
#
|
||||
# Use the hotkey to open in new tab/window, press 'o' to open URL in current tab/window
|
||||
# You can simulate "go" by pressing "o<tab>", as the current URL is always first in the list
|
||||
#
|
||||
# I personally use "<Mod4>o" to launch this script. For me, my workflow is:
|
||||
# Default keys Keys with this script
|
||||
# O <Mod4>o
|
||||
# o o
|
||||
# go o<Tab>
|
||||
# gO gC, then o<Tab>
|
||||
# (This is unnecessarily long. I use this rarely, feel free to make this script accept parameters.)
|
||||
#
|
||||
|
||||
[ -z "$QUTE_URL" ] && QUTE_URL='http://google.com'
|
||||
|
||||
url=$(echo "$QUTE_URL" | cat - ~/.config/qutebrowser/quickmarks ~/.local/share/qutebrowser/history | dmenu -l 15 -p qutebrowser)
|
||||
url=$(echo $url | sed -E 's/[^ ]+ +//g' | egrep "https?:" || echo $url)
|
||||
|
||||
[ -z "${url// }" ] && exit
|
||||
|
||||
echo "open $url" >> "$QUTE_FIFO" || qutebrowser "$url"
|
||||
|
||||
@@ -28,7 +28,7 @@ __copyright__ = "Copyright 2014-2015 Florian Bruhin (The Compiler)"
|
||||
__license__ = "GPL"
|
||||
__maintainer__ = __author__
|
||||
__email__ = "mail@qutebrowser.org"
|
||||
__version_info__ = (0, 3, 0)
|
||||
__version_info__ = (0, 1, 2)
|
||||
__version__ = '.'.join(map(str, __version_info__))
|
||||
__description__ = "A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit."
|
||||
|
||||
|
||||
1348
qutebrowser/app.py
1348
qutebrowser/app.py
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Functions related to ad blocking."""
|
||||
"""Functions related to adblocking."""
|
||||
|
||||
import io
|
||||
import os.path
|
||||
@@ -25,9 +25,11 @@ import functools
|
||||
import posixpath
|
||||
import zipfile
|
||||
|
||||
from PyQt5.QtCore import QStandardPaths
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import objreg, standarddir, log, message
|
||||
from qutebrowser.commands import cmdutils, cmdexc
|
||||
from qutebrowser.commands import cmdutils
|
||||
|
||||
|
||||
def guess_zip_filename(zf):
|
||||
@@ -90,18 +92,13 @@ class HostBlocker:
|
||||
self.blocked_hosts = set()
|
||||
self._in_progress = []
|
||||
self._done_count = 0
|
||||
data_dir = standarddir.data()
|
||||
if data_dir is None:
|
||||
self._hosts_file = None
|
||||
else:
|
||||
self._hosts_file = os.path.join(data_dir, 'blocked-hosts')
|
||||
data_dir = standarddir.get(QStandardPaths.DataLocation)
|
||||
self._hosts_file = os.path.join(data_dir, 'blocked-hosts')
|
||||
objreg.get('config').changed.connect(self.on_config_changed)
|
||||
|
||||
def read_hosts(self):
|
||||
"""Read hosts from the existing blocked-hosts file."""
|
||||
self.blocked_hosts = set()
|
||||
if self._hosts_file is None:
|
||||
return
|
||||
if os.path.exists(self._hosts_file):
|
||||
try:
|
||||
with open(self._hosts_file, 'r', encoding='utf-8') as f:
|
||||
@@ -110,17 +107,13 @@ class HostBlocker:
|
||||
except OSError:
|
||||
log.misc.exception("Failed to read host blocklist!")
|
||||
else:
|
||||
args = objreg.get('args')
|
||||
if (config.get('content', 'host-block-lists') is not None and
|
||||
args.basedir is None):
|
||||
message.info('current',
|
||||
if config.get('content', 'host-block-lists') is not None:
|
||||
message.info('last-focused',
|
||||
"Run :adblock-update to get adblock lists.")
|
||||
|
||||
@cmdutils.register(instance='host-blocker', win_id='win_id')
|
||||
def adblock_update(self, win_id):
|
||||
@cmdutils.register(instance='host-blocker')
|
||||
def adblock_update(self, win_id: {'special': 'win_id'}):
|
||||
"""Update the adblock block lists."""
|
||||
if self._hosts_file is None:
|
||||
raise cmdexc.CommandError("No data storage is configured!")
|
||||
self.blocked_hosts = set()
|
||||
self._done_count = 0
|
||||
urls = config.get('content', 'host-block-lists')
|
||||
@@ -163,8 +156,9 @@ class HostBlocker:
|
||||
f = get_fileobj(byte_io)
|
||||
except (OSError, UnicodeDecodeError, zipfile.BadZipFile,
|
||||
zipfile.LargeZipFile) as e:
|
||||
message.error('current', "adblock: Error while reading {}: {} - "
|
||||
"{}".format(byte_io.name, e.__class__.__name__, e))
|
||||
message.error('last-focused', "adblock: Error while reading {}: "
|
||||
"{} - {}".format(
|
||||
byte_io.name, e.__class__.__name__, e))
|
||||
return
|
||||
for line in f:
|
||||
line_count += 1
|
||||
@@ -192,16 +186,17 @@ class HostBlocker:
|
||||
self.blocked_hosts.add(host)
|
||||
log.misc.debug("{}: read {} lines".format(byte_io.name, line_count))
|
||||
if error_count > 0:
|
||||
message.error('current', "adblock: {} read errors for {}".format(
|
||||
error_count, byte_io.name))
|
||||
message.error('last-focused', "adblock: {} read errors for "
|
||||
"{}".format(error_count, byte_io.name))
|
||||
|
||||
def on_lists_downloaded(self):
|
||||
"""Install block lists after files have been downloaded."""
|
||||
with open(self._hosts_file, 'w', encoding='utf-8') as f:
|
||||
for host in sorted(self.blocked_hosts):
|
||||
f.write(host + '\n')
|
||||
message.info('current', "adblock: Read {} hosts from {} sources."
|
||||
.format(len(self.blocked_hosts), self._done_count))
|
||||
message.info('last-focused', "adblock: Read {} hosts from {} "
|
||||
"sources.".format(len(self.blocked_hosts),
|
||||
self._done_count))
|
||||
|
||||
@config.change_filter('content', 'host-block-lists')
|
||||
def on_config_changed(self):
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
import os.path
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot
|
||||
from PyQt5.QtCore import QStandardPaths
|
||||
from PyQt5.QtNetwork import QNetworkDiskCache, QNetworkCacheMetaData
|
||||
|
||||
from qutebrowser.config import config
|
||||
@@ -30,41 +30,24 @@ from qutebrowser.utils import utils, standarddir, objreg
|
||||
|
||||
class DiskCache(QNetworkDiskCache):
|
||||
|
||||
"""Disk cache which sets correct cache dir and size.
|
||||
|
||||
Attributes:
|
||||
_activated: Whether the cache should be used.
|
||||
"""
|
||||
"""Disk cache which sets correct cache dir and size."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
cache_dir = standarddir.cache()
|
||||
if config.get('general', 'private-browsing') or cache_dir is None:
|
||||
self._activated = False
|
||||
else:
|
||||
self._activated = True
|
||||
self.setCacheDirectory(os.path.join(standarddir.cache(), 'http'))
|
||||
cache_dir = standarddir.get(QStandardPaths.CacheLocation)
|
||||
self.setCacheDirectory(os.path.join(cache_dir, 'http'))
|
||||
self.setMaximumCacheSize(config.get('storage', 'cache-size'))
|
||||
objreg.get('config').changed.connect(self.on_config_changed)
|
||||
objreg.get('config').changed.connect(self.cache_size_changed)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, size=self.cacheSize(),
|
||||
maxsize=self.maximumCacheSize(),
|
||||
path=self.cacheDirectory())
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def on_config_changed(self, section, option):
|
||||
"""Update cache size/activated if the config was changed."""
|
||||
if (section, option) == ('storage', 'cache-size'):
|
||||
self.setMaximumCacheSize(config.get('storage', 'cache-size'))
|
||||
elif (section, option) == ('general', 'private-browsing'):
|
||||
if (config.get('general', 'private-browsing') or
|
||||
standarddir.cache() is None):
|
||||
self._activated = False
|
||||
else:
|
||||
self._activated = True
|
||||
self.setCacheDirectory(
|
||||
os.path.join(standarddir.cache(), 'http'))
|
||||
@config.change_filter('storage', 'cache-size')
|
||||
def cache_size_changed(self):
|
||||
"""Update cache size if the config was changed."""
|
||||
self.setMaximumCacheSize(config.get('storage', 'cache-size'))
|
||||
|
||||
def cacheSize(self):
|
||||
"""Return the current size taken up by the cache.
|
||||
@@ -72,13 +55,13 @@ class DiskCache(QNetworkDiskCache):
|
||||
Return:
|
||||
An int.
|
||||
"""
|
||||
if self._activated:
|
||||
return super().cacheSize()
|
||||
else:
|
||||
if objreg.get('general', 'private-browsing'):
|
||||
return 0
|
||||
else:
|
||||
return super().cacheSize()
|
||||
|
||||
def fileMetaData(self, filename):
|
||||
"""Return the QNetworkCacheMetaData for the cache file filename.
|
||||
"""Returns the QNetworkCacheMetaData for the cache file filename.
|
||||
|
||||
Args:
|
||||
filename: The file name as a string.
|
||||
@@ -86,10 +69,10 @@ class DiskCache(QNetworkDiskCache):
|
||||
Return:
|
||||
A QNetworkCacheMetaData object.
|
||||
"""
|
||||
if self._activated:
|
||||
return super().fileMetaData(filename)
|
||||
else:
|
||||
if objreg.get('general', 'private-browsing'):
|
||||
return QNetworkCacheMetaData()
|
||||
else:
|
||||
return super().fileMetaData(filename)
|
||||
|
||||
def data(self, url):
|
||||
"""Return the data associated with url.
|
||||
@@ -100,10 +83,10 @@ class DiskCache(QNetworkDiskCache):
|
||||
return:
|
||||
A QIODevice or None.
|
||||
"""
|
||||
if self._activated:
|
||||
return super().data(url)
|
||||
else:
|
||||
if objreg.get('general', 'private-browsing'):
|
||||
return None
|
||||
else:
|
||||
return super().data(url)
|
||||
|
||||
def insert(self, device):
|
||||
"""Insert the data in device and the prepared meta data into the cache.
|
||||
@@ -111,10 +94,10 @@ class DiskCache(QNetworkDiskCache):
|
||||
Args:
|
||||
device: A QIODevice.
|
||||
"""
|
||||
if self._activated:
|
||||
super().insert(device)
|
||||
if objreg.get('general', 'private-browsing'):
|
||||
return
|
||||
else:
|
||||
return None
|
||||
super().insert(device)
|
||||
|
||||
def metaData(self, url):
|
||||
"""Return the meta data for the url url.
|
||||
@@ -125,10 +108,10 @@ class DiskCache(QNetworkDiskCache):
|
||||
Return:
|
||||
A QNetworkCacheMetaData object.
|
||||
"""
|
||||
if self._activated:
|
||||
return super().metaData(url)
|
||||
else:
|
||||
if objreg.get('general', 'private-browsing'):
|
||||
return QNetworkCacheMetaData()
|
||||
else:
|
||||
return super().metaData(url)
|
||||
|
||||
def prepare(self, meta_data):
|
||||
"""Return the device that should be populated with the data.
|
||||
@@ -139,10 +122,10 @@ class DiskCache(QNetworkDiskCache):
|
||||
Return:
|
||||
A QIODevice or None.
|
||||
"""
|
||||
if self._activated:
|
||||
return super().prepare(meta_data)
|
||||
else:
|
||||
if objreg.get('general', 'private-browsing'):
|
||||
return None
|
||||
else:
|
||||
return super().prepare(meta_data)
|
||||
|
||||
def remove(self, url):
|
||||
"""Remove the cache entry for url.
|
||||
@@ -150,25 +133,25 @@ class DiskCache(QNetworkDiskCache):
|
||||
Return:
|
||||
True on success, False otherwise.
|
||||
"""
|
||||
if self._activated:
|
||||
return super().remove(url)
|
||||
else:
|
||||
if objreg.get('general', 'private-browsing'):
|
||||
return False
|
||||
else:
|
||||
return super().remove(url)
|
||||
|
||||
def updateMetaData(self, meta_data):
|
||||
"""Update the cache meta date for the meta_data's url to meta_data.
|
||||
"""Updates the cache meta date for the meta_data's url to meta_data.
|
||||
|
||||
Args:
|
||||
meta_data: A QNetworkCacheMetaData object.
|
||||
"""
|
||||
if self._activated:
|
||||
super().updateMetaData(meta_data)
|
||||
else:
|
||||
if objreg.get('general', 'private-browsing'):
|
||||
return
|
||||
else:
|
||||
super().updateMetaData(meta_data)
|
||||
|
||||
def clear(self):
|
||||
"""Remove all items from the cache."""
|
||||
if self._activated:
|
||||
super().clear()
|
||||
else:
|
||||
"""Removes all items from the cache."""
|
||||
if objreg.get('general', 'private-browsing'):
|
||||
return
|
||||
else:
|
||||
super().clear()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,25 +20,16 @@
|
||||
"""Handling of HTTP cookies."""
|
||||
|
||||
from PyQt5.QtNetwork import QNetworkCookie, QNetworkCookieJar
|
||||
from PyQt5.QtCore import pyqtSignal, QDateTime
|
||||
from PyQt5.QtCore import QStandardPaths, QDateTime
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.config.parsers import line as lineparser
|
||||
from qutebrowser.utils import utils, standarddir, objreg
|
||||
from qutebrowser.misc import lineparser
|
||||
|
||||
|
||||
class RAMCookieJar(QNetworkCookieJar):
|
||||
|
||||
"""An in-RAM cookie jar.
|
||||
|
||||
Signals:
|
||||
changed: Emitted when the cookie store was changed.
|
||||
"""
|
||||
|
||||
changed = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
"""An in-RAM cookie jar."""
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, count=len(self.allCookies()))
|
||||
@@ -56,7 +47,6 @@ class RAMCookieJar(QNetworkCookieJar):
|
||||
if config.get('content', 'cookies-accept') == 'never':
|
||||
return False
|
||||
else:
|
||||
self.changed.emit()
|
||||
return super().setCookiesFromUrl(cookies, url)
|
||||
|
||||
|
||||
@@ -65,26 +55,24 @@ class CookieJar(RAMCookieJar):
|
||||
"""A cookie jar saving cookies to disk.
|
||||
|
||||
Attributes:
|
||||
_lineparser: The LineParser managing the cookies file.
|
||||
_linecp: The LineConfigParser managing the cookies file.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._lineparser = lineparser.LineParser(
|
||||
standarddir.data(), 'cookies', binary=True, parent=self)
|
||||
datadir = standarddir.get(QStandardPaths.DataLocation)
|
||||
self._linecp = lineparser.LineConfigParser(datadir, 'cookies',
|
||||
binary=True)
|
||||
cookies = []
|
||||
for line in self._lineparser:
|
||||
for line in self._linecp:
|
||||
cookies += QNetworkCookie.parseCookies(line)
|
||||
self.setAllCookies(cookies)
|
||||
objreg.get('config').changed.connect(self.cookies_store_changed)
|
||||
objreg.get('save-manager').add_saveable(
|
||||
'cookies', self.save, self.changed,
|
||||
config_opt=('content', 'cookies-store'))
|
||||
|
||||
def purge_old_cookies(self):
|
||||
"""Purge expired cookies from the cookie jar."""
|
||||
# Based on:
|
||||
# http://doc.qt.io/qt-5/qtwebkitexamples-webkitwidgets-browser-cookiejar-cpp.html
|
||||
# http://qt-project.org/doc/qt-5/qtwebkitexamples-webkitwidgets-browser-cookiejar-cpp.html
|
||||
now = QDateTime.currentDateTime()
|
||||
cookies = [c for c in self.allCookies()
|
||||
if c.isSessionCookie() or c.expirationDate() >= now]
|
||||
@@ -92,18 +80,19 @@ class CookieJar(RAMCookieJar):
|
||||
|
||||
def save(self):
|
||||
"""Save cookies to disk."""
|
||||
if not config.get('content', 'cookies-store'):
|
||||
return
|
||||
self.purge_old_cookies()
|
||||
lines = []
|
||||
for cookie in self.allCookies():
|
||||
if not cookie.isSessionCookie():
|
||||
lines.append(cookie.toRawForm())
|
||||
self._lineparser.data = lines
|
||||
self._lineparser.save()
|
||||
self._linecp.data = lines
|
||||
self._linecp.save()
|
||||
|
||||
@config.change_filter('content', 'cookies-store')
|
||||
def cookies_store_changed(self):
|
||||
"""Delete stored cookies if cookies-store changed."""
|
||||
if not config.get('content', 'cookies-store'):
|
||||
self._lineparser.data = []
|
||||
self._lineparser.save()
|
||||
self.changed.emit()
|
||||
self._linecp.data = []
|
||||
self._linecp.save()
|
||||
|
||||
@@ -28,7 +28,8 @@ import functools
|
||||
import collections
|
||||
|
||||
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QObject, QTimer,
|
||||
Qt, QVariant, QAbstractListModel, QModelIndex, QUrl)
|
||||
QStandardPaths, Qt, QVariant, QAbstractListModel,
|
||||
QModelIndex, QUrl)
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
|
||||
# We need this import so PyQt can use it inside pyqtSlot
|
||||
@@ -49,32 +50,6 @@ ModelRole = usertypes.enum('ModelRole', ['item'], start=Qt.UserRole,
|
||||
RetryInfo = collections.namedtuple('RetryInfo', ['request', 'manager'])
|
||||
|
||||
|
||||
def _download_dir():
|
||||
"""Get the download directory to use."""
|
||||
directory = config.get('storage', 'download-directory')
|
||||
if directory is None:
|
||||
directory = standarddir.download()
|
||||
return directory
|
||||
|
||||
|
||||
def _path_suggestion(filename):
|
||||
"""Get the suggested file path.
|
||||
|
||||
Args:
|
||||
filename: The filename to use if included in the suggestion.
|
||||
"""
|
||||
suggestion = config.get('completion', 'download-path-suggestion')
|
||||
if suggestion == 'path':
|
||||
# add trailing '/' if not present
|
||||
return os.path.join(_download_dir(), '')
|
||||
elif suggestion == 'filename':
|
||||
return filename
|
||||
elif suggestion == 'both':
|
||||
return os.path.join(_download_dir(), filename)
|
||||
else:
|
||||
raise ValueError("Invalid suggestion value {}!".format(suggestion))
|
||||
|
||||
|
||||
class DownloadItemStats(QObject):
|
||||
|
||||
"""Statistics (bytes done, total bytes, time, etc.) about a download.
|
||||
@@ -148,7 +123,7 @@ class DownloadItemStats(QObject):
|
||||
|
||||
@pyqtSlot(int, int)
|
||||
def on_download_progress(self, bytes_done, bytes_total):
|
||||
"""Update local variables when the download progress changed.
|
||||
"""Upload local variables when the download progress changed.
|
||||
|
||||
Args:
|
||||
bytes_done: How many bytes are downloaded.
|
||||
@@ -158,6 +133,7 @@ class DownloadItemStats(QObject):
|
||||
bytes_total = None
|
||||
self.done = bytes_done
|
||||
self.total = bytes_total
|
||||
self.updated.emit()
|
||||
|
||||
|
||||
class DownloadItem(QObject):
|
||||
@@ -183,33 +159,32 @@ class DownloadItem(QObject):
|
||||
Attributes:
|
||||
done: Whether the download is finished.
|
||||
stats: A DownloadItemStats object.
|
||||
index: The index of the download in the view.
|
||||
successful: Whether the download has completed successfully.
|
||||
successful: Whether the download has completed sucessfully.
|
||||
error_msg: The current error message, or None
|
||||
autoclose: Whether to close the associated file if the download is
|
||||
done.
|
||||
fileobj: The file object to download the file to.
|
||||
reply: The QNetworkReply associated with this download.
|
||||
retry_info: A RetryInfo instance.
|
||||
_filename: The filename of the download.
|
||||
_redirects: How many time we were redirected already.
|
||||
_buffer: A BytesIO object to buffer incoming data until we know the
|
||||
target file.
|
||||
_read_timer: A Timer which reads the QNetworkReply into self._buffer
|
||||
_read_timer: A QTimer which reads the QNetworkReply into self._buffer
|
||||
periodically.
|
||||
_retry_info: A RetryInfo instance.
|
||||
_win_id: The window ID the DownloadItem runs in.
|
||||
|
||||
Signals:
|
||||
data_changed: The downloads metadata changed.
|
||||
finished: The download was finished.
|
||||
cancelled: The download was cancelled.
|
||||
error: An error with the download occurred.
|
||||
error: An error with the download occured.
|
||||
arg: The error message as string.
|
||||
redirected: Signal emitted when a download was redirected.
|
||||
arg 0: The new QNetworkRequest.
|
||||
arg 1: The old QNetworkReply.
|
||||
do_retry: Emitted when a download is retried.
|
||||
arg 0: The new DownloadItem
|
||||
do_retry: Emitted when a request should be re-tried.
|
||||
arg: The QNetworkRequest to download.
|
||||
"""
|
||||
|
||||
MAX_REDIRECTS = 10
|
||||
@@ -218,7 +193,7 @@ class DownloadItem(QObject):
|
||||
error = pyqtSignal(str)
|
||||
cancelled = pyqtSignal()
|
||||
redirected = pyqtSignal(QNetworkRequest, QNetworkReply)
|
||||
do_retry = pyqtSignal(object) # DownloadItem
|
||||
do_retry = pyqtSignal('QNetworkReply')
|
||||
|
||||
def __init__(self, reply, win_id, parent=None):
|
||||
"""Constructor.
|
||||
@@ -227,15 +202,14 @@ class DownloadItem(QObject):
|
||||
reply: The QNetworkReply to download.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.retry_info = None
|
||||
self._retry_info = None
|
||||
self.done = False
|
||||
self.stats = DownloadItemStats(self)
|
||||
self.stats.updated.connect(self.data_changed)
|
||||
self.index = 0
|
||||
self.autoclose = True
|
||||
self.reply = None
|
||||
self._buffer = io.BytesIO()
|
||||
self._read_timer = usertypes.Timer(self, name='download-read-timer')
|
||||
self._read_timer = QTimer()
|
||||
self._read_timer.setInterval(500)
|
||||
self._read_timer.timeout.connect(self.on_read_timer_timeout)
|
||||
self._redirects = 0
|
||||
@@ -264,9 +238,8 @@ class DownloadItem(QObject):
|
||||
else:
|
||||
errmsg = " - {}".format(self.error_msg)
|
||||
if all(e is None for e in (perc, remaining, self.stats.total)):
|
||||
return ('{index}: {name} [{speed:>10}|{down}]{errmsg}'.format(
|
||||
index=self.index, name=self.basename, speed=speed,
|
||||
down=down, errmsg=errmsg))
|
||||
return ('{name} [{speed:>10}|{down}]{errmsg}'.format(
|
||||
name=self.basename, speed=speed, down=down, errmsg=errmsg))
|
||||
if perc is None:
|
||||
perc = '??'
|
||||
else:
|
||||
@@ -277,18 +250,17 @@ class DownloadItem(QObject):
|
||||
remaining = utils.format_seconds(remaining)
|
||||
total = utils.format_size(self.stats.total, suffix='B')
|
||||
if self.done:
|
||||
return ('{index}: {name} [{perc:>2}%|{total}]{errmsg}'.format(
|
||||
index=self.index, name=self.basename, perc=perc,
|
||||
total=total, errmsg=errmsg))
|
||||
return ('{name} [{perc:>2}%|{total}]{errmsg}'.format(
|
||||
name=self.basename, perc=perc, total=total,
|
||||
errmsg=errmsg))
|
||||
else:
|
||||
return ('{index}: {name} [{speed:>10}|{remaining:>5}|{perc:>2}%|'
|
||||
return ('{name} [{speed:>10}|{remaining:>5}|{perc:>2}%|'
|
||||
'{down}/{total}]{errmsg}'.format(
|
||||
index=self.index, name=self.basename, speed=speed,
|
||||
remaining=remaining, perc=perc, down=down,
|
||||
total=total, errmsg=errmsg))
|
||||
name=self.basename, speed=speed, remaining=remaining,
|
||||
perc=perc, down=down, total=total, errmsg=errmsg))
|
||||
|
||||
def _create_fileobj(self):
|
||||
"""Create a file object using the internal filename."""
|
||||
"""Creates a file object using the internal filename."""
|
||||
try:
|
||||
fileobj = open(self._filename, 'wb')
|
||||
except OSError as e:
|
||||
@@ -304,8 +276,6 @@ class DownloadItem(QObject):
|
||||
q.answered_yes.connect(self._create_fileobj)
|
||||
q.answered_no.connect(functools.partial(self.cancel, False))
|
||||
q.cancelled.connect(functools.partial(self.cancel, False))
|
||||
self.cancelled.connect(q.abort)
|
||||
self.error.connect(q.abort)
|
||||
message_bridge = objreg.get('message-bridge', scope='window',
|
||||
window=self._win_id)
|
||||
message_bridge.ask(q, blocking=False)
|
||||
@@ -321,11 +291,7 @@ class DownloadItem(QObject):
|
||||
self.error_msg = msg
|
||||
self.stats.finish()
|
||||
self.error.emit(msg)
|
||||
with log.hide_qt_warning('QNetworkReplyImplPrivate::error: Internal '
|
||||
'problem, this method must only be called '
|
||||
'once.'):
|
||||
# See https://codereview.qt-project.org/#/c/107863/
|
||||
self.reply.abort()
|
||||
self.reply.abort()
|
||||
self.reply.deleteLater()
|
||||
self.reply = None
|
||||
self.done = True
|
||||
@@ -345,8 +311,8 @@ class DownloadItem(QObject):
|
||||
reply.finished.connect(self.on_reply_finished)
|
||||
reply.error.connect(self.on_reply_error)
|
||||
reply.readyRead.connect(self.on_ready_read)
|
||||
self.retry_info = RetryInfo(request=reply.request(),
|
||||
manager=reply.manager())
|
||||
self._retry_info = RetryInfo(request=reply.request(),
|
||||
manager=reply.manager())
|
||||
if not self.fileobj:
|
||||
self._read_timer.start()
|
||||
# We could have got signals before we connected slots to them.
|
||||
@@ -355,19 +321,12 @@ class DownloadItem(QObject):
|
||||
if reply.error() != QNetworkReply.NoError:
|
||||
QTimer.singleShot(0, lambda: self.error.emit(reply.errorString()))
|
||||
|
||||
def get_status_color(self, position):
|
||||
"""Choose an appropriate color for presenting the download's status.
|
||||
|
||||
Args:
|
||||
position: The color type requested, can be 'fg' or 'bg'.
|
||||
"""
|
||||
# pylint: disable=bad-config-call
|
||||
# WORKAROUND for https://bitbucket.org/logilab/astroid/issue/104/
|
||||
assert position in ("fg", "bg")
|
||||
start = config.get('colors', 'downloads.{}.start'.format(position))
|
||||
stop = config.get('colors', 'downloads.{}.stop'.format(position))
|
||||
system = config.get('colors', 'downloads.{}.system'.format(position))
|
||||
error = config.get('colors', 'downloads.{}.error'.format(position))
|
||||
def bg_color(self):
|
||||
"""Background color to be shown."""
|
||||
start = config.get('colors', 'downloads.bg.start')
|
||||
stop = config.get('colors', 'downloads.bg.stop')
|
||||
system = config.get('colors', 'downloads.bg.system')
|
||||
error = config.get('colors', 'downloads.bg.error')
|
||||
if self.error_msg is not None:
|
||||
assert not self.successful
|
||||
return error
|
||||
@@ -377,7 +336,6 @@ class DownloadItem(QObject):
|
||||
return utils.interpolate_color(
|
||||
start, stop, self.stats.percentage(), system)
|
||||
|
||||
@pyqtSlot()
|
||||
def cancel(self, remove_data=True):
|
||||
"""Cancel the download.
|
||||
|
||||
@@ -394,32 +352,22 @@ class DownloadItem(QObject):
|
||||
self.reply = None
|
||||
if self.fileobj is not None:
|
||||
self.fileobj.close()
|
||||
if remove_data:
|
||||
self.delete()
|
||||
try:
|
||||
if (self._filename is not None and os.path.exists(self._filename)
|
||||
and remove_data):
|
||||
os.remove(self._filename)
|
||||
except OSError:
|
||||
log.downloads.exception("Failed to remove partial file")
|
||||
self.done = True
|
||||
self.finished.emit()
|
||||
self.data_changed.emit()
|
||||
|
||||
def delete(self):
|
||||
"""Delete the downloaded file."""
|
||||
try:
|
||||
if self._filename is not None and os.path.exists(self._filename):
|
||||
os.remove(self._filename)
|
||||
except OSError:
|
||||
log.downloads.exception("Failed to remove partial file")
|
||||
|
||||
@pyqtSlot()
|
||||
def retry(self):
|
||||
"""Retry a failed download."""
|
||||
download_manager = objreg.get('download-manager', scope='window',
|
||||
window=self._win_id)
|
||||
new_reply = self.retry_info.manager.get(self.retry_info.request)
|
||||
new_download = download_manager.fetch(
|
||||
new_reply, suggested_filename=self.basename)
|
||||
self.do_retry.emit(new_download)
|
||||
self.cancel()
|
||||
new_reply = self._retry_info.manager.get(self._retry_info.request)
|
||||
self.do_retry.emit(new_reply)
|
||||
|
||||
@pyqtSlot()
|
||||
def open_file(self):
|
||||
"""Open the downloaded file."""
|
||||
assert self.successful
|
||||
@@ -442,12 +390,24 @@ class DownloadItem(QObject):
|
||||
# See https://github.com/The-Compiler/qutebrowser/issues/427
|
||||
encoding = sys.getfilesystemencoding()
|
||||
filename = utils.force_encoding(filename, encoding)
|
||||
if not self._create_full_filename(filename):
|
||||
# We only got a filename (without directory) or a relative path
|
||||
# from the user, so we append that to the default directory and
|
||||
# try again.
|
||||
self._create_full_filename(os.path.join(_download_dir(), filename))
|
||||
|
||||
if os.path.isabs(filename) and os.path.isdir(filename):
|
||||
# We got an absolute directory from the user, so we save it under
|
||||
# the default filename in that directory.
|
||||
self._filename = os.path.join(filename, self.basename)
|
||||
elif os.path.isabs(filename):
|
||||
# We got an absolute filename from the user, so we save it under
|
||||
# that filename.
|
||||
self._filename = filename
|
||||
self.basename = os.path.basename(self._filename)
|
||||
else:
|
||||
# We only got a filename (without directory) from the user, so we
|
||||
# save it under that filename in the default directory.
|
||||
download_dir = config.get('storage', 'download-directory')
|
||||
if download_dir is None:
|
||||
download_dir = standarddir.get(
|
||||
QStandardPaths.DownloadLocation)
|
||||
self._filename = os.path.join(download_dir, filename)
|
||||
self.basename = filename
|
||||
log.downloads.debug("Setting filename to {}".format(filename))
|
||||
if os.path.isfile(self._filename):
|
||||
# The file already exists, so ask the user if it should be
|
||||
@@ -456,25 +416,6 @@ class DownloadItem(QObject):
|
||||
else:
|
||||
self._create_fileobj()
|
||||
|
||||
def _create_full_filename(self, filename):
|
||||
"""Try to create the full filename.
|
||||
|
||||
Return:
|
||||
True if the full filename was created, False otherwise.
|
||||
"""
|
||||
if os.path.isabs(filename) and os.path.isdir(filename):
|
||||
# We got an absolute directory from the user, so we save it under
|
||||
# the default filename in that directory.
|
||||
self._filename = os.path.join(filename, self.basename)
|
||||
return True
|
||||
elif os.path.isabs(filename):
|
||||
# We got an absolute filename from the user, so we save it under
|
||||
# that filename.
|
||||
self._filename = filename
|
||||
self.basename = os.path.basename(self._filename)
|
||||
return True
|
||||
return False
|
||||
|
||||
def set_fileobj(self, fileobj):
|
||||
""""Set the file object to write the download to.
|
||||
|
||||
@@ -637,6 +578,18 @@ class DownloadManager(QAbstractListModel):
|
||||
self.questions.append(q)
|
||||
return q
|
||||
|
||||
@cmdutils.register(instance='download-manager', scope='window')
|
||||
def download(self, url, dest=None):
|
||||
"""Download a given URL, given as string.
|
||||
|
||||
Args:
|
||||
url: The URL to download
|
||||
dest: The file path to write the download to, or None to ask.
|
||||
"""
|
||||
url = urlutils.qurl_from_user_input(url)
|
||||
urlutils.raise_cmdexc_if_invalid(url)
|
||||
self.get(url, filename=dest)
|
||||
|
||||
@pyqtSlot('QUrl', 'QWebPage')
|
||||
def get(self, url, page=None, fileobj=None, filename=None,
|
||||
auto_remove=False):
|
||||
@@ -685,31 +638,27 @@ class DownloadManager(QAbstractListModel):
|
||||
if fileobj is not None and filename is not None:
|
||||
raise TypeError("Only one of fileobj/filename may be given!")
|
||||
# WORKAROUND for Qt corrupting data loaded from cache:
|
||||
# https://bugreports.qt.io/browse/QTBUG-42757
|
||||
# https://bugreports.qt-project.org/browse/QTBUG-42757
|
||||
request.setAttribute(QNetworkRequest.CacheLoadControlAttribute,
|
||||
QNetworkRequest.AlwaysNetwork)
|
||||
suggested_fn = urlutils.filename_from_url(request.url())
|
||||
if fileobj is not None or filename is not None:
|
||||
return self.fetch_request(request, page, fileobj, filename,
|
||||
auto_remove, suggested_fn)
|
||||
if suggested_fn is None:
|
||||
suggested_fn = 'qutebrowser-download'
|
||||
else:
|
||||
encoding = sys.getfilesystemencoding()
|
||||
suggested_fn = utils.force_encoding(suggested_fn, encoding)
|
||||
return self.fetch_request(request, filename, fileobj, page,
|
||||
auto_remove)
|
||||
q = self._prepare_question()
|
||||
q.default = _path_suggestion(suggested_fn)
|
||||
filename = urlutils.filename_from_url(request.url())
|
||||
encoding = sys.getfilesystemencoding()
|
||||
filename = utils.force_encoding(filename, encoding)
|
||||
q.default = filename
|
||||
message_bridge = objreg.get('message-bridge', scope='window',
|
||||
window=self._win_id)
|
||||
q.answered.connect(
|
||||
lambda fn: self.fetch_request(request, page, filename=fn,
|
||||
auto_remove=auto_remove,
|
||||
suggested_filename=suggested_fn))
|
||||
lambda fn: self.fetch_request(request, filename=fn, page=page,
|
||||
auto_remove=auto_remove))
|
||||
message_bridge.ask(q, blocking=False)
|
||||
return None
|
||||
|
||||
def fetch_request(self, request, page=None, fileobj=None, filename=None,
|
||||
auto_remove=False, suggested_filename=None):
|
||||
auto_remove=False):
|
||||
"""Download a QNetworkRequest to disk.
|
||||
|
||||
Args:
|
||||
@@ -728,12 +677,10 @@ class DownloadManager(QAbstractListModel):
|
||||
else:
|
||||
nam = page.networkAccessManager()
|
||||
reply = nam.get(request)
|
||||
return self.fetch(reply, fileobj, filename, auto_remove,
|
||||
suggested_filename)
|
||||
return self.fetch(reply, fileobj, filename, auto_remove)
|
||||
|
||||
@pyqtSlot('QNetworkReply')
|
||||
def fetch(self, reply, fileobj=None, filename=None, auto_remove=False,
|
||||
suggested_filename=None):
|
||||
def fetch(self, reply, fileobj=None, filename=None, auto_remove=False):
|
||||
"""Download a QNetworkReply to disk.
|
||||
|
||||
Args:
|
||||
@@ -748,13 +695,12 @@ class DownloadManager(QAbstractListModel):
|
||||
"""
|
||||
if fileobj is not None and filename is not None:
|
||||
raise TypeError("Only one of fileobj/filename may be given!")
|
||||
if not suggested_filename:
|
||||
if filename is not None:
|
||||
suggested_filename = os.path.basename(filename)
|
||||
elif fileobj is not None and getattr(fileobj, 'name', None):
|
||||
suggested_filename = fileobj.name
|
||||
else:
|
||||
_, suggested_filename = http.parse_content_disposition(reply)
|
||||
if filename is not None:
|
||||
suggested_filename = os.path.basename(filename)
|
||||
elif fileobj is not None and getattr(fileobj, 'name', None):
|
||||
suggested_filename = fileobj.name
|
||||
else:
|
||||
_inline, suggested_filename = http.parse_content_disposition(reply)
|
||||
log.downloads.debug("fetch: {} -> {}".format(reply.url(),
|
||||
suggested_filename))
|
||||
download = DownloadItem(reply, self._win_id, self)
|
||||
@@ -768,9 +714,9 @@ class DownloadManager(QAbstractListModel):
|
||||
download.error.connect(self.on_error)
|
||||
download.redirected.connect(
|
||||
functools.partial(self.on_redirect, download))
|
||||
download.do_retry.connect(self.fetch)
|
||||
download.basename = suggested_filename
|
||||
idx = len(self.downloads) + 1
|
||||
download.index = idx
|
||||
self.beginInsertRows(QModelIndex(), idx, idx)
|
||||
self.downloads.append(download)
|
||||
self.endInsertRows()
|
||||
@@ -782,7 +728,10 @@ class DownloadManager(QAbstractListModel):
|
||||
download.autoclose = False
|
||||
else:
|
||||
q = self._prepare_question()
|
||||
q.default = _path_suggestion(suggested_filename)
|
||||
encoding = sys.getfilesystemencoding()
|
||||
suggested_filename = utils.force_encoding(suggested_filename,
|
||||
encoding)
|
||||
q.default = suggested_filename
|
||||
q.answered.connect(download.set_filename)
|
||||
q.cancelled.connect(download.cancel)
|
||||
download.cancelled.connect(q.abort)
|
||||
@@ -793,82 +742,20 @@ class DownloadManager(QAbstractListModel):
|
||||
|
||||
return download
|
||||
|
||||
def raise_no_download(self, count):
|
||||
"""Raise an exception that the download doesn't exist.
|
||||
|
||||
Args:
|
||||
count: The index of the download
|
||||
"""
|
||||
if not count:
|
||||
raise cmdexc.CommandError("There's no download!")
|
||||
raise cmdexc.CommandError("There's no download {}!".format(count))
|
||||
|
||||
@cmdutils.register(instance='download-manager', scope='window',
|
||||
count='count')
|
||||
def download_cancel(self, count=0):
|
||||
"""Cancel the last/[count]th download.
|
||||
|
||||
Args:
|
||||
count: The index of the download to cancel.
|
||||
"""
|
||||
try:
|
||||
download = self.downloads[count - 1]
|
||||
except IndexError:
|
||||
self.raise_no_download(count)
|
||||
if download.done:
|
||||
if not count:
|
||||
count = len(self.downloads)
|
||||
raise cmdexc.CommandError("Download {} is already done!"
|
||||
.format(count))
|
||||
download.cancel()
|
||||
|
||||
@cmdutils.register(instance='download-manager', scope='window',
|
||||
count='count')
|
||||
def download_delete(self, count=0):
|
||||
"""Delete the last/[count]th download from disk.
|
||||
|
||||
Args:
|
||||
count: The index of the download to cancel.
|
||||
"""
|
||||
try:
|
||||
download = self.downloads[count - 1]
|
||||
except IndexError:
|
||||
self.raise_no_download(count)
|
||||
if not download.successful:
|
||||
if not count:
|
||||
count = len(self.downloads)
|
||||
raise cmdexc.CommandError("Download {} is not done!".format(count))
|
||||
download.delete()
|
||||
self.remove_item(download)
|
||||
|
||||
@cmdutils.register(instance='download-manager', scope='window',
|
||||
deprecated="Use :download-cancel instead.",
|
||||
count='count')
|
||||
def cancel_download(self, count=1):
|
||||
@cmdutils.register(instance='download-manager', scope='window')
|
||||
def cancel_download(self, count: {'special': 'count'}=1):
|
||||
"""Cancel the first/[count]th download.
|
||||
|
||||
Args:
|
||||
count: The index of the download to cancel.
|
||||
"""
|
||||
self.download_cancel(count)
|
||||
|
||||
@cmdutils.register(instance='download-manager', scope='window',
|
||||
count='count')
|
||||
def download_open(self, count=0):
|
||||
"""Open the last/[count]th download.
|
||||
|
||||
Args:
|
||||
count: The index of the download to cancel.
|
||||
"""
|
||||
if count == 0:
|
||||
return
|
||||
try:
|
||||
download = self.downloads[count - 1]
|
||||
except IndexError:
|
||||
self.raise_no_download(count)
|
||||
if not download.successful:
|
||||
if not count:
|
||||
count = len(self.downloads)
|
||||
raise cmdexc.CommandError("Download {} is not done!".format(count))
|
||||
download.open_file()
|
||||
raise cmdexc.CommandError("There's no download {}!".format(count))
|
||||
download.cancel()
|
||||
|
||||
@pyqtSlot(QNetworkRequest, QNetworkReply)
|
||||
def on_redirect(self, download, request, reply):
|
||||
@@ -910,44 +797,21 @@ class DownloadManager(QAbstractListModel):
|
||||
Return:
|
||||
A boolean.
|
||||
"""
|
||||
assert nam.adopted_downloads == 0
|
||||
for download in self.downloads:
|
||||
running_download = (download.reply is not None and
|
||||
download.reply.manager() is nam)
|
||||
# user could request retry after tab is closed.
|
||||
failed_download = (download.done and (not download.successful) and
|
||||
download.retry_info.manager is nam)
|
||||
if running_download or failed_download:
|
||||
nam.adopt_download(download)
|
||||
return nam.adopted_downloads
|
||||
if download.reply is not None and download.reply.manager() is nam:
|
||||
return True
|
||||
return False
|
||||
|
||||
def can_clear(self):
|
||||
"""Check if there are finished downloads to clear."""
|
||||
return any(download.done for download in self.downloads)
|
||||
|
||||
@cmdutils.register(instance='download-manager', scope='window',
|
||||
count='count')
|
||||
def download_remove(self, all_=False, count=0):
|
||||
"""Remove the last/[count]th download from the list.
|
||||
|
||||
Args:
|
||||
all_: If given removes all finished downloads.
|
||||
count: The index of the download to cancel.
|
||||
"""
|
||||
if all_:
|
||||
finished_items = [d for d in self.downloads if d.done]
|
||||
self.remove_items(finished_items)
|
||||
if self.downloads:
|
||||
return any(download.done for download in self.downloads)
|
||||
else:
|
||||
try:
|
||||
download = self.downloads[count - 1]
|
||||
except IndexError:
|
||||
self.raise_no_download(count)
|
||||
if not download.done:
|
||||
if not count:
|
||||
count = len(self.downloads)
|
||||
raise cmdexc.CommandError("Download {} is not done!"
|
||||
.format(count))
|
||||
self.remove_item(download)
|
||||
return False
|
||||
|
||||
def clear(self):
|
||||
"""Remove all finished downloads."""
|
||||
self.remove_items(d for d in self.downloads if d.done)
|
||||
|
||||
def last_index(self):
|
||||
"""Get the last index in the model.
|
||||
@@ -969,7 +833,6 @@ class DownloadManager(QAbstractListModel):
|
||||
del self.downloads[idx]
|
||||
self.endRemoveRows()
|
||||
download.deleteLater()
|
||||
self.update_indexes()
|
||||
|
||||
def remove_items(self, downloads):
|
||||
"""Remove an iterable of downloads."""
|
||||
@@ -999,18 +862,6 @@ class DownloadManager(QAbstractListModel):
|
||||
download.deleteLater()
|
||||
self.endRemoveRows()
|
||||
|
||||
def update_indexes(self):
|
||||
"""Update indexes of all DownloadItems."""
|
||||
first_idx = None
|
||||
for i, d in enumerate(self.downloads, 1):
|
||||
if first_idx is None and d.index != i:
|
||||
first_idx = i - 1
|
||||
d.index = i
|
||||
if first_idx is not None:
|
||||
model_idx = self.index(first_idx, 0)
|
||||
qtutils.ensure_valid(model_idx)
|
||||
self.dataChanged.emit(model_idx, self.last_index())
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
"""Simple constant header."""
|
||||
if (section == 0 and orientation == Qt.Horizontal and
|
||||
@@ -1029,9 +880,9 @@ class DownloadManager(QAbstractListModel):
|
||||
if role == Qt.DisplayRole:
|
||||
data = str(item)
|
||||
elif role == Qt.ForegroundRole:
|
||||
data = item.get_status_color('fg')
|
||||
data = config.get('colors', 'downloads.fg')
|
||||
elif role == Qt.BackgroundRole:
|
||||
data = item.get_status_color('bg')
|
||||
data = item.bg_color()
|
||||
elif role == ModelRole.item:
|
||||
data = item
|
||||
elif role == Qt.ToolTipRole:
|
||||
@@ -1047,7 +898,7 @@ class DownloadManager(QAbstractListModel):
|
||||
"""Override flags so items aren't selectable.
|
||||
|
||||
The default would be Qt.ItemIsEnabled | Qt.ItemIsSelectable."""
|
||||
return Qt.ItemIsEnabled | Qt.ItemNeverHasChildren
|
||||
return Qt.ItemIsEnabled
|
||||
|
||||
def rowCount(self, parent=QModelIndex()):
|
||||
"""Get count of active downloads."""
|
||||
|
||||
@@ -31,7 +31,9 @@ from qutebrowser.utils import qtutils, utils, objreg
|
||||
|
||||
|
||||
def update_geometry(obj):
|
||||
"""Weird WORKAROUND for some weird PyQt bug (probably).
|
||||
"""WORKAROUND
|
||||
|
||||
This is a horrible workaround for some weird PyQt bug (probably).
|
||||
|
||||
This actually should be a method of DownloadView, but for some reason the
|
||||
rowsInserted/rowsRemoved signals don't get disconnected from this method
|
||||
@@ -42,6 +44,7 @@ def update_geometry(obj):
|
||||
Original bug: https://github.com/The-Compiler/qutebrowser/issues/167
|
||||
Workaround bug: https://github.com/The-Compiler/qutebrowser/issues/171
|
||||
"""
|
||||
|
||||
def _update_geometry():
|
||||
"""Actually update the geometry if the object still exists."""
|
||||
if sip.isdeleted(obj):
|
||||
@@ -123,7 +126,7 @@ class DownloadView(QListView):
|
||||
Return:
|
||||
A list of either:
|
||||
- (QAction, callable) tuples.
|
||||
- (None, None) for a separator
|
||||
- (None, None) for a seperator
|
||||
"""
|
||||
actions = []
|
||||
if item is None:
|
||||
@@ -139,8 +142,7 @@ class DownloadView(QListView):
|
||||
actions.append(("Cancel", item.cancel))
|
||||
if self.model().can_clear():
|
||||
actions.append((None, None))
|
||||
actions.append(("Remove all finished", functools.partial(
|
||||
self.model().download_remove, True)))
|
||||
actions.append(("Remove all finished", self.model().clear))
|
||||
return actions
|
||||
|
||||
@pyqtSlot('QPoint')
|
||||
|
||||
@@ -21,30 +21,28 @@
|
||||
|
||||
import math
|
||||
import functools
|
||||
import subprocess
|
||||
import collections
|
||||
|
||||
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl,
|
||||
QTimer)
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl
|
||||
from PyQt5.QtGui import QMouseEvent, QClipboard
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PyQt5.QtWebKit import QWebElement
|
||||
from PyQt5.QtWebKitWidgets import QWebPage
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.keyinput import modeman, modeparsers
|
||||
from qutebrowser.browser import webelem
|
||||
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
|
||||
from qutebrowser.utils import usertypes, log, qtutils, message, objreg
|
||||
from qutebrowser.misc import guiprocess
|
||||
|
||||
|
||||
ElemTuple = collections.namedtuple('ElemTuple', ['elem', 'label'])
|
||||
|
||||
|
||||
Target = usertypes.enum('Target', ['normal', 'tab', 'tab_fg', 'tab_bg',
|
||||
'window', 'yank', 'yank_primary', 'run',
|
||||
'fill', 'hover', 'rapid', 'rapid_win',
|
||||
'download', 'userscript', 'spawn'])
|
||||
Target = usertypes.enum('Target', ['normal', 'tab', 'tab_bg', 'window', 'yank',
|
||||
'yank_primary', 'run', 'fill', 'hover',
|
||||
'rapid', 'rapid_win', 'download',
|
||||
'userscript', 'spawn'])
|
||||
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
@@ -62,10 +60,10 @@ class HintContext:
|
||||
frames: The QWebFrames to use.
|
||||
destroyed_frames: id()'s of QWebFrames which have been destroyed.
|
||||
(Workaround for https://github.com/The-Compiler/qutebrowser/issues/152)
|
||||
elems: A mapping from key strings to (elem, label) namedtuples.
|
||||
elems: A mapping from keystrings to (elem, label) namedtuples.
|
||||
baseurl: The URL of the current page.
|
||||
target: What to do with the opened links.
|
||||
normal/tab/tab_fg/tab_bg/window: Get passed to BrowserTab.
|
||||
normal/tab/tab_bg/window: Get passed to BrowserTab.
|
||||
yank/yank_primary: Yank to clipboard/primary selection.
|
||||
run: Run a command.
|
||||
fill: Fill commandline with link.
|
||||
@@ -75,9 +73,6 @@ class HintContext:
|
||||
spawn: Spawn a simple command.
|
||||
to_follow: The link to follow when enter is pressed.
|
||||
args: Custom arguments for userscript/spawn
|
||||
rapid: Whether to do rapid hinting.
|
||||
mainframe: The main QWebFrame where we started hinting in.
|
||||
group: The group of web elements to hint.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
@@ -85,12 +80,9 @@ class HintContext:
|
||||
self.target = None
|
||||
self.baseurl = None
|
||||
self.to_follow = None
|
||||
self.rapid = False
|
||||
self.frames = []
|
||||
self.destroyed_frames = []
|
||||
self.args = []
|
||||
self.mainframe = None
|
||||
self.group = None
|
||||
|
||||
def get_args(self, urlstr):
|
||||
"""Get the arguments, with {hint-url} replaced by the given URL."""
|
||||
@@ -116,30 +108,28 @@ class HintManager(QObject):
|
||||
Signals:
|
||||
mouse_event: Mouse event to be posted in the web view.
|
||||
arg: A QMouseEvent
|
||||
start_hinting: Emitted when hinting starts, before a link is clicked.
|
||||
arg: The ClickTarget to use.
|
||||
stop_hinting: Emitted after a link was clicked.
|
||||
set_open_target: Set a new target to open the links in.
|
||||
"""
|
||||
|
||||
HINT_TEXTS = {
|
||||
Target.normal: "Follow hint",
|
||||
Target.tab: "Follow hint in new tab",
|
||||
Target.tab_fg: "Follow hint in foreground tab",
|
||||
Target.tab_bg: "Follow hint in background tab",
|
||||
Target.window: "Follow hint in new window",
|
||||
Target.yank: "Yank hint to clipboard",
|
||||
Target.yank_primary: "Yank hint to primary selection",
|
||||
Target.run: "Run a command on a hint",
|
||||
Target.fill: "Set hint in commandline",
|
||||
Target.hover: "Hover over a hint",
|
||||
Target.download: "Download hint",
|
||||
Target.userscript: "Call userscript via hint",
|
||||
Target.spawn: "Spawn command via hint",
|
||||
Target.normal: "Follow hint...",
|
||||
Target.tab: "Follow hint in new tab...",
|
||||
Target.tab_bg: "Follow hint in background tab...",
|
||||
Target.window: "Follow hint in new window...",
|
||||
Target.yank: "Yank hint to clipboard...",
|
||||
Target.yank_primary: "Yank hint to primary selection...",
|
||||
Target.run: "Run a command on a hint...",
|
||||
Target.fill: "Set hint in commandline...",
|
||||
Target.hover: "Hover over a hint...",
|
||||
Target.rapid: "Follow hint (rapid mode)...",
|
||||
Target.rapid_win: "Follow hint in new window (rapid mode)...",
|
||||
Target.download: "Download hint...",
|
||||
Target.userscript: "Call userscript via hint...",
|
||||
Target.spawn: "Spawn command via hint...",
|
||||
}
|
||||
|
||||
mouse_event = pyqtSignal('QMouseEvent')
|
||||
start_hinting = pyqtSignal(usertypes.ClickTarget)
|
||||
stop_hinting = pyqtSignal()
|
||||
set_open_target = pyqtSignal(str)
|
||||
|
||||
def __init__(self, win_id, tab_id, parent=None):
|
||||
"""Constructor."""
|
||||
@@ -151,14 +141,6 @@ class HintManager(QObject):
|
||||
window=win_id)
|
||||
mode_manager.left.connect(self.on_mode_left)
|
||||
|
||||
def _get_text(self):
|
||||
"""Get a hint text based on the current context."""
|
||||
text = self.HINT_TEXTS[self._context.target]
|
||||
if self._context.rapid:
|
||||
text += ' (rapid mode)'
|
||||
text += '...'
|
||||
return text
|
||||
|
||||
def _cleanup(self):
|
||||
"""Clean up after hinting."""
|
||||
for elem in self._context.elems.values():
|
||||
@@ -182,7 +164,7 @@ class HintManager(QObject):
|
||||
# See # https://github.com/The-Compiler/qutebrowser/issues/263
|
||||
pass
|
||||
log.hints.debug("Disconnected.")
|
||||
text = self._get_text()
|
||||
text = self.HINT_TEXTS[self._context.target]
|
||||
message_bridge = objreg.get('message-bridge', scope='window',
|
||||
window=self._win_id)
|
||||
message_bridge.maybe_reset_text(text)
|
||||
@@ -203,32 +185,14 @@ class HintManager(QObject):
|
||||
chars = '0123456789'
|
||||
else:
|
||||
chars = config.get('hints', 'chars')
|
||||
min_chars = config.get('hints', 'min-chars')
|
||||
if config.get('hints', 'scatter'):
|
||||
return self._hint_scattered(min_chars, chars, elems)
|
||||
else:
|
||||
return self._hint_linear(min_chars, chars, elems)
|
||||
|
||||
def _hint_scattered(self, min_chars, chars, elems):
|
||||
"""Produce scattered hint labels with variable length (like Vimium).
|
||||
|
||||
Args:
|
||||
min_chars: The minimum length of labels.
|
||||
chars: The alphabet to use for labels.
|
||||
elems: The elements to generate labels for.
|
||||
"""
|
||||
# Determine how many digits the link hints will require in the worst
|
||||
# case. Usually we do not need all of these digits for every link
|
||||
# single hint, so we can show shorter hints for a few of the links.
|
||||
needed = max(min_chars, math.ceil(math.log(len(elems), len(chars))))
|
||||
needed = math.ceil(math.log(len(elems), len(chars)))
|
||||
# Short hints are the number of hints we can possibly show which are
|
||||
# (needed - 1) digits in length.
|
||||
if needed > min_chars:
|
||||
short_count = math.floor((len(chars) ** needed - len(elems)) /
|
||||
len(chars))
|
||||
else:
|
||||
short_count = 0
|
||||
|
||||
short_count = math.floor((len(chars) ** needed - len(elems)) /
|
||||
len(chars))
|
||||
long_count = len(elems) - short_count
|
||||
|
||||
strings = []
|
||||
@@ -243,20 +207,6 @@ class HintManager(QObject):
|
||||
|
||||
return self._shuffle_hints(strings, len(chars))
|
||||
|
||||
def _hint_linear(self, min_chars, chars, elems):
|
||||
"""Produce linear hint labels with constant length (like dwb).
|
||||
|
||||
Args:
|
||||
min_chars: The minimum length of labels.
|
||||
chars: The alphabet to use for labels.
|
||||
elems: The elements to generate labels for.
|
||||
"""
|
||||
strings = []
|
||||
needed = max(min_chars, math.ceil(math.log(len(elems), len(chars))))
|
||||
for i in range(len(elems)):
|
||||
strings.append(self._number_to_hint_str(i, chars, needed))
|
||||
return strings
|
||||
|
||||
def _shuffle_hints(self, hints, length):
|
||||
"""Shuffle the given set of hints so that they're scattered.
|
||||
|
||||
@@ -334,7 +284,7 @@ class HintManager(QObject):
|
||||
"""
|
||||
attrs = [
|
||||
('display', 'inline !important'),
|
||||
('z-index', '{} !important'.format(int(2 ** 32 / 2 - 1))),
|
||||
('z-index', '100000 !important'),
|
||||
('pointer-events', 'none !important'),
|
||||
('position', 'absolute !important'),
|
||||
('color', config.get('colors', 'hints.fg') + ' !important'),
|
||||
@@ -402,11 +352,6 @@ class HintManager(QObject):
|
||||
label.setPlainText(string)
|
||||
return label
|
||||
|
||||
def _show_url_error(self):
|
||||
"""Show an error because no link was found."""
|
||||
message.error(self._win_id, "No suitable link found for this element.",
|
||||
immediately=True)
|
||||
|
||||
def _click(self, elem, context):
|
||||
"""Click an element.
|
||||
|
||||
@@ -414,57 +359,40 @@ class HintManager(QObject):
|
||||
elem: The QWebElement to click.
|
||||
context: The HintContext to use.
|
||||
"""
|
||||
target_mapping = {
|
||||
Target.rapid: usertypes.ClickTarget.tab_bg,
|
||||
Target.rapid_win: usertypes.ClickTarget.window,
|
||||
Target.normal: usertypes.ClickTarget.normal,
|
||||
Target.tab_fg: usertypes.ClickTarget.tab,
|
||||
Target.tab_bg: usertypes.ClickTarget.tab_bg,
|
||||
Target.window: usertypes.ClickTarget.window,
|
||||
Target.hover: usertypes.ClickTarget.normal,
|
||||
}
|
||||
if config.get('tabs', 'background-tabs'):
|
||||
target_mapping[Target.tab] = usertypes.ClickTarget.tab_bg
|
||||
if context.target == Target.rapid:
|
||||
target = Target.tab_bg
|
||||
elif context.target == Target.rapid_win:
|
||||
target = Target.window
|
||||
else:
|
||||
target_mapping[Target.tab] = usertypes.ClickTarget.tab
|
||||
target = context.target
|
||||
# FIXME Instead of clicking the center, we could have nicer heuristics.
|
||||
# e.g. parse (-webkit-)border-radius correctly and click text fields at
|
||||
# the bottom right, and everything else on the top left or so.
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/70
|
||||
pos = elem.rect_on_view().center()
|
||||
action = "Hovering" if context.target == Target.hover else "Clicking"
|
||||
action = "Hovering" if target == Target.hover else "Clicking"
|
||||
log.hints.debug("{} on '{}' at {}/{}".format(
|
||||
action, elem, pos.x(), pos.y()))
|
||||
self.start_hinting.emit(target_mapping[context.target])
|
||||
if context.target in [Target.tab, Target.tab_fg, Target.tab_bg,
|
||||
Target.window, Target.rapid, Target.rapid_win]:
|
||||
modifiers = Qt.ControlModifier
|
||||
else:
|
||||
modifiers = Qt.NoModifier
|
||||
events = [
|
||||
QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton,
|
||||
Qt.NoModifier),
|
||||
]
|
||||
if context.target != Target.hover:
|
||||
if target != Target.hover:
|
||||
self.set_open_target.emit(target.name)
|
||||
events += [
|
||||
QMouseEvent(QEvent.MouseButtonPress, pos, Qt.LeftButton,
|
||||
Qt.LeftButton, modifiers),
|
||||
Qt.NoButton, Qt.NoModifier),
|
||||
QMouseEvent(QEvent.MouseButtonRelease, pos, Qt.LeftButton,
|
||||
Qt.NoButton, modifiers),
|
||||
Qt.NoButton, Qt.NoModifier),
|
||||
]
|
||||
for evt in events:
|
||||
self.mouse_event.emit(evt)
|
||||
if elem.is_text_input() and elem.is_editable():
|
||||
QTimer.singleShot(0, functools.partial(
|
||||
elem.webFrame().page().triggerAction,
|
||||
QWebPage.MoveToEndOfDocument))
|
||||
QTimer.singleShot(0, self.stop_hinting.emit)
|
||||
|
||||
def _yank(self, url, context):
|
||||
"""Yank an element to the clipboard or primary selection.
|
||||
|
||||
Args:
|
||||
url: The URL to open as a QUrl.
|
||||
url: The URL to open as a QURL.
|
||||
context: The HintContext to use.
|
||||
"""
|
||||
sel = context.target == Target.yank_primary
|
||||
@@ -512,32 +440,24 @@ class HintManager(QObject):
|
||||
"""
|
||||
url = self._resolve_url(elem, context.baseurl)
|
||||
if url is None:
|
||||
self._show_url_error()
|
||||
message.error(self._win_id,
|
||||
"No suitable link found for this element.",
|
||||
immediately=True)
|
||||
return
|
||||
download_manager = objreg.get('download-manager', scope='window',
|
||||
window=self._win_id)
|
||||
download_manager.get(url, elem.webFrame().page())
|
||||
|
||||
def _call_userscript(self, elem, context):
|
||||
def _call_userscript(self, url, context):
|
||||
"""Call an userscript from a hint.
|
||||
|
||||
Args:
|
||||
elem: The QWebElement to use in the userscript.
|
||||
url: The URL to open as a QUrl.
|
||||
context: The HintContext to use.
|
||||
"""
|
||||
cmd = context.args[0]
|
||||
args = context.args[1:]
|
||||
frame = context.mainframe
|
||||
env = {
|
||||
'QUTE_MODE': 'hints',
|
||||
'QUTE_SELECTED_TEXT': str(elem),
|
||||
'QUTE_SELECTED_HTML': elem.toOuterXml(),
|
||||
}
|
||||
url = self._resolve_url(elem, context.baseurl)
|
||||
if url is not None:
|
||||
env['QUTE_URL'] = url.toString(QUrl.FullyEncoded)
|
||||
env.update(userscripts.store_source(frame))
|
||||
userscripts.run(cmd, *args, win_id=self._win_id, env=env)
|
||||
userscripts.run(cmd, *args, url=url, win_id=self._win_id)
|
||||
|
||||
def _spawn(self, url, context):
|
||||
"""Spawn a simple command from a hint.
|
||||
@@ -548,9 +468,11 @@ class HintManager(QObject):
|
||||
"""
|
||||
urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword)
|
||||
args = context.get_args(urlstr)
|
||||
cmd, *args = args
|
||||
proc = guiprocess.GUIProcess(self._win_id, what='command', parent=self)
|
||||
proc.start(cmd, args)
|
||||
try:
|
||||
subprocess.Popen(args)
|
||||
except OSError as e:
|
||||
msg = "Error while spawning command: {}".format(e)
|
||||
message.error(self._win_id, msg, immediately=True)
|
||||
|
||||
def _resolve_url(self, elem, baseurl):
|
||||
"""Resolve a URL and check if we want to keep it.
|
||||
@@ -640,17 +562,21 @@ class HintManager(QObject):
|
||||
raise cmdexc.CommandError(
|
||||
"'args' is only allowed with target userscript/spawn.")
|
||||
|
||||
def _init_elements(self):
|
||||
"""Initialize the elements and labels based on the context set."""
|
||||
def _init_elements(self, mainframe, group):
|
||||
"""Initialize the elements and labels based on the context set.
|
||||
|
||||
Args:
|
||||
mainframe: The main QWebFrame.
|
||||
group: A Group enum member (which elements to find).
|
||||
"""
|
||||
elems = []
|
||||
for f in self._context.frames:
|
||||
elems += f.findAllElements(webelem.SELECTORS[self._context.group])
|
||||
elems = [e for e in elems
|
||||
if webelem.is_visible(e, self._context.mainframe)]
|
||||
elems += f.findAllElements(webelem.SELECTORS[group])
|
||||
elems = [e for e in elems if webelem.is_visible(e, mainframe)]
|
||||
# We wrap the elements late for performance reasons, as wrapping 1000s
|
||||
# of elements (with ~50 methods each) just takes too much time...
|
||||
elems = [webelem.WebElementWrapper(e) for e in elems]
|
||||
filterfunc = webelem.FILTERS.get(self._context.group, lambda e: True)
|
||||
filterfunc = webelem.FILTERS.get(group, lambda e: True)
|
||||
elems = [e for e in elems if filterfunc(e)]
|
||||
if not elems:
|
||||
raise cmdexc.CommandError("No elements found.")
|
||||
@@ -675,7 +601,6 @@ class HintManager(QObject):
|
||||
background: True to open in a background tab.
|
||||
window: True to open in a new window, False for the current one.
|
||||
"""
|
||||
from qutebrowser.mainwindow import mainwindow
|
||||
elem = self._find_prevnext(frame, prev)
|
||||
if elem is None:
|
||||
raise cmdexc.CommandError("No {} links found!".format(
|
||||
@@ -686,10 +611,11 @@ class HintManager(QObject):
|
||||
"prev" if prev else "forward"))
|
||||
qtutils.ensure_valid(url)
|
||||
if window:
|
||||
new_window = mainwindow.MainWindow()
|
||||
new_window.show()
|
||||
main_window = objreg.get('main-window', scope='window',
|
||||
window=self._win_id)
|
||||
win_id = main_window.spawn()
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=new_window.win_id)
|
||||
window=win_id)
|
||||
tabbed_browser.tabopen(url, background=False)
|
||||
elif tab:
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
@@ -700,16 +626,12 @@ class HintManager(QObject):
|
||||
tab=self._tab_id)
|
||||
webview.openurl(url)
|
||||
|
||||
@cmdutils.register(instance='hintmanager', scope='tab', name='hint',
|
||||
win_id='win_id')
|
||||
def start(self, rapid=False, group=webelem.Group.all, target=Target.normal,
|
||||
*args: {'nargs': '*'}, win_id):
|
||||
@cmdutils.register(instance='hintmanager', scope='tab', name='hint')
|
||||
def start(self, group=webelem.Group.all, target=Target.normal,
|
||||
*args: {'nargs': '*'}):
|
||||
"""Start hinting.
|
||||
|
||||
Args:
|
||||
rapid: Whether to do rapid hinting. This is only possible with
|
||||
targets `tab` (with background-tabs=true), `tab-bg`,
|
||||
`window`, `run`, `hover`, `userscript` and `spawn`.
|
||||
group: The hinting mode to use.
|
||||
|
||||
- `all`: All clickable elements.
|
||||
@@ -719,9 +641,7 @@ class HintManager(QObject):
|
||||
target: What to do with the selected element.
|
||||
|
||||
- `normal`: Open the link in the current tab.
|
||||
- `tab`: Open the link in a new tab (honoring the
|
||||
background-tabs setting).
|
||||
- `tab-fg`: Open the link in a new foreground tab.
|
||||
- `tab`: Open the link in a new tab.
|
||||
- `tab-bg`: Open the link in a new background tab.
|
||||
- `window`: Open the link in a new window.
|
||||
- `hover` : Hover over the link.
|
||||
@@ -730,6 +650,9 @@ class HintManager(QObject):
|
||||
- `run`: Run the argument as command.
|
||||
- `fill`: Fill the commandline with the command given as
|
||||
argument.
|
||||
- `rapid`: Open the link in a new tab and stay in hinting mode.
|
||||
- `rapid-win`: Open the link in a new window and stay in
|
||||
hinting mode.
|
||||
- `download`: Download the link.
|
||||
- `userscript`: Call an userscript with `$QUTE_URL` set to the
|
||||
link.
|
||||
@@ -757,28 +680,11 @@ class HintManager(QObject):
|
||||
mode_manager = objreg.get('mode-manager', scope='window',
|
||||
window=self._win_id)
|
||||
if mode_manager.mode == usertypes.KeyMode.hint:
|
||||
modeman.leave(win_id, usertypes.KeyMode.hint, 're-hinting')
|
||||
|
||||
if rapid:
|
||||
if target in [Target.tab_bg, Target.window, Target.run,
|
||||
Target.hover, Target.userscript, Target.spawn]:
|
||||
pass
|
||||
elif (target == Target.tab and
|
||||
config.get('tabs', 'background-tabs')):
|
||||
pass
|
||||
else:
|
||||
name = target.name.replace('_', '-')
|
||||
raise cmdexc.CommandError("Rapid hinting makes no sense with "
|
||||
"target {}!".format(name))
|
||||
|
||||
raise cmdexc.CommandError("Already hinting!")
|
||||
self._check_args(target, *args)
|
||||
self._context = HintContext()
|
||||
self._context.target = target
|
||||
self._context.rapid = rapid
|
||||
try:
|
||||
self._context.baseurl = tabbed_browser.current_url()
|
||||
except qtutils.QtValueError:
|
||||
raise cmdexc.CommandError("No URL set for this page yet!")
|
||||
self._context.baseurl = tabbed_browser.current_url()
|
||||
self._context.frames = webelem.get_child_frames(mainframe)
|
||||
for frame in self._context.frames:
|
||||
# WORKAROUND for
|
||||
@@ -786,40 +692,14 @@ class HintManager(QObject):
|
||||
frame.destroyed.connect(functools.partial(
|
||||
self._context.destroyed_frames.append, id(frame)))
|
||||
self._context.args = args
|
||||
self._context.mainframe = mainframe
|
||||
self._context.group = group
|
||||
self._handle_old_rapid_targets(win_id)
|
||||
self._init_elements()
|
||||
self._init_elements(mainframe, group)
|
||||
message_bridge = objreg.get('message-bridge', scope='window',
|
||||
window=self._win_id)
|
||||
message_bridge.set_text(self._get_text())
|
||||
message_bridge.set_text(self.HINT_TEXTS[target])
|
||||
self._connect_frame_signals()
|
||||
modeman.enter(self._win_id, usertypes.KeyMode.hint,
|
||||
'HintManager.start')
|
||||
|
||||
def _handle_old_rapid_targets(self, win_id):
|
||||
"""Switch to the new way for rapid hinting with a rapid target.
|
||||
|
||||
Args:
|
||||
win_id: The window ID to display the warning in.
|
||||
|
||||
DEPRECATED.
|
||||
"""
|
||||
old_rapid_targets = {
|
||||
Target.rapid: Target.tab_bg,
|
||||
Target.rapid_win: Target.window,
|
||||
}
|
||||
target = self._context.target
|
||||
if target in old_rapid_targets:
|
||||
self._context.target = old_rapid_targets[target]
|
||||
self._context.rapid = True
|
||||
name = target.name.replace('_', '-')
|
||||
group_name = self._context.group.name.replace('_', '-')
|
||||
new_name = self._context.target.name.replace('_', '-')
|
||||
message.warning(
|
||||
win_id, ':hint with target {} is deprecated, use :hint '
|
||||
'--rapid {} {} instead!'.format(name, group_name, new_name))
|
||||
|
||||
def handle_partial_key(self, keystr):
|
||||
"""Handle a new partial keypress."""
|
||||
log.hints.debug("Handling new keystring: '{}'".format(keystr))
|
||||
@@ -833,7 +713,7 @@ class HintManager(QObject):
|
||||
'<font color="{}">{}</font>{}'.format(
|
||||
match_color, matched, rest))
|
||||
if self._is_hidden(elems.label):
|
||||
# hidden element which matches again -> show it
|
||||
# hidden element which matches again -> unhide it
|
||||
self._show_elem(elems.label)
|
||||
else:
|
||||
# element doesn't match anymore -> hide it
|
||||
@@ -852,7 +732,7 @@ class HintManager(QObject):
|
||||
if (filterstr is None or
|
||||
str(elems.elem).lower().startswith(filterstr)):
|
||||
if self._is_hidden(elems.label):
|
||||
# hidden element which matches again -> show it
|
||||
# hidden element which matches again -> unhide it
|
||||
self._show_elem(elems.label)
|
||||
else:
|
||||
# element doesn't match anymore -> hide it
|
||||
@@ -861,11 +741,8 @@ class HintManager(QObject):
|
||||
pass
|
||||
visible = {}
|
||||
for k, e in self._context.elems.items():
|
||||
try:
|
||||
if not self._is_hidden(e.label):
|
||||
visible[k] = e
|
||||
except webelem.IsNullError:
|
||||
pass
|
||||
if not self._is_hidden(e.label):
|
||||
visible[k] = e
|
||||
if not visible:
|
||||
# Whoops, filtered all hints
|
||||
modeman.leave(self._win_id, usertypes.KeyMode.hint, 'all filtered')
|
||||
@@ -888,13 +765,13 @@ class HintManager(QObject):
|
||||
elem_handlers = {
|
||||
Target.normal: self._click,
|
||||
Target.tab: self._click,
|
||||
Target.tab_fg: self._click,
|
||||
Target.tab_bg: self._click,
|
||||
Target.window: self._click,
|
||||
Target.rapid: self._click,
|
||||
Target.rapid_win: self._click,
|
||||
Target.hover: self._click,
|
||||
# _download needs a QWebElement to get the frame.
|
||||
Target.download: self._download,
|
||||
Target.userscript: self._call_userscript,
|
||||
}
|
||||
# Handlers which take a QUrl
|
||||
url_handlers = {
|
||||
@@ -902,26 +779,25 @@ class HintManager(QObject):
|
||||
Target.yank_primary: self._yank,
|
||||
Target.run: self._run_cmd,
|
||||
Target.fill: self._preset_cmd_text,
|
||||
Target.userscript: self._call_userscript,
|
||||
Target.spawn: self._spawn,
|
||||
}
|
||||
elem = self._context.elems[keystr].elem
|
||||
if elem.webFrame() is None:
|
||||
message.error(self._win_id, "This element has no webframe.",
|
||||
immediately=True)
|
||||
return
|
||||
if self._context.target in elem_handlers:
|
||||
handler = functools.partial(
|
||||
elem_handlers[self._context.target], elem, self._context)
|
||||
elif self._context.target in url_handlers:
|
||||
url = self._resolve_url(elem, self._context.baseurl)
|
||||
if url is None:
|
||||
self._show_url_error()
|
||||
message.error(self._win_id,
|
||||
"No suitable link found for this element.",
|
||||
immediately=True)
|
||||
return
|
||||
handler = functools.partial(
|
||||
url_handlers[self._context.target], url, self._context)
|
||||
else:
|
||||
raise ValueError("No suitable handler found!")
|
||||
if not self._context.rapid:
|
||||
if self._context.target not in (Target.rapid, Target.rapid_win):
|
||||
modeman.maybe_leave(self._win_id, usertypes.KeyMode.hint,
|
||||
'followed')
|
||||
else:
|
||||
|
||||
@@ -1,216 +0,0 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Simple history which gets written to disk."""
|
||||
|
||||
import time
|
||||
import collections
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, QUrl
|
||||
from PyQt5.QtWebKit import QWebHistoryInterface
|
||||
|
||||
from qutebrowser.utils import utils, objreg, standarddir, log
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.misc import lineparser
|
||||
|
||||
|
||||
class HistoryEntry:
|
||||
|
||||
"""A single entry in the web history.
|
||||
|
||||
Attributes:
|
||||
atime: The time the page was accessed.
|
||||
url: The URL which was accessed as QUrl.
|
||||
url_string: The URL which was accessed as string.
|
||||
"""
|
||||
|
||||
def __init__(self, atime, url):
|
||||
self.atime = float(atime)
|
||||
self.url = QUrl(url)
|
||||
self.url_string = url
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, constructor=True, atime=self.atime,
|
||||
url=self.url.toDisplayString())
|
||||
|
||||
def __str__(self):
|
||||
return '{} {}'.format(int(self.atime), self.url_string)
|
||||
|
||||
@classmethod
|
||||
def from_str(cls, s):
|
||||
"""Get a history based on a 'TIME URL' string."""
|
||||
return cls(*s.split(' ', maxsplit=1))
|
||||
|
||||
|
||||
class WebHistory(QWebHistoryInterface):
|
||||
|
||||
"""A QWebHistoryInterface which supports being written to disk.
|
||||
|
||||
Attributes:
|
||||
_lineparser: The AppendLineParser used to save the history.
|
||||
_history_dict: An OrderedDict of URLs read from the on-disk history.
|
||||
_new_history: A list of HistoryEntry items of the current session.
|
||||
_saved_count: How many HistoryEntries have been written to disk.
|
||||
_initial_read_started: Whether async_read was called.
|
||||
_initial_read_done: Whether async_read has completed.
|
||||
_temp_history: OrderedDict of temporary history entries before
|
||||
async_read was called.
|
||||
|
||||
Signals:
|
||||
add_completion_item: Emitted before a new HistoryEntry is added.
|
||||
arg: The new HistoryEntry.
|
||||
item_added: Emitted after a new HistoryEntry is added.
|
||||
arg: The new HistoryEntry.
|
||||
"""
|
||||
|
||||
add_completion_item = pyqtSignal(HistoryEntry)
|
||||
item_added = pyqtSignal(HistoryEntry)
|
||||
async_read_done = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._initial_read_started = False
|
||||
self._initial_read_done = False
|
||||
self._lineparser = lineparser.AppendLineParser(
|
||||
standarddir.data(), 'history', parent=self)
|
||||
self._history_dict = collections.OrderedDict()
|
||||
self._temp_history = collections.OrderedDict()
|
||||
self._new_history = []
|
||||
self._saved_count = 0
|
||||
objreg.get('save-manager').add_saveable(
|
||||
'history', self.save, self.item_added)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, length=len(self))
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._new_history[key]
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._history_dict.values())
|
||||
|
||||
def __len__(self):
|
||||
return len(self._history_dict)
|
||||
|
||||
def async_read(self):
|
||||
"""Read the initial history."""
|
||||
if self._initial_read_started:
|
||||
log.init.debug("Ignoring async_read() because reading is started.")
|
||||
return
|
||||
self._initial_read_started = True
|
||||
|
||||
if standarddir.data() is None:
|
||||
self._initial_read_done = True
|
||||
self.async_read_done.emit()
|
||||
return
|
||||
|
||||
with self._lineparser.open():
|
||||
for line in self._lineparser:
|
||||
yield
|
||||
data = line.rstrip().split(maxsplit=1)
|
||||
if not data:
|
||||
# empty line
|
||||
continue
|
||||
elif len(data) != 2:
|
||||
# other malformed line
|
||||
log.init.warning("Invalid history entry {!r}!".format(
|
||||
line))
|
||||
continue
|
||||
atime, url = data
|
||||
if atime.startswith('\0'):
|
||||
log.init.warning(
|
||||
"Removing NUL bytes from entry {!r} - see "
|
||||
"https://github.com/The-Compiler/qutebrowser/issues/"
|
||||
"670".format(data))
|
||||
atime = atime.lstrip('\0')
|
||||
# This de-duplicates history entries; only the latest
|
||||
# entry for each URL is kept. If you want to keep
|
||||
# information about previous hits change the items in
|
||||
# old_urls to be lists or change HistoryEntry to have a
|
||||
# list of atimes.
|
||||
entry = HistoryEntry(atime, url)
|
||||
self._add_entry(entry)
|
||||
|
||||
self._initial_read_done = True
|
||||
self.async_read_done.emit()
|
||||
|
||||
for url, entry in self._temp_history.items():
|
||||
self._new_history.append(entry)
|
||||
self._add_entry(entry)
|
||||
self.add_completion_item.emit(entry)
|
||||
|
||||
def _add_entry(self, entry, target=None):
|
||||
"""Add an entry to self._history_dict or another given OrderedDict."""
|
||||
if target is None:
|
||||
target = self._history_dict
|
||||
target[entry.url_string] = entry
|
||||
target.move_to_end(entry.url_string)
|
||||
|
||||
def get_recent(self):
|
||||
"""Get the most recent history entries."""
|
||||
old = self._lineparser.get_recent()
|
||||
return old + [str(e) for e in self._new_history]
|
||||
|
||||
def save(self):
|
||||
"""Save the history to disk."""
|
||||
new = (str(e) for e in self._new_history[self._saved_count:])
|
||||
self._lineparser.new_data = new
|
||||
self._lineparser.save()
|
||||
self._saved_count = len(self._new_history)
|
||||
|
||||
def addHistoryEntry(self, url_string):
|
||||
"""Called by WebKit when an URL should be added to the history.
|
||||
|
||||
Args:
|
||||
url_string: An url as string to add to the history.
|
||||
"""
|
||||
if not url_string:
|
||||
return
|
||||
if config.get('general', 'private-browsing'):
|
||||
return
|
||||
entry = HistoryEntry(time.time(), url_string)
|
||||
if self._initial_read_done:
|
||||
self.add_completion_item.emit(entry)
|
||||
self._new_history.append(entry)
|
||||
self._add_entry(entry)
|
||||
self.item_added.emit(entry)
|
||||
else:
|
||||
self._add_entry(entry, target=self._temp_history)
|
||||
|
||||
def historyContains(self, url_string):
|
||||
"""Called by WebKit to determine if an URL is contained in the history.
|
||||
|
||||
Args:
|
||||
url_string: The URL (as string) to check for.
|
||||
|
||||
Return:
|
||||
True if the url is in the history, False otherwise.
|
||||
"""
|
||||
return url_string in self._history_dict
|
||||
|
||||
|
||||
def init(parent=None):
|
||||
"""Initialize the web history.
|
||||
|
||||
Args:
|
||||
parent: The parent to use for WebHistory.
|
||||
"""
|
||||
history = WebHistory(parent)
|
||||
objreg.register('web-history', history)
|
||||
QWebHistoryInterface.setDefaultInterface(history)
|
||||
@@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Other utilities which don't fit anywhere else."""
|
||||
"""Other utilities which don't fit anywhere else. """
|
||||
|
||||
|
||||
import os.path
|
||||
@@ -50,7 +50,7 @@ def parse_content_disposition(reply):
|
||||
bytes(reply.rawHeader(content_disposition_header)))
|
||||
filename = content_disposition.filename()
|
||||
except UnicodeDecodeError:
|
||||
log.rfc6266.exception("Error while decoding filename")
|
||||
log.misc.exception("Error while decoding filename")
|
||||
else:
|
||||
is_inline = content_disposition.is_inline()
|
||||
# Then try to get filename from url
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Customized QWebInspector."""
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
|
||||
from PyQt5.QtWebKitWidgets import QWebInspector
|
||||
|
||||
from qutebrowser.utils import log, objreg
|
||||
|
||||
|
||||
class WebInspector(QWebInspector):
|
||||
|
||||
"""A customized WebInspector which stores its geometry."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._load_state_geometry()
|
||||
|
||||
def closeEvent(self, e):
|
||||
"""Save the geometry when closed."""
|
||||
state_config = objreg.get('state-config')
|
||||
data = bytes(self.saveGeometry())
|
||||
geom = base64.b64encode(data).decode('ASCII')
|
||||
state_config['geometry']['inspector'] = geom
|
||||
super().closeEvent(e)
|
||||
|
||||
def _load_state_geometry(self):
|
||||
"""Load the geometry from the state file."""
|
||||
state_config = objreg.get('state-config')
|
||||
try:
|
||||
data = state_config['geometry']['inspector']
|
||||
geom = base64.b64decode(data, validate=True)
|
||||
except KeyError:
|
||||
# First start
|
||||
pass
|
||||
except binascii.Error:
|
||||
log.misc.exception("Error while reading geometry")
|
||||
else:
|
||||
log.init.debug("Loading geometry from {}".format(geom))
|
||||
ok = self.restoreGeometry(geom)
|
||||
if not ok:
|
||||
log.init.warning("Error while loading geometry.")
|
||||
@@ -19,45 +19,23 @@
|
||||
|
||||
"""Our own QNetworkAccessManager."""
|
||||
|
||||
import collections
|
||||
from PyQt5.QtCore import pyqtSlot, pyqtSignal, PYQT_VERSION, QCoreApplication
|
||||
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply
|
||||
|
||||
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, QCoreApplication,
|
||||
QUrl)
|
||||
from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkReply, QSslError,
|
||||
QSslSocket)
|
||||
try:
|
||||
from PyQt5.QtNetwork import QSslSocket
|
||||
except ImportError:
|
||||
SSL_AVAILABLE = False
|
||||
else:
|
||||
SSL_AVAILABLE = QSslSocket.supportsSsl()
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import (message, log, usertypes, utils, objreg, qtutils,
|
||||
urlutils)
|
||||
from qutebrowser.utils import message, log, usertypes, utils, objreg
|
||||
from qutebrowser.browser import cookies
|
||||
from qutebrowser.browser.network import qutescheme, networkreply
|
||||
|
||||
|
||||
HOSTBLOCK_ERROR_STRING = '%HOSTBLOCK%'
|
||||
ProxyId = collections.namedtuple('ProxyId', 'type, hostname, port')
|
||||
_proxy_auth_cache = {}
|
||||
|
||||
|
||||
def init():
|
||||
"""Disable insecure SSL ciphers on old Qt versions."""
|
||||
if not qtutils.version_check('5.3.0'):
|
||||
# Disable weak SSL ciphers.
|
||||
# See https://codereview.qt-project.org/#/c/75943/
|
||||
good_ciphers = [c for c in QSslSocket.supportedCiphers()
|
||||
if c.usedBits() >= 128]
|
||||
QSslSocket.setDefaultCiphers(good_ciphers)
|
||||
|
||||
|
||||
class SslError(QSslError):
|
||||
|
||||
"""A QSslError subclass which provides __hash__ on Qt < 5.4."""
|
||||
|
||||
def __hash__(self):
|
||||
try:
|
||||
# Qt >= 5.4
|
||||
return super().__hash__()
|
||||
except TypeError:
|
||||
return hash((self.certificate().toDer(), self.error()))
|
||||
|
||||
|
||||
class NetworkManager(QNetworkAccessManager):
|
||||
@@ -65,18 +43,11 @@ class NetworkManager(QNetworkAccessManager):
|
||||
"""Our own QNetworkAccessManager.
|
||||
|
||||
Attributes:
|
||||
adopted_downloads: If downloads are running with this QNAM but the
|
||||
associated tab gets closed already, the NAM gets
|
||||
reparented to the DownloadManager. This counts the
|
||||
still running downloads, so the QNAM can clean
|
||||
itself up when this reaches zero again.
|
||||
_requests: Pending requests.
|
||||
_scheme_handlers: A dictionary (scheme -> handler) of supported custom
|
||||
schemes.
|
||||
_win_id: The window ID this NetworkManager is associated with.
|
||||
_tab_id: The tab ID this NetworkManager is associated with.
|
||||
_rejected_ssl_errors: A {QUrl: [SslError]} dict of rejected errors.
|
||||
_accepted_ssl_errors: A {QUrl: [SslError]} dict of accepted errors.
|
||||
|
||||
Signals:
|
||||
shutting_down: Emitted when the QNAM is shutting down.
|
||||
@@ -91,7 +62,6 @@ class NetworkManager(QNetworkAccessManager):
|
||||
# http://www.riverbankcomputing.com/pipermail/pyqt/2014-November/035045.html
|
||||
super().__init__(parent)
|
||||
log.init.debug("NetworkManager init done")
|
||||
self.adopted_downloads = 0
|
||||
self._win_id = win_id
|
||||
self._tab_id = tab_id
|
||||
self._requests = []
|
||||
@@ -100,9 +70,8 @@ class NetworkManager(QNetworkAccessManager):
|
||||
}
|
||||
self._set_cookiejar()
|
||||
self._set_cache()
|
||||
self.sslErrors.connect(self.on_ssl_errors)
|
||||
self._rejected_ssl_errors = collections.defaultdict(list)
|
||||
self._accepted_ssl_errors = collections.defaultdict(list)
|
||||
if SSL_AVAILABLE:
|
||||
self.sslErrors.connect(self.on_ssl_errors)
|
||||
self.authenticationRequired.connect(self.on_authentication_required)
|
||||
self.proxyAuthenticationRequired.connect(
|
||||
self.on_proxy_authentication_required)
|
||||
@@ -138,10 +107,11 @@ class NetworkManager(QNetworkAccessManager):
|
||||
self.setCache(cache)
|
||||
cache.setParent(app)
|
||||
|
||||
def _ask(self, text, mode, owner=None):
|
||||
def _ask(self, win_id, text, mode, owner=None):
|
||||
"""Ask a blocking question in the statusbar.
|
||||
|
||||
Args:
|
||||
win_id: The ID of the window which is calling this function.
|
||||
text: The text to display to the user.
|
||||
mode: A PromptMode.
|
||||
owner: An object which will abort the question if destroyed, or
|
||||
@@ -159,12 +129,21 @@ class NetworkManager(QNetworkAccessManager):
|
||||
webview = objreg.get('webview', scope='tab', window=self._win_id,
|
||||
tab=self._tab_id)
|
||||
webview.loadStarted.connect(q.abort)
|
||||
bridge = objreg.get('message-bridge', scope='window',
|
||||
window=self._win_id)
|
||||
bridge = objreg.get('message-bridge', scope='window', window=win_id)
|
||||
bridge.ask(q, blocking=True)
|
||||
q.deleteLater()
|
||||
return q.answer
|
||||
|
||||
def _fill_authenticator(self, authenticator, answer):
|
||||
"""Fill a given QAuthenticator object with an answer."""
|
||||
if answer is not None:
|
||||
# Since the answer could be something else than (user, password)
|
||||
# pylint seems to think we're unpacking a non-sequence. However we
|
||||
# *did* explicitely ask for a tuple, so it *will* always be one.
|
||||
user, password = answer
|
||||
authenticator.setUser(user)
|
||||
authenticator.setPassword(password)
|
||||
|
||||
def shutdown(self):
|
||||
"""Abort all running requests."""
|
||||
self.setNetworkAccessible(QNetworkAccessManager.NotAccessible)
|
||||
@@ -174,7 +153,7 @@ class NetworkManager(QNetworkAccessManager):
|
||||
self.shutting_down.emit()
|
||||
|
||||
@pyqtSlot('QNetworkReply*', 'QList<QSslError>')
|
||||
def on_ssl_errors(self, reply, errors): # pragma: no mccabe
|
||||
def on_ssl_errors(self, reply, errors):
|
||||
"""Decide if SSL errors should be ignored or not.
|
||||
|
||||
This slot is called on SSL/TLS errors by the self.sslErrors signal.
|
||||
@@ -183,37 +162,15 @@ class NetworkManager(QNetworkAccessManager):
|
||||
reply: The QNetworkReply that is encountering the errors.
|
||||
errors: A list of errors.
|
||||
"""
|
||||
errors = [SslError(e) for e in errors]
|
||||
ssl_strict = config.get('network', 'ssl-strict')
|
||||
if ssl_strict == 'ask':
|
||||
try:
|
||||
host_tpl = urlutils.host_tuple(reply.url())
|
||||
except ValueError:
|
||||
host_tpl = None
|
||||
is_accepted = False
|
||||
is_rejected = False
|
||||
else:
|
||||
is_accepted = set(errors).issubset(
|
||||
self._accepted_ssl_errors[host_tpl])
|
||||
is_rejected = set(errors).issubset(
|
||||
self._rejected_ssl_errors[host_tpl])
|
||||
if is_accepted:
|
||||
err_string = '\n'.join('- ' + err.errorString() for err in errors)
|
||||
answer = self._ask(self._win_id,
|
||||
'SSL errors - continue?\n{}'.format(err_string),
|
||||
mode=usertypes.PromptMode.yesno,
|
||||
owner=reply)
|
||||
if answer:
|
||||
reply.ignoreSslErrors()
|
||||
elif is_rejected:
|
||||
pass
|
||||
else:
|
||||
err_string = '\n'.join('- ' + err.errorString() for err in
|
||||
errors)
|
||||
answer = self._ask('SSL errors - continue?\n{}'.format(
|
||||
err_string), mode=usertypes.PromptMode.yesno,
|
||||
owner=reply)
|
||||
if answer:
|
||||
reply.ignoreSslErrors()
|
||||
d = self._accepted_ssl_errors
|
||||
else:
|
||||
d = self._rejected_ssl_errors
|
||||
if host_tpl is not None:
|
||||
d[host_tpl] += errors
|
||||
elif ssl_strict:
|
||||
pass
|
||||
else:
|
||||
@@ -224,43 +181,21 @@ class NetworkManager(QNetworkAccessManager):
|
||||
'SSL error: {}'.format(err.errorString()))
|
||||
reply.ignoreSslErrors()
|
||||
|
||||
@pyqtSlot(QUrl)
|
||||
def clear_rejected_ssl_errors(self, url):
|
||||
"""Clear the rejected SSL errors on a reload.
|
||||
|
||||
Args:
|
||||
url: The URL to remove.
|
||||
"""
|
||||
try:
|
||||
del self._rejected_ssl_errors[url]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@pyqtSlot('QNetworkReply', 'QAuthenticator')
|
||||
def on_authentication_required(self, reply, authenticator):
|
||||
"""Called when a website needs authentication."""
|
||||
answer = self._ask("Username ({}):".format(authenticator.realm()),
|
||||
answer = self._ask(self._win_id,
|
||||
"Username ({}):".format(authenticator.realm()),
|
||||
mode=usertypes.PromptMode.user_pwd,
|
||||
owner=reply)
|
||||
if answer is not None:
|
||||
authenticator.setUser(answer.user)
|
||||
authenticator.setPassword(answer.password)
|
||||
self._fill_authenticator(authenticator, answer)
|
||||
|
||||
@pyqtSlot('QNetworkProxy', 'QAuthenticator')
|
||||
def on_proxy_authentication_required(self, proxy, authenticator):
|
||||
def on_proxy_authentication_required(self, _proxy, authenticator):
|
||||
"""Called when a proxy needs authentication."""
|
||||
proxy_id = ProxyId(proxy.type(), proxy.hostName(), proxy.port())
|
||||
if proxy_id in _proxy_auth_cache:
|
||||
user, password = _proxy_auth_cache[proxy_id]
|
||||
authenticator.setUser(user)
|
||||
authenticator.setPassword(password)
|
||||
else:
|
||||
answer = self._ask("Proxy username ({}):".format(
|
||||
authenticator.realm()), mode=usertypes.PromptMode.user_pwd)
|
||||
if answer is not None:
|
||||
authenticator.setUser(answer.user)
|
||||
authenticator.setPassword(answer.password)
|
||||
_proxy_auth_cache[proxy_id] = answer
|
||||
answer = self._ask(self._win_id, "Proxy username ({}):".format(
|
||||
authenticator.realm()), mode=usertypes.PromptMode.user_pwd)
|
||||
self._fill_authenticator(authenticator, answer)
|
||||
|
||||
@config.change_filter('general', 'private-browsing')
|
||||
def on_config_changed(self):
|
||||
@@ -273,28 +208,6 @@ class NetworkManager(QNetworkAccessManager):
|
||||
# switched from private mode to normal mode
|
||||
self._set_cookiejar()
|
||||
|
||||
@pyqtSlot()
|
||||
def on_adopted_download_destroyed(self):
|
||||
"""Check if we can clean up if an adopted download was destroyed.
|
||||
|
||||
See the description for adopted_downloads for details.
|
||||
"""
|
||||
self.adopted_downloads -= 1
|
||||
log.downloads.debug("Adopted download destroyed, {} left.".format(
|
||||
self.adopted_downloads))
|
||||
assert self.adopted_downloads >= 0
|
||||
if self.adopted_downloads == 0:
|
||||
self.deleteLater()
|
||||
|
||||
@pyqtSlot(object) # DownloadItem
|
||||
def adopt_download(self, download):
|
||||
"""Adopt a new DownloadItem."""
|
||||
self.adopted_downloads += 1
|
||||
log.downloads.debug("Adopted download, {} adopted.".format(
|
||||
self.adopted_downloads))
|
||||
download.destroyed.connect(self.on_adopted_download_destroyed)
|
||||
download.do_retry.connect(self.adopt_download)
|
||||
|
||||
# WORKAROUND for:
|
||||
# http://www.riverbankcomputing.com/pipermail/pyqt/2014-September/034806.html
|
||||
#
|
||||
@@ -317,20 +230,20 @@ class NetworkManager(QNetworkAccessManager):
|
||||
A QNetworkReply.
|
||||
"""
|
||||
scheme = req.url().scheme()
|
||||
if scheme in self._scheme_handlers:
|
||||
if scheme == 'https' and not SSL_AVAILABLE:
|
||||
return networkreply.ErrorNetworkReply(
|
||||
req, "SSL is not supported by the installed Qt library!",
|
||||
QNetworkReply.ProtocolUnknownError, self)
|
||||
elif scheme in self._scheme_handlers:
|
||||
return self._scheme_handlers[scheme].createRequest(
|
||||
op, req, outgoing_data)
|
||||
|
||||
host_blocker = objreg.get('host-blocker')
|
||||
if (op == QNetworkAccessManager.GetOperation and
|
||||
req.url().host() in host_blocker.blocked_hosts and
|
||||
config.get('content', 'host-blocking-enabled')):
|
||||
req.url().host() in objreg.get('host-blocker').blocked_hosts):
|
||||
log.webview.info("Request to {} blocked by host blocker.".format(
|
||||
req.url().host()))
|
||||
return networkreply.ErrorNetworkReply(
|
||||
req, HOSTBLOCK_ERROR_STRING, QNetworkReply.ContentAccessDenied,
|
||||
self)
|
||||
|
||||
if config.get('network', 'do-not-track'):
|
||||
dnt = '1'.encode('ascii')
|
||||
else:
|
||||
@@ -354,5 +267,5 @@ class NetworkManager(QNetworkAccessManager):
|
||||
else:
|
||||
reply = super().createRequest(op, req, outgoing_data)
|
||||
self._requests.append(reply)
|
||||
reply.destroyed.connect(self._requests.remove)
|
||||
reply.destroyed.connect(lambda obj: self._requests.remove(obj))
|
||||
return reply
|
||||
|
||||
@@ -54,7 +54,6 @@ class FixedDataNetworkReply(QNetworkReply):
|
||||
self.setAttribute(QNetworkRequest.HttpReasonPhraseAttribute, 'OK')
|
||||
# For some reason, a segfault will be triggered if these lambdas aren't
|
||||
# there.
|
||||
# pylint: disable=unnecessary-lambda
|
||||
QTimer.singleShot(0, lambda: self.metaDataChanged.emit())
|
||||
QTimer.singleShot(0, lambda: self.readyRead.emit())
|
||||
QTimer.singleShot(0, lambda: self.finished.emit())
|
||||
@@ -113,7 +112,6 @@ class ErrorNetworkReply(QNetworkReply):
|
||||
self.setError(error, errorstring)
|
||||
# For some reason, a segfault will be triggered if these lambdas aren't
|
||||
# there.
|
||||
# pylint: disable=unnecessary-lambda
|
||||
QTimer.singleShot(0, lambda: self.error.emit(error))
|
||||
QTimer.singleShot(0, lambda: self.finished.emit())
|
||||
|
||||
|
||||
@@ -19,19 +19,21 @@
|
||||
|
||||
"""Client for the pastebin."""
|
||||
|
||||
import functools
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl
|
||||
|
||||
from qutebrowser.misc import httpclient
|
||||
from PyQt5.QtCore import pyqtSignal, QObject, QUrl
|
||||
from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkRequest,
|
||||
QNetworkReply)
|
||||
|
||||
|
||||
class PastebinClient(QObject):
|
||||
|
||||
"""A client for http://p.cmpl.cc/ using HTTPClient.
|
||||
"""A client for http://p.cmpl.cc/ using QNetworkAccessManager.
|
||||
|
||||
Attributes:
|
||||
_client: The HTTPClient used.
|
||||
_nam: The QNetworkAccessManager used.
|
||||
|
||||
Class attributes:
|
||||
API_URL: The base API URL.
|
||||
@@ -49,9 +51,7 @@ class PastebinClient(QObject):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._client = httpclient.HTTPClient(self)
|
||||
self._client.error.connect(self.error)
|
||||
self._client.success.connect(self.on_client_success)
|
||||
self._nam = QNetworkAccessManager(self)
|
||||
|
||||
def paste(self, name, title, text, parent=None):
|
||||
"""Paste the text into a pastebin and return the URL.
|
||||
@@ -69,17 +69,33 @@ class PastebinClient(QObject):
|
||||
}
|
||||
if parent is not None:
|
||||
data['reply'] = parent
|
||||
url = QUrl(urllib.parse.urljoin(self.API_URL, 'create'))
|
||||
self._client.post(url, data)
|
||||
encoded_data = urllib.parse.urlencode(data).encode('utf-8')
|
||||
create_url = urllib.parse.urljoin(self.API_URL, 'create')
|
||||
request = QNetworkRequest(QUrl(create_url))
|
||||
request.setHeader(QNetworkRequest.ContentTypeHeader,
|
||||
'application/x-www-form-urlencoded;charset=utf-8')
|
||||
reply = self._nam.post(request, encoded_data)
|
||||
if reply.isFinished():
|
||||
self.on_reply_finished(reply)
|
||||
else:
|
||||
reply.finished.connect(functools.partial(
|
||||
self.on_reply_finished, reply))
|
||||
|
||||
@pyqtSlot(str)
|
||||
def on_client_success(self, data):
|
||||
"""Process the data and finish when the client finished.
|
||||
def on_reply_finished(self, reply):
|
||||
"""Read the data and finish when the reply finished.
|
||||
|
||||
Args:
|
||||
data: A string with the received data.
|
||||
reply: The QNetworkReply which finished.
|
||||
"""
|
||||
if data.startswith('http://'):
|
||||
self.success.emit(data)
|
||||
if reply.error() != QNetworkReply.NoError:
|
||||
self.error.emit(reply.errorString())
|
||||
return
|
||||
try:
|
||||
url = bytes(reply.readAll()).decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
self.error.emit("Invalid UTF-8 data received in reply!")
|
||||
return
|
||||
if url.startswith('http://'):
|
||||
self.success.emit(url)
|
||||
else:
|
||||
self.error.emit("Invalid data received in reply!")
|
||||
|
||||
@@ -19,9 +19,7 @@
|
||||
#
|
||||
# pylint complains when using .render() on jinja templates, so we make it shut
|
||||
# up for this whole module.
|
||||
|
||||
# pylint: disable=no-member
|
||||
# https://bitbucket.org/logilab/pylint/issue/490/
|
||||
# pylint: disable=maybe-no-member
|
||||
|
||||
"""Handler functions for different qute:... pages.
|
||||
|
||||
@@ -29,7 +27,6 @@ Module attributes:
|
||||
pyeval_output: The output of the last :pyeval command.
|
||||
"""
|
||||
|
||||
import functools
|
||||
import configparser
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, QObject
|
||||
@@ -96,12 +93,6 @@ class JSBridge(QObject):
|
||||
@pyqtSlot(int, str, str, str)
|
||||
def set(self, win_id, sectname, optname, value):
|
||||
"""Slot to set a setting from qute:settings."""
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/727
|
||||
if ((sectname, optname) == ('content', 'allow-javascript') and
|
||||
value == 'false'):
|
||||
message.error(win_id, "Refusing to disable javascript via "
|
||||
"qute:settings as it needs javascript support.")
|
||||
return
|
||||
try:
|
||||
objreg.get('config').set('conf', sectname, optname, value)
|
||||
except (configexc.Error, configparser.Error) as e:
|
||||
@@ -159,7 +150,7 @@ def qute_help(win_id, request):
|
||||
url=request.url().toDisplayString(),
|
||||
error="This most likely means the documentation was not generated "
|
||||
"properly. If you are running qutebrowser from the git "
|
||||
"repository, please run scripts/asciidoc2html.py. "
|
||||
"repository, please run scripts/asciidoc2html.py."
|
||||
"If you're running a released version this is a bug, please "
|
||||
"use :report to report it.",
|
||||
icon='')
|
||||
@@ -177,11 +168,9 @@ def qute_help(win_id, request):
|
||||
|
||||
|
||||
def qute_settings(win_id, _request):
|
||||
"""Handler for qute:settings. View/change qute configuration."""
|
||||
config_getter = functools.partial(objreg.get('config').get, raw=True)
|
||||
"""Handler for qute:settings. View/change qute configuration"""
|
||||
html = jinja.env.get_template('settings.html').render(
|
||||
win_id=win_id, title='settings', config=configdata,
|
||||
confget=config_getter)
|
||||
win_id=win_id, title='settings', config=configdata)
|
||||
return html.encode('UTF-8', errors='xmlcharrefreplace')
|
||||
|
||||
|
||||
|
||||
@@ -24,15 +24,14 @@ OrderedDict. This is because we read them from a file at start and write them
|
||||
to a file on shutdown, so it makes sense to keep them as strings here.
|
||||
"""
|
||||
|
||||
import os.path
|
||||
import functools
|
||||
import collections
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, QUrl, QObject
|
||||
from PyQt5.QtCore import pyqtSignal, QStandardPaths, QUrl, QObject
|
||||
|
||||
from qutebrowser.utils import message, usertypes, urlutils, standarddir, objreg
|
||||
from qutebrowser.utils import message, usertypes, urlutils, standarddir
|
||||
from qutebrowser.commands import cmdexc, cmdutils
|
||||
from qutebrowser.misc import lineparser
|
||||
from qutebrowser.config.parsers import line as lineparser
|
||||
|
||||
|
||||
class QuickmarkManager(QObject):
|
||||
@@ -41,21 +40,10 @@ class QuickmarkManager(QObject):
|
||||
|
||||
Attributes:
|
||||
marks: An OrderedDict of all quickmarks.
|
||||
_lineparser: The LineParser used for the quickmarks, or None
|
||||
(when qutebrowser is started with -c '').
|
||||
|
||||
Signals:
|
||||
changed: Emitted when anything changed.
|
||||
added: Emitted when a new quickmark was added.
|
||||
arg 0: The name of the quickmark.
|
||||
arg 1: The URL of the quickmark, as string.
|
||||
removed: Emitted when an existing quickmark was removed.
|
||||
arg 0: The name of the quickmark.
|
||||
_linecp: The LineConfigParser used for the quickmarks.
|
||||
"""
|
||||
|
||||
changed = pyqtSignal()
|
||||
added = pyqtSignal(str, str)
|
||||
removed = pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""Initialize and read quickmarks."""
|
||||
@@ -63,32 +51,20 @@ class QuickmarkManager(QObject):
|
||||
|
||||
self.marks = collections.OrderedDict()
|
||||
|
||||
if standarddir.config() is None:
|
||||
self._lineparser = None
|
||||
else:
|
||||
self._lineparser = lineparser.LineParser(
|
||||
standarddir.config(), 'quickmarks', parent=self)
|
||||
for line in self._lineparser:
|
||||
if not line.strip():
|
||||
# Ignore empty or whitespace-only lines.
|
||||
continue
|
||||
try:
|
||||
key, url = line.rsplit(maxsplit=1)
|
||||
except ValueError:
|
||||
message.error(0, "Invalid quickmark '{}'".format(line))
|
||||
else:
|
||||
self.marks[key] = url
|
||||
filename = os.path.join(standarddir.config(), 'quickmarks')
|
||||
objreg.get('save-manager').add_saveable(
|
||||
'quickmark-manager', self.save, self.changed,
|
||||
filename=filename)
|
||||
confdir = standarddir.get(QStandardPaths.ConfigLocation)
|
||||
self._linecp = lineparser.LineConfigParser(confdir, 'quickmarks')
|
||||
for line in self._linecp:
|
||||
try:
|
||||
key, url = line.rsplit(maxsplit=1)
|
||||
except ValueError:
|
||||
message.error(0, "Invalid quickmark '{}'".format(line))
|
||||
else:
|
||||
self.marks[key] = url
|
||||
|
||||
def save(self):
|
||||
"""Save the quickmarks to disk."""
|
||||
if self._lineparser is not None:
|
||||
self._lineparser.data = [' '.join(tpl)
|
||||
for tpl in self.marks.items()]
|
||||
self._lineparser.save()
|
||||
self._linecp.data = [' '.join(tpl) for tpl in self.marks.items()]
|
||||
self._linecp.save()
|
||||
|
||||
def prompt_save(self, win_id, url):
|
||||
"""Prompt for a new quickmark name to be added and add it.
|
||||
@@ -105,8 +81,8 @@ class QuickmarkManager(QObject):
|
||||
win_id, "Add quickmark:", usertypes.PromptMode.text,
|
||||
functools.partial(self.quickmark_add, win_id, urlstr))
|
||||
|
||||
@cmdutils.register(instance='quickmark-manager', win_id='win_id')
|
||||
def quickmark_add(self, win_id, url, name):
|
||||
@cmdutils.register(instance='quickmark-manager')
|
||||
def quickmark_add(self, win_id: {'special': 'win_id'}, url, name):
|
||||
"""Add a new quickmark.
|
||||
|
||||
Args:
|
||||
@@ -127,7 +103,6 @@ class QuickmarkManager(QObject):
|
||||
"""Really set the quickmark."""
|
||||
self.marks[name] = url
|
||||
self.changed.emit()
|
||||
self.added.emit(name, url)
|
||||
|
||||
if name in self.marks:
|
||||
message.confirm_async(
|
||||
@@ -149,7 +124,6 @@ class QuickmarkManager(QObject):
|
||||
raise cmdexc.CommandError("Quickmark '{}' not found!".format(name))
|
||||
else:
|
||||
self.changed.emit()
|
||||
self.removed.emit(name)
|
||||
|
||||
def get(self, name):
|
||||
"""Get the URL of the quickmark named name as a QUrl."""
|
||||
@@ -158,12 +132,9 @@ class QuickmarkManager(QObject):
|
||||
"Quickmark '{}' does not exist!".format(name))
|
||||
urlstr = self.marks[name]
|
||||
try:
|
||||
url = urlutils.fuzzy_url(urlstr, do_search=False)
|
||||
except urlutils.FuzzyUrlError as e:
|
||||
if e.url is None or not e.url.errorString():
|
||||
errstr = ''
|
||||
else:
|
||||
errstr = ' ({})'.format(e.url.errorString())
|
||||
raise cmdexc.CommandError("Invalid URL for quickmark {}: "
|
||||
"{}{}".format(name, urlstr, errstr))
|
||||
url = urlutils.fuzzy_url(urlstr)
|
||||
except urlutils.FuzzyUrlError:
|
||||
raise cmdexc.CommandError(
|
||||
"Invalid URL for quickmark {}: {} ({})".format(
|
||||
name, urlstr, url.errorString()))
|
||||
return url
|
||||
|
||||
@@ -17,14 +17,15 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""pyPEG parsing for the RFC 6266 (Content-Disposition) header."""
|
||||
"""pyPEG parsing for the RFC 6266 (Content-Disposition) header. """
|
||||
|
||||
import collections
|
||||
import urllib.parse
|
||||
import string
|
||||
import re
|
||||
|
||||
import pypeg2 as peg
|
||||
import pypeg2 as peg # pylint: disable=import-error
|
||||
# (fails on win7 in venv...)
|
||||
|
||||
from qutebrowser.utils import log, utils
|
||||
|
||||
@@ -121,7 +122,6 @@ class Language(str):
|
||||
FIXME: This grammar is not 100% correct yet.
|
||||
https://github.com/The-Compiler/qutebrowser/issues/105
|
||||
"""
|
||||
|
||||
grammar = re.compile('[A-Za-z0-9-]+')
|
||||
|
||||
|
||||
@@ -235,7 +235,7 @@ class ContentDisposition:
|
||||
"""
|
||||
|
||||
def __init__(self, disposition='inline', assocs=None):
|
||||
"""Used internally after parsing the header.
|
||||
"""This constructor is used internally after parsing the header.
|
||||
|
||||
Instances should generally be created from a factory
|
||||
function, such as parse_headers and its variants.
|
||||
@@ -265,6 +265,7 @@ class ContentDisposition:
|
||||
well, due to a certain browser using the part after the dot for
|
||||
mime-sniffing. Saving it to a database is fine by itself though.
|
||||
"""
|
||||
|
||||
if 'filename*' in self.assocs:
|
||||
return self.assocs['filename*']
|
||||
elif 'filename' in self.assocs:
|
||||
@@ -292,9 +293,7 @@ def normalize_ws(text):
|
||||
|
||||
def parse_headers(content_disposition):
|
||||
"""Build a ContentDisposition from header values."""
|
||||
# https://bitbucket.org/logilab/pylint/issue/492/
|
||||
# pylint: disable=no-member
|
||||
|
||||
# pylint: disable=maybe-no-member
|
||||
# We allow non-ascii here (it will only be parsed inside of qdtext, and
|
||||
# rejected by the grammar if it appears in other places), although parsing
|
||||
# it can be ambiguous. Parsing it ensures that a non-ambiguous filename*
|
||||
|
||||
@@ -41,8 +41,7 @@ class SignalFilter(QObject):
|
||||
BLACKLIST: List of signal names which should not be logged.
|
||||
"""
|
||||
|
||||
BLACKLIST = ['cur_scroll_perc_changed', 'cur_progress',
|
||||
'cur_statusbar_message', 'cur_link_hovered']
|
||||
BLACKLIST = ['cur_scroll_perc_changed', 'cur_progress']
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Utilities related to QWebHistory."""
|
||||
|
||||
|
||||
from PyQt5.QtCore import QByteArray, QDataStream, QIODevice, QUrl
|
||||
|
||||
from qutebrowser.utils import utils, qtutils
|
||||
|
||||
|
||||
HISTORY_STREAM_VERSION = 2
|
||||
BACK_FORWARD_TREE_VERSION = 2
|
||||
|
||||
|
||||
class TabHistoryItem:
|
||||
|
||||
"""A single item in the tab history.
|
||||
|
||||
Attributes:
|
||||
url: The QUrl of this item.
|
||||
title: The title as string of this item.
|
||||
active: Whether this item is the item currently navigated to.
|
||||
user_data: The user data for this item.
|
||||
"""
|
||||
|
||||
def __init__(self, url, original_url, title, active=False, user_data=None):
|
||||
self.url = url
|
||||
self.original_url = original_url
|
||||
self.title = title
|
||||
self.active = active
|
||||
self.user_data = user_data
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, constructor=True, url=self.url,
|
||||
original_url=self.original_url, title=self.title,
|
||||
active=self.active, user_data=self.user_data)
|
||||
|
||||
|
||||
def _encode_url(url):
|
||||
"""Encode an QUrl suitable to pass to QWebHistory."""
|
||||
data = bytes(QUrl.toPercentEncoding(url.toString(), b':/#?&+=@%*'))
|
||||
return data.decode('ascii')
|
||||
|
||||
|
||||
def _serialize_item(i, item, stream):
|
||||
"""Serialize a single WebHistoryItem into a QDataStream.
|
||||
|
||||
Args:
|
||||
i: The index of the current item.
|
||||
item: The WebHistoryItem to write.
|
||||
stream: The QDataStream to write to.
|
||||
"""
|
||||
### Source/WebCore/history/qt/HistoryItemQt.cpp restoreState
|
||||
## urlString
|
||||
stream.writeQString(_encode_url(item.url))
|
||||
## title
|
||||
stream.writeQString(item.title)
|
||||
## originalURLString
|
||||
stream.writeQString(_encode_url(item.original_url))
|
||||
|
||||
### Source/WebCore/history/HistoryItem.cpp decodeBackForwardTree
|
||||
## backForwardTreeEncodingVersion
|
||||
stream.writeUInt32(BACK_FORWARD_TREE_VERSION)
|
||||
## size (recursion stack)
|
||||
stream.writeUInt64(0)
|
||||
## node->m_documentSequenceNumber
|
||||
# If two HistoryItems have the same document sequence number, then they
|
||||
# refer to the same instance of a document. Traversing history from one
|
||||
# such HistoryItem to another preserves the document.
|
||||
stream.writeInt64(i + 1)
|
||||
## size (node->m_documentState)
|
||||
stream.writeUInt64(0)
|
||||
## node->m_formContentType
|
||||
# info used to repost form data
|
||||
stream.writeQString(None)
|
||||
## hasFormData
|
||||
stream.writeBool(False)
|
||||
## node->m_itemSequenceNumber
|
||||
# If two HistoryItems have the same item sequence number, then they are
|
||||
# clones of one another. Traversing history from one such HistoryItem to
|
||||
# another is a no-op. HistoryItem clones are created for parent and
|
||||
# sibling frames when only a subframe navigates.
|
||||
stream.writeInt64(i + 1)
|
||||
## node->m_referrer
|
||||
stream.writeQString(None)
|
||||
## node->m_scrollPoint (x)
|
||||
try:
|
||||
stream.writeInt32(item.user_data['scroll-pos'].x())
|
||||
except (KeyError, TypeError):
|
||||
stream.writeInt32(0)
|
||||
## node->m_scrollPoint (y)
|
||||
try:
|
||||
stream.writeInt32(item.user_data['scroll-pos'].y())
|
||||
except (KeyError, TypeError):
|
||||
stream.writeInt32(0)
|
||||
## node->m_pageScaleFactor
|
||||
stream.writeFloat(1)
|
||||
## hasStateObject
|
||||
# Support for HTML5 History
|
||||
stream.writeBool(False)
|
||||
## node->m_target
|
||||
stream.writeQString(None)
|
||||
|
||||
### Source/WebCore/history/qt/HistoryItemQt.cpp restoreState
|
||||
## validUserData
|
||||
# We could restore the user data here, but we prefer to use the
|
||||
# QWebHistoryItem API for that.
|
||||
stream.writeBool(False)
|
||||
|
||||
|
||||
def serialize(items):
|
||||
"""Serialize a list of QWebHistoryItems to a data stream.
|
||||
|
||||
Args:
|
||||
items: An iterable of WebHistoryItems.
|
||||
|
||||
Return:
|
||||
A (stream, data, user_data) tuple.
|
||||
stream: The reseted QDataStream.
|
||||
data: The QByteArray with the raw data.
|
||||
user_data: A list with each item's user data.
|
||||
|
||||
Warning:
|
||||
If 'data' goes out of scope, reading from 'stream' will result in a
|
||||
segfault!
|
||||
"""
|
||||
data = QByteArray()
|
||||
stream = QDataStream(data, QIODevice.ReadWrite)
|
||||
user_data = []
|
||||
|
||||
current_idx = None
|
||||
|
||||
for i, item in enumerate(items):
|
||||
if item.active:
|
||||
if current_idx is not None:
|
||||
raise ValueError("Multiple active items ({} and {}) "
|
||||
"found!".format(current_idx, i))
|
||||
else:
|
||||
current_idx = i
|
||||
|
||||
if items:
|
||||
if current_idx is None:
|
||||
raise ValueError("No active item found!")
|
||||
else:
|
||||
current_idx = 0
|
||||
|
||||
### Source/WebKit/qt/Api/qwebhistory.cpp operator<<
|
||||
stream.writeInt(HISTORY_STREAM_VERSION)
|
||||
stream.writeInt(len(items))
|
||||
stream.writeInt(current_idx)
|
||||
|
||||
for i, item in enumerate(items):
|
||||
_serialize_item(i, item, stream)
|
||||
user_data.append(item.user_data)
|
||||
|
||||
stream.device().reset()
|
||||
qtutils.check_qdatastream(stream)
|
||||
return stream, data, user_data
|
||||
@@ -277,12 +277,6 @@ class WebElementWrapper(collections.abc.MutableMapping):
|
||||
else:
|
||||
return False
|
||||
|
||||
def is_text_input(self):
|
||||
"""Check if this element is some kind of text box."""
|
||||
roles = ('combobox', 'textbox')
|
||||
tag = self._elem.tagName().lower()
|
||||
return self.get('role', None) in roles or tag in ('input', 'textarea')
|
||||
|
||||
def debug_text(self):
|
||||
"""Get a text based on an element suitable for debug output."""
|
||||
self._check_vanished()
|
||||
@@ -312,7 +306,7 @@ def javascript_escape(text):
|
||||
def get_child_frames(startframe):
|
||||
"""Get all children recursively of a given QWebFrame.
|
||||
|
||||
Loosely based on http://blog.nextgenetics.net/?e=64
|
||||
Loosly based on http://blog.nextgenetics.net/?e=64
|
||||
|
||||
Args:
|
||||
startframe: The QWebFrame to start with.
|
||||
@@ -332,7 +326,7 @@ def get_child_frames(startframe):
|
||||
|
||||
|
||||
def focus_elem(frame):
|
||||
"""Get the focused element in a web frame.
|
||||
"""Get the focused element in a webframe.
|
||||
|
||||
FIXME: Add tests.
|
||||
|
||||
|
||||
@@ -21,8 +21,7 @@
|
||||
|
||||
import functools
|
||||
|
||||
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, Qt, QUrl, QPoint,
|
||||
QTimer)
|
||||
from PyQt5.QtCore import pyqtSlot, PYQT_VERSION, Qt, QUrl
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
|
||||
from PyQt5.QtWidgets import QFileDialog
|
||||
@@ -30,10 +29,10 @@ from PyQt5.QtPrintSupport import QPrintDialog
|
||||
from PyQt5.QtWebKitWidgets import QWebPage
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.browser import http, tabhistory
|
||||
from qutebrowser.browser import http
|
||||
from qutebrowser.browser.network import networkmanager
|
||||
from qutebrowser.utils import (message, usertypes, log, jinja, qtutils, utils,
|
||||
objreg, debug)
|
||||
objreg)
|
||||
|
||||
|
||||
class BrowserPage(QWebPage):
|
||||
@@ -41,52 +40,30 @@ class BrowserPage(QWebPage):
|
||||
"""Our own QWebPage with advanced features.
|
||||
|
||||
Attributes:
|
||||
error_occurred: Whether an error occurred while loading.
|
||||
open_target: Where to open the next navigation request.
|
||||
("normal", "tab", "tab_bg")
|
||||
_hint_target: Override for open_target while hinting, or None.
|
||||
error_occured: Whether an error occured while loading.
|
||||
_extension_handlers: Mapping of QWebPage extensions to their handlers.
|
||||
_networkmnager: The NetworkManager used.
|
||||
_win_id: The window ID this BrowserPage is associated with.
|
||||
_ignore_load_started: Whether to ignore the next loadStarted signal.
|
||||
_is_shutting_down: Whether the page is currently shutting down.
|
||||
|
||||
Signals:
|
||||
shutting_down: Emitted when the page is currently shutting down.
|
||||
reloading: Emitted before a web page reloads.
|
||||
arg: The URL which gets reloaded.
|
||||
"""
|
||||
|
||||
shutting_down = pyqtSignal()
|
||||
reloading = pyqtSignal(QUrl)
|
||||
|
||||
def __init__(self, win_id, tab_id, parent=None):
|
||||
super().__init__(parent)
|
||||
self._win_id = win_id
|
||||
self._is_shutting_down = False
|
||||
self._extension_handlers = {
|
||||
QWebPage.ErrorPageExtension: self._handle_errorpage,
|
||||
QWebPage.ChooseMultipleFilesExtension: self._handle_multiple_files,
|
||||
}
|
||||
self._ignore_load_started = False
|
||||
self.error_occurred = False
|
||||
self.open_target = usertypes.ClickTarget.normal
|
||||
self._hint_target = None
|
||||
self.error_occured = False
|
||||
self._networkmanager = networkmanager.NetworkManager(
|
||||
win_id, tab_id, self)
|
||||
self.setNetworkAccessManager(self._networkmanager)
|
||||
self.setForwardUnsupportedContent(True)
|
||||
self.reloading.connect(self._networkmanager.clear_rejected_ssl_errors)
|
||||
self.printRequested.connect(self.on_print_requested)
|
||||
self.downloadRequested.connect(self.on_download_requested)
|
||||
self.unsupportedContent.connect(self.on_unsupported_content)
|
||||
self.loadStarted.connect(self.on_load_started)
|
||||
self.featurePermissionRequested.connect(
|
||||
self.on_feature_permission_requested)
|
||||
self.saveFrameStateRequested.connect(
|
||||
self.on_save_frame_state_requested)
|
||||
self.restoreFrameStateRequested.connect(
|
||||
self.on_restore_frame_state_requested)
|
||||
|
||||
if PYQT_VERSION > 0x050300:
|
||||
# WORKAROUND (remove this when we bump the requirements to 5.3.1)
|
||||
@@ -96,9 +73,6 @@ class BrowserPage(QWebPage):
|
||||
|
||||
def javaScriptPrompt(self, _frame, msg, default):
|
||||
"""Override javaScriptPrompt to use the statusbar."""
|
||||
if (self._is_shutting_down or
|
||||
config.get('content', 'ignore-javascript-prompt')):
|
||||
return (False, "")
|
||||
answer = self._ask("js: {}".format(msg), usertypes.PromptMode.text,
|
||||
default)
|
||||
if answer is None:
|
||||
@@ -109,7 +83,7 @@ class BrowserPage(QWebPage):
|
||||
def _handle_errorpage(self, info, errpage):
|
||||
"""Display an error page if needed.
|
||||
|
||||
Loosely based on Helpviewer/HelpBrowserWV.py from eric5
|
||||
Loosly based on Helpviewer/HelpBrowserWV.py from eric5
|
||||
(line 260 @ 5d937eb378dd)
|
||||
|
||||
Args:
|
||||
@@ -147,29 +121,18 @@ class BrowserPage(QWebPage):
|
||||
else:
|
||||
error_str = info.errorString
|
||||
if error_str == networkmanager.HOSTBLOCK_ERROR_STRING:
|
||||
# We don't set error_occurred in this case.
|
||||
error_str = "Request blocked by host blocker."
|
||||
main_frame = info.frame.page().mainFrame()
|
||||
if info.frame != main_frame:
|
||||
# Content in an iframe -> Hide the frame so it doesn't use
|
||||
# any space. We can't hide the frame's documentElement
|
||||
# directly though.
|
||||
for elem in main_frame.documentElement().findAll('iframe'):
|
||||
if QUrl(elem.attribute('src')) == info.url:
|
||||
elem.setAttribute('style', 'display: none')
|
||||
return False
|
||||
# we don't set error_occured in this case.
|
||||
else:
|
||||
self._ignore_load_started = True
|
||||
self.error_occurred = True
|
||||
self.error_occured = True
|
||||
log.webview.error("Error while loading {}: {}".format(
|
||||
urlstr, error_str))
|
||||
log.webview.debug("Error domain: {}, error code: {}".format(
|
||||
info.domain, info.error))
|
||||
title = "Error loading page: {}".format(urlstr)
|
||||
template = jinja.env.get_template('error.html')
|
||||
# pylint: disable=no-member
|
||||
# https://bitbucket.org/logilab/pylint/issue/490/
|
||||
html = template.render(
|
||||
html = template.render( # pylint: disable=maybe-no-member
|
||||
title=title, url=urlstr, error=error_str, icon='')
|
||||
errpage.content = html.encode('utf-8')
|
||||
errpage.encoding = 'utf-8'
|
||||
@@ -178,7 +141,7 @@ class BrowserPage(QWebPage):
|
||||
def _handle_multiple_files(self, info, files):
|
||||
"""Handle uploading of multiple files.
|
||||
|
||||
Loosely based on Helpviewer/HelpBrowserWV.py from eric5.
|
||||
Loosly based on Helpviewer/HelpBrowserWV.py from eric5.
|
||||
|
||||
Args:
|
||||
info: The ChooseMultipleFilesExtensionOption instance.
|
||||
@@ -211,44 +174,14 @@ class BrowserPage(QWebPage):
|
||||
q.mode = mode
|
||||
q.default = default
|
||||
self.loadStarted.connect(q.abort)
|
||||
self.shutting_down.connect(q.abort)
|
||||
bridge = objreg.get('message-bridge', scope='window',
|
||||
window=self._win_id)
|
||||
bridge.ask(q, blocking=True)
|
||||
q.deleteLater()
|
||||
return q.answer
|
||||
|
||||
def shutdown(self):
|
||||
"""Prepare the web page for being deleted."""
|
||||
self._is_shutting_down = True
|
||||
self.shutting_down.emit()
|
||||
download_manager = objreg.get('download-manager', scope='window',
|
||||
window=self._win_id)
|
||||
nam = self.networkAccessManager()
|
||||
if download_manager.has_downloads_with_nam(nam):
|
||||
nam.setParent(download_manager)
|
||||
else:
|
||||
nam.shutdown()
|
||||
|
||||
def load_history(self, entries):
|
||||
"""Load the history from a list of TabHistoryItem objects."""
|
||||
stream, _data, user_data = tabhistory.serialize(entries)
|
||||
history = self.history()
|
||||
qtutils.deserialize_stream(stream, history)
|
||||
for i, data in enumerate(user_data):
|
||||
history.itemAt(i).setUserData(data)
|
||||
cur_data = history.currentItem().userData()
|
||||
if cur_data is not None:
|
||||
frame = self.mainFrame()
|
||||
if 'zoom' in cur_data:
|
||||
frame.page().view().zoom_perc(cur_data['zoom'] * 100)
|
||||
if ('scroll-pos' in cur_data and
|
||||
frame.scrollPosition() == QPoint(0, 0)):
|
||||
QTimer.singleShot(0, functools.partial(
|
||||
frame.setScrollPosition, cur_data['scroll-pos']))
|
||||
|
||||
def display_content(self, reply, mimetype):
|
||||
"""Display a QNetworkReply with an explicitly set mimetype."""
|
||||
"""Display a QNetworkReply with an explicitely set mimetype."""
|
||||
self.mainFrame().setContent(reply.readAll(), mimetype, reply.url())
|
||||
reply.deleteLater()
|
||||
|
||||
@@ -286,13 +219,12 @@ class BrowserPage(QWebPage):
|
||||
At some point we might want to implement the MIME Sniffing standard
|
||||
here: http://mimesniff.spec.whatwg.org/
|
||||
"""
|
||||
inline, suggested_filename = http.parse_content_disposition(reply)
|
||||
inline, _suggested_filename = http.parse_content_disposition(reply)
|
||||
download_manager = objreg.get('download-manager', scope='window',
|
||||
window=self._win_id)
|
||||
if not inline:
|
||||
# Content-Disposition: attachment -> force download
|
||||
download_manager.fetch(reply,
|
||||
suggested_filename=suggested_filename)
|
||||
download_manager.fetch(reply)
|
||||
return
|
||||
mimetype, _rest = http.parse_content_type(reply)
|
||||
if mimetype == 'image/jpg':
|
||||
@@ -307,136 +239,15 @@ class BrowserPage(QWebPage):
|
||||
self.display_content, reply, 'image/jpeg'))
|
||||
else:
|
||||
# Unknown mimetype, so download anyways.
|
||||
download_manager.fetch(reply,
|
||||
suggested_filename=suggested_filename)
|
||||
download_manager.fetch(reply)
|
||||
|
||||
@pyqtSlot()
|
||||
def on_load_started(self):
|
||||
"""Reset error_occurred when loading of a new page started."""
|
||||
"""Reset error_occured when loading of a new page started."""
|
||||
if self._ignore_load_started:
|
||||
self._ignore_load_started = False
|
||||
else:
|
||||
self.error_occurred = False
|
||||
|
||||
@pyqtSlot('QWebFrame', 'QWebPage::Feature')
|
||||
def on_feature_permission_requested(self, frame, feature):
|
||||
"""Ask the user for approval for geolocation/notifications."""
|
||||
options = {
|
||||
QWebPage.Notifications: ('content', 'notifications'),
|
||||
QWebPage.Geolocation: ('content', 'geolocation'),
|
||||
}
|
||||
config_val = config.get(*options[feature])
|
||||
if config_val == 'ask':
|
||||
bridge = objreg.get('message-bridge', scope='window',
|
||||
window=self._win_id)
|
||||
q = usertypes.Question(bridge)
|
||||
q.mode = usertypes.PromptMode.yesno
|
||||
|
||||
msgs = {
|
||||
QWebPage.Notifications: 'show notifications',
|
||||
QWebPage.Geolocation: 'access your location',
|
||||
}
|
||||
|
||||
host = frame.url().host()
|
||||
if host:
|
||||
q.text = "Allow the website at {} to {}?".format(
|
||||
frame.url().host(), msgs[feature])
|
||||
else:
|
||||
q.text = "Allow the website to {}?".format(msgs[feature])
|
||||
|
||||
yes_action = functools.partial(
|
||||
self.setFeaturePermission, frame, feature,
|
||||
QWebPage.PermissionGrantedByUser)
|
||||
q.answered_yes.connect(yes_action)
|
||||
|
||||
no_action = functools.partial(
|
||||
self.setFeaturePermission, frame, feature,
|
||||
QWebPage.PermissionDeniedByUser)
|
||||
q.answered_no.connect(no_action)
|
||||
q.cancelled.connect(no_action)
|
||||
|
||||
q.completed.connect(q.deleteLater)
|
||||
|
||||
self.featurePermissionRequestCanceled.connect(functools.partial(
|
||||
self.on_feature_permission_cancelled, q, frame, feature))
|
||||
self.loadStarted.connect(q.abort)
|
||||
|
||||
bridge.ask(q, blocking=False)
|
||||
elif config_val:
|
||||
self.setFeaturePermission(frame, feature,
|
||||
QWebPage.PermissionGrantedByUser)
|
||||
else:
|
||||
self.setFeaturePermission(frame, feature,
|
||||
QWebPage.PermissionDeniedByUser)
|
||||
|
||||
def on_feature_permission_cancelled(self, question, frame, feature,
|
||||
cancelled_frame, cancelled_feature):
|
||||
"""Slot invoked when a feature permission request was cancelled.
|
||||
|
||||
To be used with functools.partial.
|
||||
"""
|
||||
if frame is cancelled_frame and feature == cancelled_feature:
|
||||
try:
|
||||
question.abort()
|
||||
except RuntimeError:
|
||||
# The question could already be deleted, e.g. because it was
|
||||
# aborted after a loadStarted signal.
|
||||
pass
|
||||
|
||||
def on_save_frame_state_requested(self, frame, item):
|
||||
"""Save scroll position and zoom in history.
|
||||
|
||||
Args:
|
||||
frame: The QWebFrame which gets saved.
|
||||
item: The QWebHistoryItem to be saved.
|
||||
"""
|
||||
try:
|
||||
if frame != self.mainFrame():
|
||||
return
|
||||
except RuntimeError:
|
||||
# With Qt 5.2.1 (Ubuntu Trusty) we get this when closing a tab:
|
||||
# RuntimeError: wrapped C/C++ object of type BrowserPage has
|
||||
# been deleted
|
||||
# Since the information here isn't that important for closing web
|
||||
# views anyways, we ignore this error.
|
||||
return
|
||||
data = {
|
||||
'zoom': frame.zoomFactor(),
|
||||
'scroll-pos': frame.scrollPosition(),
|
||||
}
|
||||
item.setUserData(data)
|
||||
|
||||
def on_restore_frame_state_requested(self, frame):
|
||||
"""Restore scroll position and zoom from history.
|
||||
|
||||
Args:
|
||||
frame: The QWebFrame which gets restored.
|
||||
"""
|
||||
if frame != self.mainFrame():
|
||||
return
|
||||
data = self.history().currentItem().userData()
|
||||
if data is None:
|
||||
return
|
||||
if 'zoom' in data:
|
||||
frame.page().view().zoom_perc(data['zoom'] * 100)
|
||||
if 'scroll-pos' in data and frame.scrollPosition() == QPoint(0, 0):
|
||||
frame.setScrollPosition(data['scroll-pos'])
|
||||
|
||||
@pyqtSlot(str)
|
||||
def on_start_hinting(self, hint_target):
|
||||
"""Emitted before a hinting-click takes place.
|
||||
|
||||
Args:
|
||||
hint_target: A ClickTarget member to set self._hint_target to.
|
||||
"""
|
||||
log.webview.debug("Setting force target to {}".format(hint_target))
|
||||
self._hint_target = hint_target
|
||||
|
||||
@pyqtSlot()
|
||||
def on_stop_hinting(self):
|
||||
"""Emitted when hinting is finished."""
|
||||
log.webview.debug("Finishing hinting.")
|
||||
self._hint_target = None
|
||||
self.error_occured = False
|
||||
|
||||
def userAgentForUrl(self, url):
|
||||
"""Override QWebPage::userAgentForUrl to customize the user agent."""
|
||||
@@ -478,33 +289,19 @@ class BrowserPage(QWebPage):
|
||||
return super().extension(ext, opt, out)
|
||||
return handler(opt, out)
|
||||
|
||||
def javaScriptAlert(self, frame, msg):
|
||||
def javaScriptAlert(self, _frame, msg):
|
||||
"""Override javaScriptAlert to use the statusbar."""
|
||||
log.js.debug("alert: {}".format(msg))
|
||||
if config.get('ui', 'modal-js-dialog'):
|
||||
return super().javaScriptAlert(frame, msg)
|
||||
|
||||
if (self._is_shutting_down or
|
||||
config.get('content', 'ignore-javascript-alert')):
|
||||
return
|
||||
self._ask("[js alert] {}".format(msg), usertypes.PromptMode.alert)
|
||||
|
||||
def javaScriptConfirm(self, frame, msg):
|
||||
def javaScriptConfirm(self, _frame, msg):
|
||||
"""Override javaScriptConfirm to use the statusbar."""
|
||||
log.js.debug("confirm: {}".format(msg))
|
||||
if config.get('ui', 'modal-js-dialog'):
|
||||
return super().javaScriptConfirm(frame, msg)
|
||||
|
||||
if self._is_shutting_down:
|
||||
return False
|
||||
ans = self._ask("[js confirm] {}".format(msg),
|
||||
usertypes.PromptMode.yesno)
|
||||
return bool(ans)
|
||||
|
||||
def javaScriptConsoleMessage(self, msg, line, source):
|
||||
"""Override javaScriptConsoleMessage to use debug log."""
|
||||
if config.get('general', 'log-javascript-console'):
|
||||
log.js.debug("[{}:{}] {}".format(source, line, msg))
|
||||
log.js.debug("[{}:{}] {}".format(source, line, msg))
|
||||
|
||||
def chooseFile(self, _frame, suggested_file):
|
||||
"""Override QWebPage's chooseFile to be able to chose a file to upload.
|
||||
@@ -539,41 +336,30 @@ class BrowserPage(QWebPage):
|
||||
request: QNetworkRequest
|
||||
typ: QWebPage::NavigationType
|
||||
"""
|
||||
url = request.url()
|
||||
urlstr = url.toDisplayString()
|
||||
if typ == QWebPage.NavigationTypeReload:
|
||||
self.reloading.emit(url)
|
||||
if typ != QWebPage.NavigationTypeLinkClicked:
|
||||
return True
|
||||
url = request.url()
|
||||
urlstr = url.toDisplayString()
|
||||
if not url.isValid():
|
||||
message.error(self._win_id, "Invalid link {} clicked!".format(
|
||||
urlstr))
|
||||
log.webview.debug(url.errorString())
|
||||
self.open_target = usertypes.ClickTarget.normal
|
||||
return False
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=self._win_id)
|
||||
log.webview.debug("acceptNavigationRequest, url {}, type {}, hint "
|
||||
"target {}, open_target {}".format(
|
||||
urlstr, debug.qenum_key(QWebPage, typ),
|
||||
self._hint_target, self.open_target))
|
||||
if self._hint_target is not None:
|
||||
target = self._hint_target
|
||||
else:
|
||||
target = self.open_target
|
||||
self.open_target = usertypes.ClickTarget.normal
|
||||
if target == usertypes.ClickTarget.tab:
|
||||
open_target = self.view().open_target
|
||||
if open_target == usertypes.ClickTarget.tab:
|
||||
tabbed_browser.tabopen(url, False)
|
||||
return False
|
||||
elif target == usertypes.ClickTarget.tab_bg:
|
||||
elif open_target == usertypes.ClickTarget.tab_bg:
|
||||
tabbed_browser.tabopen(url, True)
|
||||
return False
|
||||
elif target == usertypes.ClickTarget.window:
|
||||
from qutebrowser.mainwindow import mainwindow
|
||||
window = mainwindow.MainWindow()
|
||||
window.show()
|
||||
elif open_target == usertypes.ClickTarget.window:
|
||||
main_window = objreg.get('main-window', scope='window',
|
||||
window=self._win_id)
|
||||
win_id = main_window.spawn()
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=window.win_id)
|
||||
window=win_id)
|
||||
tabbed_browser.tabopen(url, False)
|
||||
return False
|
||||
else:
|
||||
|
||||
@@ -24,7 +24,6 @@ import itertools
|
||||
import functools
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer, QUrl
|
||||
from PyQt5.QtGui import QPalette
|
||||
from PyQt5.QtWidgets import QApplication, QStyleFactory
|
||||
from PyQt5.QtWebKit import QWebSettings
|
||||
from PyQt5.QtWebKitWidgets import QWebView, QWebPage
|
||||
@@ -33,6 +32,7 @@ from qutebrowser.config import config
|
||||
from qutebrowser.keyinput import modeman
|
||||
from qutebrowser.utils import message, log, usertypes, utils, qtutils, objreg
|
||||
from qutebrowser.browser import webpage, hints, webelem
|
||||
from qutebrowser.commands import cmdexc
|
||||
|
||||
|
||||
LoadStatus = usertypes.enum('LoadStatus', ['none', 'success', 'error', 'warn',
|
||||
@@ -52,25 +52,23 @@ class WebView(QWebView):
|
||||
hintmanager: The HintManager instance for this view.
|
||||
progress: loading progress of this page.
|
||||
scroll_pos: The current scroll position as (x%, y%) tuple.
|
||||
statusbar_message: The current javascript statusbar message.
|
||||
statusbar_message: The current javscript statusbar message.
|
||||
inspector: The QWebInspector used for this webview.
|
||||
load_status: loading status of this page (index into LoadStatus)
|
||||
open_target: Where to open the next tab ("normal", "tab", "tab_bg")
|
||||
viewing_source: Whether the webview is currently displaying source
|
||||
code.
|
||||
keep_icon: Whether the (e.g. cloned) icon should not be cleared on page
|
||||
load.
|
||||
registry: The ObjectRegistry associated with this tab.
|
||||
tab_id: The tab ID of the view.
|
||||
win_id: The window ID of the view.
|
||||
search_text: The text of the last search.
|
||||
search_flags: The search flags of the last search.
|
||||
_cur_url: The current URL (accessed via cur_url property).
|
||||
_has_ssl_errors: Whether SSL errors occurred during loading.
|
||||
_has_ssl_errors: Whether SSL errors occured during loading.
|
||||
_zoom: A NeighborList with the zoom levels.
|
||||
_old_scroll_pos: The old scroll position.
|
||||
_force_open_target: Override for open_target.
|
||||
_check_insertmode: If True, in mouseReleaseEvent we should check if we
|
||||
need to enter/leave insert mode.
|
||||
_default_zoom_changed: Whether the zoom was changed from the default.
|
||||
_win_id: The window ID of the view.
|
||||
|
||||
Signals:
|
||||
scroll_pos_changed: Scroll percentage of current tab changed.
|
||||
@@ -79,14 +77,12 @@ class WebView(QWebView):
|
||||
linkHovered: QWebPages linkHovered signal exposed.
|
||||
load_status_changed: The loading status changed
|
||||
url_text_changed: Current URL string changed.
|
||||
shutting_down: Emitted when the view is shutting down.
|
||||
"""
|
||||
|
||||
scroll_pos_changed = pyqtSignal(int, int)
|
||||
linkHovered = pyqtSignal(str, str, str)
|
||||
load_status_changed = pyqtSignal(str)
|
||||
url_text_changed = pyqtSignal(str)
|
||||
shutting_down = pyqtSignal()
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -94,21 +90,18 @@ class WebView(QWebView):
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-42948
|
||||
# See https://github.com/The-Compiler/qutebrowser/issues/462
|
||||
self.setStyle(QStyleFactory.create('Fusion'))
|
||||
self.win_id = win_id
|
||||
self._win_id = win_id
|
||||
self.load_status = LoadStatus.none
|
||||
self._check_insertmode = False
|
||||
self.inspector = None
|
||||
self.scroll_pos = (-1, -1)
|
||||
self.statusbar_message = ''
|
||||
self._old_scroll_pos = (-1, -1)
|
||||
self.open_target = usertypes.ClickTarget.normal
|
||||
self._force_open_target = None
|
||||
self._zoom = None
|
||||
self._has_ssl_errors = False
|
||||
self.keep_icon = False
|
||||
self.search_text = None
|
||||
self.search_flags = 0
|
||||
self.selection_enabled = False
|
||||
self.init_neighborlist()
|
||||
self._set_bg_color()
|
||||
cfg = objreg.get('config')
|
||||
cfg.changed.connect(self.init_neighborlist)
|
||||
# For some reason, this signal doesn't get disconnected automatically
|
||||
@@ -125,44 +118,36 @@ class WebView(QWebView):
|
||||
window=win_id)
|
||||
tab_registry[self.tab_id] = self
|
||||
objreg.register('webview', self, registry=self.registry)
|
||||
page = self._init_page()
|
||||
page = webpage.BrowserPage(win_id, self.tab_id, self)
|
||||
self.setPage(page)
|
||||
hintmanager = hints.HintManager(win_id, self.tab_id, self)
|
||||
hintmanager.mouse_event.connect(self.on_mouse_event)
|
||||
hintmanager.start_hinting.connect(page.on_start_hinting)
|
||||
hintmanager.stop_hinting.connect(page.on_stop_hinting)
|
||||
hintmanager.set_open_target.connect(self.set_force_open_target)
|
||||
objreg.register('hintmanager', hintmanager, registry=self.registry)
|
||||
mode_manager = objreg.get('mode-manager', scope='window',
|
||||
window=win_id)
|
||||
mode_manager.entered.connect(self.on_mode_entered)
|
||||
mode_manager.left.connect(self.on_mode_left)
|
||||
page.linkHovered.connect(self.linkHovered)
|
||||
page.mainFrame().loadStarted.connect(self.on_load_started)
|
||||
self.urlChanged.connect(self.on_url_changed)
|
||||
page.mainFrame().loadFinished.connect(self.on_load_finished)
|
||||
self.loadProgress.connect(lambda p: setattr(self, 'progress', p))
|
||||
self.page().statusBarMessage.connect(
|
||||
lambda msg: setattr(self, 'statusbar_message', msg))
|
||||
self.page().networkAccessManager().sslErrors.connect(
|
||||
lambda *args: setattr(self, '_has_ssl_errors', True))
|
||||
self.viewing_source = False
|
||||
self.setZoomFactor(float(config.get('ui', 'default-zoom')) / 100)
|
||||
self._default_zoom_changed = False
|
||||
if config.get('input', 'rocker-gestures'):
|
||||
self.setContextMenuPolicy(Qt.PreventContextMenu)
|
||||
self.urlChanged.connect(self.on_url_changed)
|
||||
self.loadProgress.connect(lambda p: setattr(self, 'progress', p))
|
||||
objreg.get('config').changed.connect(self.on_config_changed)
|
||||
|
||||
def _init_page(self):
|
||||
"""Initialize the QWebPage used by this view."""
|
||||
page = webpage.BrowserPage(self.win_id, self.tab_id, self)
|
||||
self.setPage(page)
|
||||
page.linkHovered.connect(self.linkHovered)
|
||||
page.mainFrame().loadStarted.connect(self.on_load_started)
|
||||
page.mainFrame().loadFinished.connect(self.on_load_finished)
|
||||
page.statusBarMessage.connect(
|
||||
lambda msg: setattr(self, 'statusbar_message', msg))
|
||||
page.networkAccessManager().sslErrors.connect(
|
||||
lambda *args: setattr(self, '_has_ssl_errors', True))
|
||||
return page
|
||||
|
||||
def __repr__(self):
|
||||
url = utils.elide(self.url().toDisplayString(), 50)
|
||||
return utils.get_repr(self, tab_id=self.tab_id, url=url)
|
||||
|
||||
def __del__(self):
|
||||
# Explicitly releasing the page here seems to prevent some segfaults
|
||||
# Explicitely releasing the page here seems to prevent some segfaults
|
||||
# when quitting.
|
||||
# Copied from:
|
||||
# https://code.google.com/p/webscraping/source/browse/webkit.py#325
|
||||
@@ -182,15 +167,6 @@ class WebView(QWebView):
|
||||
self.load_status = val
|
||||
self.load_status_changed.emit(val.name)
|
||||
|
||||
def _set_bg_color(self):
|
||||
"""Set the webpage background color as configured."""
|
||||
col = config.get('colors', 'webpage.bg')
|
||||
palette = self.palette()
|
||||
if col is None:
|
||||
col = self.style().standardPalette().color(QPalette.Base)
|
||||
palette.setColor(QPalette.Base, col)
|
||||
self.setPalette(palette)
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def on_config_changed(self, section, option):
|
||||
"""Reinitialize the zoom neighborlist if related config changed."""
|
||||
@@ -200,13 +176,6 @@ class WebView(QWebView):
|
||||
100)
|
||||
self._default_zoom_changed = False
|
||||
self.init_neighborlist()
|
||||
elif section == 'input' and option == 'rocker-gestures':
|
||||
if config.get('input', 'rocker-gestures'):
|
||||
self.setContextMenuPolicy(Qt.PreventContextMenu)
|
||||
else:
|
||||
self.setContextMenuPolicy(Qt.DefaultContextMenu)
|
||||
elif section == 'colors' and option == 'webpage.bg':
|
||||
self._set_bg_color()
|
||||
|
||||
def init_neighborlist(self):
|
||||
"""Initialize the _zoom neighborlist."""
|
||||
@@ -221,19 +190,19 @@ class WebView(QWebView):
|
||||
Args:
|
||||
e: The QMouseEvent.
|
||||
"""
|
||||
if e.button() in (Qt.XButton1, Qt.LeftButton):
|
||||
# Back button on mice which have it, or rocker gesture
|
||||
if e.button() == Qt.XButton1:
|
||||
# Back button on mice which have it.
|
||||
if self.page().history().canGoBack():
|
||||
self.back()
|
||||
else:
|
||||
message.error(self.win_id, "At beginning of history.",
|
||||
message.error(self._win_id, "At beginning of history.",
|
||||
immediately=True)
|
||||
elif e.button() in (Qt.XButton2, Qt.RightButton):
|
||||
# Forward button on mice which have it, or rocker gesture
|
||||
elif e.button() == Qt.XButton2:
|
||||
# Forward button on mice which have it.
|
||||
if self.page().history().canGoForward():
|
||||
self.forward()
|
||||
else:
|
||||
message.error(self.win_id, "At end of history.",
|
||||
message.error(self._win_id, "At end of history.",
|
||||
immediately=True)
|
||||
|
||||
def _mousepress_insertmode(self, e):
|
||||
@@ -256,7 +225,7 @@ class WebView(QWebView):
|
||||
# me, but it works this way.
|
||||
hitresult = frame.hitTestContent(pos)
|
||||
if hitresult.isNull():
|
||||
# For some reason, the whole hit result can be null sometimes (e.g.
|
||||
# For some reason, the whole hitresult can be null sometimes (e.g.
|
||||
# on doodle menu links). If this is the case, we schedule a check
|
||||
# later (in mouseReleaseEvent) which uses webelem.focus_elem.
|
||||
log.mouse.debug("Hitresult is null!")
|
||||
@@ -265,7 +234,7 @@ class WebView(QWebView):
|
||||
try:
|
||||
elem = webelem.WebElementWrapper(hitresult.element())
|
||||
except webelem.IsNullError:
|
||||
# For some reason, the hit result element can be a null element
|
||||
# For some reason, the hitresult element can be a null element
|
||||
# sometimes (e.g. when clicking the timetable fields on
|
||||
# http://www.sbb.ch/ ). If this is the case, we schedule a check
|
||||
# later (in mouseReleaseEvent) which uses webelem.focus_elem.
|
||||
@@ -275,12 +244,12 @@ class WebView(QWebView):
|
||||
if ((hitresult.isContentEditable() and elem.is_writable()) or
|
||||
elem.is_editable()):
|
||||
log.mouse.debug("Clicked editable element!")
|
||||
modeman.enter(self.win_id, usertypes.KeyMode.insert, 'click',
|
||||
modeman.enter(self._win_id, usertypes.KeyMode.insert, 'click',
|
||||
only_if_normal=True)
|
||||
else:
|
||||
log.mouse.debug("Clicked non-editable element!")
|
||||
if config.get('input', 'auto-leave-insert-mode'):
|
||||
modeman.maybe_leave(self.win_id, usertypes.KeyMode.insert,
|
||||
modeman.maybe_leave(self._win_id, usertypes.KeyMode.insert,
|
||||
'click')
|
||||
|
||||
def mouserelease_insertmode(self):
|
||||
@@ -290,17 +259,17 @@ class WebView(QWebView):
|
||||
self._check_insertmode = False
|
||||
try:
|
||||
elem = webelem.focus_elem(self.page().currentFrame())
|
||||
except (webelem.IsNullError, RuntimeError):
|
||||
log.mouse.warning("Element/page vanished!")
|
||||
except webelem.IsNullError:
|
||||
log.mouse.warning("Element vanished!")
|
||||
return
|
||||
if elem.is_editable():
|
||||
log.mouse.debug("Clicked editable element (delayed)!")
|
||||
modeman.enter(self.win_id, usertypes.KeyMode.insert,
|
||||
modeman.enter(self._win_id, usertypes.KeyMode.insert,
|
||||
'click-delayed', only_if_normal=True)
|
||||
else:
|
||||
log.mouse.debug("Clicked non-editable element (delayed)!")
|
||||
if config.get('input', 'auto-leave-insert-mode'):
|
||||
modeman.maybe_leave(self.win_id, usertypes.KeyMode.insert,
|
||||
modeman.maybe_leave(self._win_id, usertypes.KeyMode.insert,
|
||||
'click-delayed')
|
||||
|
||||
def _mousepress_opentarget(self, e):
|
||||
@@ -309,30 +278,41 @@ class WebView(QWebView):
|
||||
Args:
|
||||
e: The QMouseEvent.
|
||||
"""
|
||||
if e.button() == Qt.MidButton or e.modifiers() & Qt.ControlModifier:
|
||||
if self._force_open_target is not None:
|
||||
self.open_target = self._force_open_target
|
||||
self._force_open_target = None
|
||||
log.mouse.debug("Setting force target: {}".format(
|
||||
self.open_target))
|
||||
elif (e.button() == Qt.MidButton or
|
||||
e.modifiers() & Qt.ControlModifier):
|
||||
background_tabs = config.get('tabs', 'background-tabs')
|
||||
if e.modifiers() & Qt.ShiftModifier:
|
||||
background_tabs = not background_tabs
|
||||
if background_tabs:
|
||||
target = usertypes.ClickTarget.tab_bg
|
||||
self.open_target = usertypes.ClickTarget.tab_bg
|
||||
else:
|
||||
target = usertypes.ClickTarget.tab
|
||||
self.page().open_target = target
|
||||
log.mouse.debug("Middle click, setting target: {}".format(target))
|
||||
self.open_target = usertypes.ClickTarget.tab
|
||||
log.mouse.debug("Middle click, setting target: {}".format(
|
||||
self.open_target))
|
||||
else:
|
||||
self.page().open_target = usertypes.ClickTarget.normal
|
||||
self.open_target = usertypes.ClickTarget.normal
|
||||
log.mouse.debug("Normal click, setting normal target")
|
||||
|
||||
def shutdown(self):
|
||||
"""Shut down the webview."""
|
||||
self.shutting_down.emit()
|
||||
# We disable javascript because that prevents some segfaults when
|
||||
# quitting it seems.
|
||||
log.destroy.debug("Shutting down {!r}.".format(self))
|
||||
settings = self.settings()
|
||||
settings.setAttribute(QWebSettings.JavascriptEnabled, False)
|
||||
self.stop()
|
||||
self.page().shutdown()
|
||||
download_manager = objreg.get('download-manager', scope='window',
|
||||
window=self._win_id)
|
||||
nam = self.page().networkAccessManager()
|
||||
if download_manager.has_downloads_with_nam(nam):
|
||||
nam.setParent(download_manager)
|
||||
else:
|
||||
nam.shutdown()
|
||||
|
||||
def openurl(self, url):
|
||||
"""Open a URL in the browser.
|
||||
@@ -368,8 +348,9 @@ class WebView(QWebView):
|
||||
if fuzzyval:
|
||||
self._zoom.fuzzyval = int(perc)
|
||||
if perc < 0:
|
||||
raise ValueError("Can't zoom {}%!".format(perc))
|
||||
raise cmdexc.CommandError("Can't zoom {}%!".format(perc))
|
||||
self.setZoomFactor(float(perc) / 100)
|
||||
message.info(self._win_id, "Zoom level: {}%".format(perc))
|
||||
self._default_zoom_changed = True
|
||||
|
||||
def zoom(self, offset):
|
||||
@@ -377,13 +358,9 @@ class WebView(QWebView):
|
||||
|
||||
Args:
|
||||
offset: The offset in the zoom level list.
|
||||
|
||||
Return:
|
||||
The new zoom percentage.
|
||||
"""
|
||||
level = self._zoom.getitem(offset)
|
||||
self.zoom_perc(level, fuzzyval=False)
|
||||
return level
|
||||
|
||||
@pyqtSlot('QUrl')
|
||||
def on_url_changed(self, url):
|
||||
@@ -394,12 +371,10 @@ class WebView(QWebView):
|
||||
if url.isValid():
|
||||
self.cur_url = url
|
||||
self.url_text_changed.emit(url.toDisplayString())
|
||||
if not self.title():
|
||||
self.titleChanged.emit(self.url().toDisplayString())
|
||||
|
||||
@pyqtSlot('QMouseEvent')
|
||||
def on_mouse_event(self, evt):
|
||||
"""Post a new mouse event from a hintmanager."""
|
||||
"""Post a new mouseevent from a hintmanager."""
|
||||
log.modes.debug("Hint triggered, focusing {!r}".format(self))
|
||||
self.setFocus()
|
||||
QApplication.postEvent(self, evt)
|
||||
@@ -414,29 +389,23 @@ class WebView(QWebView):
|
||||
|
||||
@pyqtSlot()
|
||||
def on_load_finished(self):
|
||||
"""Handle a finished page load.
|
||||
"""Handle auto-insert-mode after loading finished.
|
||||
|
||||
We don't take loadFinished's ok argument here as it always seems to be
|
||||
true when the QWebPage has an ErrorPageExtension implemented.
|
||||
See https://github.com/The-Compiler/qutebrowser/issues/84
|
||||
"""
|
||||
ok = not self.page().error_occurred
|
||||
ok = not self.page().error_occured
|
||||
if ok and not self._has_ssl_errors:
|
||||
self._set_load_status(LoadStatus.success)
|
||||
elif ok:
|
||||
self._set_load_status(LoadStatus.warn)
|
||||
else:
|
||||
self._set_load_status(LoadStatus.error)
|
||||
if not self.title():
|
||||
self.titleChanged.emit(self.url().toDisplayString())
|
||||
self._handle_auto_insert_mode(ok)
|
||||
|
||||
def _handle_auto_insert_mode(self, ok):
|
||||
"""Handle auto-insert-mode after loading finished."""
|
||||
if not config.get('input', 'auto-insert-mode'):
|
||||
return
|
||||
mode_manager = objreg.get('mode-manager', scope='window',
|
||||
window=self.win_id)
|
||||
window=self._win_id)
|
||||
cur_mode = mode_manager.mode
|
||||
if cur_mode == usertypes.KeyMode.insert or not ok:
|
||||
return
|
||||
@@ -448,7 +417,7 @@ class WebView(QWebView):
|
||||
return
|
||||
log.modes.debug("focus element: {}".format(repr(elem)))
|
||||
if elem.is_editable():
|
||||
modeman.enter(self.win_id, usertypes.KeyMode.insert,
|
||||
modeman.enter(self._win_id, usertypes.KeyMode.insert,
|
||||
'load finished', only_if_normal=True)
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
@@ -459,25 +428,6 @@ class WebView(QWebView):
|
||||
log.webview.debug("Ignoring focus because mode {} was "
|
||||
"entered.".format(mode))
|
||||
self.setFocusPolicy(Qt.NoFocus)
|
||||
elif mode == usertypes.KeyMode.caret:
|
||||
settings = self.settings()
|
||||
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True)
|
||||
self.selection_enabled = bool(self.page().selectedText())
|
||||
|
||||
if self.isVisible():
|
||||
# Sometimes the caret isn't immediately visible, but unfocusing
|
||||
# and refocusing it fixes that.
|
||||
self.clearFocus()
|
||||
self.setFocus(Qt.OtherFocusReason)
|
||||
|
||||
# Move the caret to the first element in the viewport if there
|
||||
# isn't any text which is already selected.
|
||||
#
|
||||
# Note: We can't use hasSelection() here, as that's always
|
||||
# true in caret mode.
|
||||
if not self.page().selectedText():
|
||||
self.page().currentFrame().evaluateJavaScript(
|
||||
utils.read_file('javascript/position_caret.js'))
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def on_mode_left(self, mode):
|
||||
@@ -486,58 +436,18 @@ class WebView(QWebView):
|
||||
usertypes.KeyMode.yesno):
|
||||
log.webview.debug("Restoring focus policy because mode {} was "
|
||||
"left.".format(mode))
|
||||
elif mode == usertypes.KeyMode.caret:
|
||||
settings = self.settings()
|
||||
if settings.testAttribute(QWebSettings.CaretBrowsingEnabled):
|
||||
if self.selection_enabled and self.hasSelection():
|
||||
# Remove selection if it exists
|
||||
self.triggerPageAction(QWebPage.MoveToNextChar)
|
||||
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, False)
|
||||
self.selection_enabled = False
|
||||
|
||||
self.setFocusPolicy(Qt.WheelFocus)
|
||||
|
||||
def search(self, text, flags):
|
||||
"""Search for text in the current page.
|
||||
@pyqtSlot(str)
|
||||
def set_force_open_target(self, target):
|
||||
"""Change the forced link target. Setter for _force_open_target.
|
||||
|
||||
Args:
|
||||
text: The text to search for.
|
||||
flags: The QWebPage::FindFlags.
|
||||
target: A string to set self._force_open_target to.
|
||||
"""
|
||||
log.webview.debug("Searching with text '{}' and flags "
|
||||
"0x{:04x}.".format(text, int(flags)))
|
||||
old_scroll_pos = self.scroll_pos
|
||||
flags = QWebPage.FindFlags(flags)
|
||||
found = self.findText(text, flags)
|
||||
backward = flags & QWebPage.FindBackward
|
||||
|
||||
if not found and not flags & QWebPage.HighlightAllOccurrences and text:
|
||||
# User disabled wrapping; but findText() just returns False. If we
|
||||
# have a selection, we know there's a match *somewhere* on the page
|
||||
if (not flags & QWebPage.FindWrapsAroundDocument and
|
||||
self.hasSelection()):
|
||||
if not backward:
|
||||
message.warning(self.win_id, "Search hit BOTTOM without "
|
||||
"match for: {}".format(text),
|
||||
immediately=True)
|
||||
else:
|
||||
message.warning(self.win_id, "Search hit TOP without "
|
||||
"match for: {}".format(text),
|
||||
immediately=True)
|
||||
else:
|
||||
message.error(self.win_id, "Text '{}' not found on "
|
||||
"page!".format(text), immediately=True)
|
||||
else:
|
||||
def check_scroll_pos():
|
||||
"""Check if the scroll position got smaller and show info."""
|
||||
if not backward and self.scroll_pos < old_scroll_pos:
|
||||
message.info(self.win_id, "Search hit BOTTOM, continuing "
|
||||
"at TOP", immediately=True)
|
||||
elif backward and self.scroll_pos > old_scroll_pos:
|
||||
message.info(self.win_id, "Search hit TOP, continuing at "
|
||||
"BOTTOM", immediately=True)
|
||||
# We first want QWebPage to refresh.
|
||||
QTimer.singleShot(0, check_scroll_pos)
|
||||
t = getattr(usertypes.ClickTarget, target)
|
||||
log.webview.debug("Setting force target to {}/{}".format(target, t))
|
||||
self._force_open_target = t
|
||||
|
||||
def createWindow(self, wintype):
|
||||
"""Called by Qt when a page wants to create a new window.
|
||||
@@ -562,7 +472,7 @@ class WebView(QWebView):
|
||||
log.webview.warning("WebModalDialog requested, but we don't "
|
||||
"support that!")
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=self.win_id)
|
||||
window=self._win_id)
|
||||
return tabbed_browser.tabopen(background=False)
|
||||
|
||||
def paintEvent(self, e):
|
||||
@@ -597,7 +507,7 @@ class WebView(QWebView):
|
||||
|
||||
This does the following things:
|
||||
- Check if a link was clicked with the middle button or Ctrl and
|
||||
set the page's open_target attribute accordingly.
|
||||
set the open_target attribute accordingly.
|
||||
- Emit the editable_elem_selected signal if an editable element was
|
||||
clicked.
|
||||
|
||||
@@ -607,10 +517,7 @@ class WebView(QWebView):
|
||||
Return:
|
||||
The superclass return value.
|
||||
"""
|
||||
is_rocker_gesture = (config.get('input', 'rocker-gestures') and
|
||||
e.buttons() == Qt.LeftButton | Qt.RightButton)
|
||||
|
||||
if e.button() in (Qt.XButton1, Qt.XButton2) or is_rocker_gesture:
|
||||
if e.button() in (Qt.XButton1, Qt.XButton2):
|
||||
self._mousepress_backforward(e)
|
||||
super().mousePressEvent(e)
|
||||
return
|
||||
@@ -624,30 +531,3 @@ class WebView(QWebView):
|
||||
# We want to make sure we check the focus element after the WebView is
|
||||
# updated completely.
|
||||
QTimer.singleShot(0, self.mouserelease_insertmode)
|
||||
|
||||
def contextMenuEvent(self, e):
|
||||
"""Save a reference to the context menu so we can close it."""
|
||||
menu = self.page().createStandardContextMenu()
|
||||
self.shutting_down.connect(menu.close)
|
||||
modeman.instance(self.win_id).entered.connect(menu.close)
|
||||
menu.exec_(e.globalPos())
|
||||
|
||||
def wheelEvent(self, e):
|
||||
"""Zoom on Ctrl-Mousewheel.
|
||||
|
||||
Args:
|
||||
e: The QWheelEvent.
|
||||
"""
|
||||
if e.modifiers() & Qt.ControlModifier:
|
||||
e.accept()
|
||||
divider = config.get('input', 'mouse-zoom-divider')
|
||||
factor = self.zoomFactor() + e.angleDelta().y() / divider
|
||||
if factor < 0:
|
||||
return
|
||||
perc = int(100 * factor)
|
||||
message.info(self.win_id, "Zoom level: {}%".format(perc))
|
||||
self._zoom.fuzzyval = perc
|
||||
self.setZoomFactor(factor)
|
||||
self._default_zoom_changed = True
|
||||
else:
|
||||
super().wheelEvent(e)
|
||||
|
||||
@@ -86,6 +86,7 @@ class ArgumentParser(argparse.ArgumentParser):
|
||||
|
||||
def enum_getter(enum):
|
||||
"""Function factory to get an enum getter."""
|
||||
|
||||
def _get_enum_item(key):
|
||||
"""Helper function to get an enum item.
|
||||
|
||||
@@ -103,6 +104,7 @@ def enum_getter(enum):
|
||||
|
||||
def multitype_conv(tpl):
|
||||
"""Function factory to get a type converter for a choice of types."""
|
||||
|
||||
def _convert(value):
|
||||
"""Convert a value according to an iterable of possible arg types."""
|
||||
for typ in set(tpl):
|
||||
|
||||
@@ -32,7 +32,7 @@ class CommandError(Exception):
|
||||
|
||||
class CommandMetaError(Exception):
|
||||
|
||||
"""Common base class for exceptions occurring before a command is run."""
|
||||
"""Common base class for exceptions occuring before a command is run."""
|
||||
|
||||
|
||||
class NoSuchCommandError(CommandMetaError):
|
||||
|
||||
@@ -23,7 +23,7 @@ Module attributes:
|
||||
cmd_dict: A mapping from command-strings to command objects.
|
||||
"""
|
||||
|
||||
from qutebrowser.utils import qtutils, log
|
||||
from qutebrowser.utils import usertypes, qtutils, log
|
||||
from qutebrowser.commands import command, cmdexc
|
||||
|
||||
cmd_dict = {}
|
||||
@@ -99,11 +99,22 @@ class register: # pylint: disable=invalid-name
|
||||
|
||||
Attributes:
|
||||
_instance: The object from the object registry to be used as "self".
|
||||
_scope: The scope to get _instance for.
|
||||
_name: The name (as string) or names (as list) of the command.
|
||||
_kwargs: The arguments to pass to Command.
|
||||
_maxsplit: The maxium amounts of splits to do for the commandline, or
|
||||
None.
|
||||
_hide: Whether to hide the command or not.
|
||||
_completion: Which completion to use for arguments, as a list of
|
||||
strings.
|
||||
_modes/_not_modes: List of modes to use/not use.
|
||||
_needs_js: If javascript is needed for this command.
|
||||
_debug: Whether this is a debugging command (only shown with --debug).
|
||||
_ignore_args: Whether to ignore the arguments of the function.
|
||||
"""
|
||||
|
||||
def __init__(self, *, instance=None, name=None, **kwargs):
|
||||
def __init__(self, instance=None, name=None, maxsplit=None, hide=False,
|
||||
completion=None, modes=None, not_modes=None, needs_js=False,
|
||||
debug=False, ignore_args=False, scope='global'):
|
||||
"""Save decorator arguments.
|
||||
|
||||
Gets called on parse-time with the decorator arguments.
|
||||
@@ -111,14 +122,33 @@ class register: # pylint: disable=invalid-name
|
||||
Args:
|
||||
See class attributes.
|
||||
"""
|
||||
self._instance = instance
|
||||
# pylint: disable=too-many-arguments
|
||||
if modes is not None and not_modes is not None:
|
||||
raise ValueError("Only modes or not_modes can be given!")
|
||||
self._name = name
|
||||
self._kwargs = kwargs
|
||||
self._maxsplit = maxsplit
|
||||
self._hide = hide
|
||||
self._instance = instance
|
||||
self._scope = scope
|
||||
self._completion = completion
|
||||
self._modes = modes
|
||||
self._not_modes = not_modes
|
||||
self._needs_js = needs_js
|
||||
self._debug = debug
|
||||
self._ignore_args = ignore_args
|
||||
if modes is not None:
|
||||
for m in modes:
|
||||
if not isinstance(m, usertypes.KeyMode):
|
||||
raise TypeError("Mode {} is no KeyMode member!".format(m))
|
||||
if not_modes is not None:
|
||||
for m in not_modes:
|
||||
if not isinstance(m, usertypes.KeyMode):
|
||||
raise TypeError("Mode {} is no KeyMode member!".format(m))
|
||||
|
||||
def _get_names(self, func):
|
||||
"""Get the name(s) which should be used for the current command.
|
||||
|
||||
If the name hasn't been overridden explicitly, the function name is
|
||||
If the name hasn't been overridden explicitely, the function name is
|
||||
transformed.
|
||||
|
||||
If it has been set, it can either be a string which is
|
||||
@@ -157,8 +187,12 @@ class register: # pylint: disable=invalid-name
|
||||
for name in names:
|
||||
if name in cmd_dict:
|
||||
raise ValueError("{} is already registered!".format(name))
|
||||
cmd = command.Command(name=names[0], instance=self._instance,
|
||||
handler=func, **self._kwargs)
|
||||
cmd = command.Command(
|
||||
name=names[0], maxsplit=self._maxsplit, hide=self._hide,
|
||||
instance=self._instance, scope=self._scope,
|
||||
completion=self._completion, modes=self._modes,
|
||||
not_modes=self._not_modes, needs_js=self._needs_js,
|
||||
is_debug=self._debug, ignore_args=self._ignore_args, handler=func)
|
||||
for name in names:
|
||||
cmd_dict[name] = cmd
|
||||
aliases += names[1:]
|
||||
|
||||
@@ -25,13 +25,7 @@ import collections
|
||||
from PyQt5.QtWebKit import QWebSettings
|
||||
|
||||
from qutebrowser.commands import cmdexc, argparser
|
||||
from qutebrowser.utils import log, utils, message, docutils, objreg, usertypes
|
||||
from qutebrowser.utils import debug as debug_utils
|
||||
|
||||
|
||||
def arg_name(name):
|
||||
"""Get the name an argument should have based on its Python name."""
|
||||
return name.rstrip('_').replace('_', '-')
|
||||
from qutebrowser.utils import log, utils, message, debug, docutils, objreg
|
||||
|
||||
|
||||
class Command:
|
||||
@@ -43,17 +37,16 @@ class Command:
|
||||
maxsplit: The maximum amount of splits to do for the commandline, or
|
||||
None.
|
||||
hide: Whether to hide the arguments or not.
|
||||
deprecated: False, or a string to describe why a command is deprecated.
|
||||
desc: The description of the command.
|
||||
handler: The handler function to call.
|
||||
completion: Completions to use for arguments, as a list of strings.
|
||||
debug: Whether this is a debugging command (only shown with --debug).
|
||||
parser: The ArgumentParser to use to parse this command.
|
||||
count_arg: The name of the count parameter, or None.
|
||||
win_id_arg: The name of the win_id parameter, or None.
|
||||
special_params: A dict with the names of the special parameters as
|
||||
values.
|
||||
flags_with_args: A list of flags which take an argument.
|
||||
no_cmd_split: If true, ';;' to split sub-commands is ignored.
|
||||
_type_conv: A mapping of conversion functions for arguments.
|
||||
_name_conv: A mapping of argument names to parameter names.
|
||||
_needs_js: Whether the command needs javascript enabled
|
||||
_modes: The modes the command can be executed in.
|
||||
_not_modes: The modes the command can not be executed in.
|
||||
@@ -66,45 +59,26 @@ class Command:
|
||||
"""
|
||||
|
||||
AnnotationInfo = collections.namedtuple('AnnotationInfo',
|
||||
['kwargs', 'type', 'flag', 'hide',
|
||||
'metavar'])
|
||||
['kwargs', 'type', 'name', 'flag',
|
||||
'special'])
|
||||
|
||||
def __init__(self, *, handler, name, instance=None, maxsplit=None,
|
||||
hide=False, completion=None, modes=None, not_modes=None,
|
||||
needs_js=False, debug=False, ignore_args=False,
|
||||
deprecated=False, no_cmd_split=False, scope='global',
|
||||
count=None, win_id=None):
|
||||
def __init__(self, name, maxsplit, hide, instance, completion, modes,
|
||||
not_modes, needs_js, is_debug, ignore_args,
|
||||
handler, scope):
|
||||
# I really don't know how to solve this in a better way, I tried.
|
||||
# pylint: disable=too-many-arguments,too-many-locals
|
||||
if modes is not None and not_modes is not None:
|
||||
raise ValueError("Only modes or not_modes can be given!")
|
||||
if modes is not None:
|
||||
for m in modes:
|
||||
if not isinstance(m, usertypes.KeyMode):
|
||||
raise TypeError("Mode {} is no KeyMode member!".format(m))
|
||||
if not_modes is not None:
|
||||
for m in not_modes:
|
||||
if not isinstance(m, usertypes.KeyMode):
|
||||
raise TypeError("Mode {} is no KeyMode member!".format(m))
|
||||
if scope != 'global' and instance is None:
|
||||
raise ValueError("Setting scope without setting instance makes "
|
||||
"no sense!")
|
||||
self.name = name
|
||||
self.maxsplit = maxsplit
|
||||
self.hide = hide
|
||||
self.deprecated = deprecated
|
||||
self._instance = instance
|
||||
self.completion = completion
|
||||
self._modes = modes
|
||||
self._not_modes = not_modes
|
||||
self._scope = scope
|
||||
self._needs_js = needs_js
|
||||
self.debug = debug
|
||||
self.debug = is_debug
|
||||
self.ignore_args = ignore_args
|
||||
self.handler = handler
|
||||
self.no_cmd_split = no_cmd_split
|
||||
self.count_arg = count
|
||||
self.win_id_arg = win_id
|
||||
self.docparser = docutils.DocstringParser(handler)
|
||||
self.parser = argparser.ArgumentParser(
|
||||
name, description=self.docparser.short_desc,
|
||||
@@ -117,13 +91,12 @@ class Command:
|
||||
self.namespace = None
|
||||
self._count = None
|
||||
self.pos_args = []
|
||||
self.special_params = {'count': None, 'win_id': None}
|
||||
self.desc = None
|
||||
self.flags_with_args = []
|
||||
self._type_conv = {}
|
||||
count = self._inspect_func()
|
||||
if self.completion is not None and len(self.completion) > count:
|
||||
raise ValueError("Got {} completions, but only {} "
|
||||
"arguments!".format(len(self.completion), count))
|
||||
self._name_conv = {}
|
||||
self._inspect_func()
|
||||
|
||||
def _check_prerequisites(self, win_id):
|
||||
"""Check if the command is permitted to run currently.
|
||||
@@ -148,9 +121,6 @@ class Command:
|
||||
QWebSettings.JavascriptEnabled):
|
||||
raise cmdexc.PrerequisitesError(
|
||||
"{}: This command needs javascript enabled.".format(self.name))
|
||||
if self.deprecated:
|
||||
message.warning(win_id, '{} is deprecated - {}'.format(
|
||||
self.name, self.deprecated))
|
||||
|
||||
def _check_func(self):
|
||||
"""Make sure the function parameters don't violate any rules."""
|
||||
@@ -181,68 +151,84 @@ class Command:
|
||||
type_conv[param.name] = argparser.multitype_conv(typ)
|
||||
return type_conv
|
||||
|
||||
def _inspect_special_param(self, param):
|
||||
def _get_nameconv(self, param, annotation_info):
|
||||
"""Get a dict with a name conversion for the paraeter.
|
||||
|
||||
Args:
|
||||
param: The inspect.Parameter to handle.
|
||||
annotation_info: The AnnotationInfo tuple for the parameter.
|
||||
"""
|
||||
d = {}
|
||||
if annotation_info.name is not None:
|
||||
d[param.name] = annotation_info.name
|
||||
return d
|
||||
|
||||
def _inspect_special_param(self, param, annotation_info):
|
||||
"""Check if the given parameter is a special one.
|
||||
|
||||
Args:
|
||||
param: The inspect.Parameter to handle.
|
||||
annotation_info: The AnnotationInfo tuple for the parameter.
|
||||
|
||||
Return:
|
||||
True if the parameter is special, False otherwise.
|
||||
"""
|
||||
if param.name == self.count_arg:
|
||||
special = annotation_info.special
|
||||
if special == 'count':
|
||||
if self.special_params['count'] is not None:
|
||||
raise ValueError("Registered multiple parameters ({}/{}) as "
|
||||
"count!".format(self.special_params['count'],
|
||||
param.name))
|
||||
if param.default is inspect.Parameter.empty:
|
||||
raise TypeError("{}: handler has count parameter "
|
||||
"without default!".format(self.name))
|
||||
self.special_params['count'] = param.name
|
||||
return True
|
||||
elif param.name == self.win_id_arg:
|
||||
elif special == 'win_id':
|
||||
if self.special_params['win_id'] is not None:
|
||||
raise ValueError("Registered multiple parameters ({}/{}) as "
|
||||
"win_id!".format(
|
||||
self.special_params['win_id'],
|
||||
param.name))
|
||||
self.special_params['win_id'] = param.name
|
||||
return True
|
||||
elif special is None:
|
||||
return False
|
||||
else:
|
||||
raise ValueError("{}: Invalid value '{}' for 'special' "
|
||||
"annotation!".format(self.name, special))
|
||||
|
||||
def _inspect_func(self):
|
||||
"""Inspect the function to get useful informations from it.
|
||||
|
||||
Sets instance attributes (desc, type_conv, name_conv) based on the
|
||||
informations.
|
||||
|
||||
Return:
|
||||
How many user-visible arguments the command has.
|
||||
"""
|
||||
signature = inspect.signature(self.handler)
|
||||
doc = inspect.getdoc(self.handler)
|
||||
arg_count = 0
|
||||
if doc is not None:
|
||||
self.desc = doc.splitlines()[0].strip()
|
||||
else:
|
||||
self.desc = ""
|
||||
|
||||
if (self.count_arg is not None and
|
||||
self.count_arg not in signature.parameters):
|
||||
raise ValueError("count parameter {} does not exist!".format(
|
||||
self.count_arg))
|
||||
if (self.win_id_arg is not None and
|
||||
self.win_id_arg not in signature.parameters):
|
||||
raise ValueError("win_id parameter {} does not exist!".format(
|
||||
self.win_id_arg))
|
||||
|
||||
if not self.ignore_args:
|
||||
for param in signature.parameters.values():
|
||||
annotation_info = self._parse_annotation(param)
|
||||
if param.name == 'self':
|
||||
continue
|
||||
if self._inspect_special_param(param):
|
||||
if self._inspect_special_param(param, annotation_info):
|
||||
continue
|
||||
arg_count += 1
|
||||
typ = self._get_type(param, annotation_info)
|
||||
kwargs = self._param_to_argparse_kwargs(param, annotation_info)
|
||||
args = self._param_to_argparse_args(param, annotation_info)
|
||||
self._type_conv.update(self._get_typeconv(param, typ))
|
||||
callsig = debug_utils.format_call(
|
||||
self._name_conv.update(
|
||||
self._get_nameconv(param, annotation_info))
|
||||
callsig = debug.format_call(
|
||||
self.parser.add_argument, args, kwargs,
|
||||
full=False)
|
||||
log.commands.vdebug('Adding arg {} of type {} -> {}'.format(
|
||||
param.name, typ, callsig))
|
||||
self.parser.add_argument(*args, **kwargs)
|
||||
return arg_count
|
||||
|
||||
def _param_to_argparse_kwargs(self, param, annotation_info):
|
||||
"""Get argparse keyword arguments for a parameter.
|
||||
@@ -262,13 +248,11 @@ class Command:
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
kwargs['dest'] = param.name
|
||||
|
||||
if isinstance(typ, tuple):
|
||||
kwargs['metavar'] = annotation_info.metavar or param.name
|
||||
pass
|
||||
elif utils.is_enum(typ):
|
||||
kwargs['choices'] = [arg_name(e.name) for e in typ]
|
||||
kwargs['metavar'] = annotation_info.metavar or param.name
|
||||
kwargs['choices'] = [e.name.replace('_', '-') for e in typ]
|
||||
kwargs['metavar'] = param.name
|
||||
elif typ is bool:
|
||||
kwargs['action'] = 'store_true'
|
||||
elif typ is not None:
|
||||
@@ -295,8 +279,8 @@ class Command:
|
||||
A list of args.
|
||||
"""
|
||||
args = []
|
||||
name = arg_name(param.name)
|
||||
shortname = annotation_info.flag or name[0]
|
||||
name = annotation_info.name or param.name
|
||||
shortname = annotation_info.flag or param.name[0]
|
||||
if len(shortname) != 1:
|
||||
raise ValueError("Flag '{}' of parameter {} (command {}) must be "
|
||||
"exactly 1 char!".format(shortname, name,
|
||||
@@ -308,11 +292,11 @@ class Command:
|
||||
args.append(long_flag)
|
||||
args.append(short_flag)
|
||||
self.opt_args[param.name] = long_flag, short_flag
|
||||
if typ is not bool:
|
||||
self.flags_with_args += [short_flag, long_flag]
|
||||
if param.kind == inspect.Parameter.KEYWORD_ONLY:
|
||||
self.flags_with_args.append(param.name)
|
||||
else:
|
||||
if not annotation_info.hide:
|
||||
self.pos_args.append((param.name, name))
|
||||
args.append(name)
|
||||
self.pos_args.append((param.name, name))
|
||||
return args
|
||||
|
||||
def _parse_annotation(self, param):
|
||||
@@ -329,12 +313,12 @@ class Command:
|
||||
flag: The short name/flag if overridden.
|
||||
name: The long name if overridden.
|
||||
"""
|
||||
info = {'kwargs': {}, 'type': None, 'flag': None, 'hide': False,
|
||||
'metavar': None}
|
||||
info = {'kwargs': {}, 'type': None, 'flag': None, 'name': None,
|
||||
'special': None}
|
||||
if param.annotation is not inspect.Parameter.empty:
|
||||
log.commands.vdebug("Parsing annotation {}".format(
|
||||
param.annotation))
|
||||
for field in ('type', 'flag', 'name', 'hide', 'metavar'):
|
||||
for field in ('type', 'flag', 'name', 'special'):
|
||||
if field in param.annotation:
|
||||
info[field] = param.annotation[field]
|
||||
if 'nargs' in param.annotation:
|
||||
@@ -364,17 +348,10 @@ class Command:
|
||||
args: The positional argument list. Gets modified directly.
|
||||
"""
|
||||
assert param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
|
||||
if self._scope == 'global':
|
||||
tab_id = None
|
||||
if self._scope is not 'window':
|
||||
win_id = None
|
||||
elif self._scope == 'tab':
|
||||
tab_id = 'current'
|
||||
elif self._scope == 'window':
|
||||
tab_id = None
|
||||
else:
|
||||
raise ValueError("Invalid scope {}!".format(self._scope))
|
||||
obj = objreg.get(self._instance, scope=self._scope, window=win_id,
|
||||
tab=tab_id)
|
||||
obj = objreg.get(self._instance, scope=self._scope,
|
||||
window=win_id)
|
||||
args.append(obj)
|
||||
|
||||
def _get_count_arg(self, param, args, kwargs):
|
||||
@@ -414,18 +391,19 @@ class Command:
|
||||
raise TypeError("{}: invalid parameter type {} for argument "
|
||||
"{!r}!".format(self.name, param.kind, param.name))
|
||||
|
||||
def _get_param_value(self, param):
|
||||
"""Get the converted value for an inspect.Parameter."""
|
||||
value = getattr(self.namespace, param.name)
|
||||
def _get_param_name_and_value(self, param):
|
||||
"""Get the converted name and value for an inspect.Parameter."""
|
||||
name = self._name_conv.get(param.name, param.name)
|
||||
value = getattr(self.namespace, name)
|
||||
if param.name in self._type_conv:
|
||||
# We convert enum types after getting the values from
|
||||
# argparse, because argparse's choices argument is
|
||||
# processed after type conversation, which is not what we
|
||||
# want.
|
||||
value = self._type_conv[param.name](value)
|
||||
return value
|
||||
return name, value
|
||||
|
||||
def _get_call_args(self, win_id):
|
||||
def _get_call_args(self, win_id): # noqa
|
||||
"""Get arguments for a function call.
|
||||
|
||||
Args:
|
||||
@@ -434,6 +412,7 @@ class Command:
|
||||
Return:
|
||||
An (args, kwargs) tuple.
|
||||
"""
|
||||
|
||||
args = []
|
||||
kwargs = {}
|
||||
signature = inspect.signature(self.handler)
|
||||
@@ -449,22 +428,22 @@ class Command:
|
||||
# Special case for 'self'.
|
||||
self._get_self_arg(win_id, param, args)
|
||||
continue
|
||||
elif param.name == self.count_arg:
|
||||
elif param.name == self.special_params['count']:
|
||||
# Special case for count parameter.
|
||||
self._get_count_arg(param, args, kwargs)
|
||||
continue
|
||||
elif param.name == self.win_id_arg:
|
||||
elif param.name == self.special_params['win_id']:
|
||||
# Special case for win_id parameter.
|
||||
self._get_win_id_arg(win_id, param, args, kwargs)
|
||||
continue
|
||||
value = self._get_param_value(param)
|
||||
name, value = self._get_param_name_and_value(param)
|
||||
if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
|
||||
args.append(value)
|
||||
elif param.kind == inspect.Parameter.VAR_POSITIONAL:
|
||||
if value is not None:
|
||||
args += value
|
||||
elif param.kind == inspect.Parameter.KEYWORD_ONLY:
|
||||
kwargs[param.name] = value
|
||||
kwargs[name] = value
|
||||
else:
|
||||
raise TypeError("{}: Invalid parameter type {} for argument "
|
||||
"'{}'!".format(
|
||||
@@ -500,5 +479,5 @@ class Command:
|
||||
posargs, kwargs = self._get_call_args(win_id)
|
||||
self._check_prerequisites(win_id)
|
||||
log.commands.debug('Calling {}'.format(
|
||||
debug_utils.format_call(self.handler, posargs, kwargs)))
|
||||
debug.format_call(self.handler, posargs, kwargs)))
|
||||
self.handler(*posargs, **kwargs)
|
||||
|
||||
@@ -19,52 +19,151 @@
|
||||
|
||||
"""Module containing command managers (SearchRunner and CommandRunner)."""
|
||||
|
||||
import collections
|
||||
import re
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, QUrl, QObject
|
||||
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QUrl
|
||||
from PyQt5.QtWebKitWidgets import QWebPage
|
||||
|
||||
from qutebrowser.config import config, configexc
|
||||
from qutebrowser.commands import cmdexc, cmdutils
|
||||
from qutebrowser.utils import message, log, objreg, qtutils
|
||||
from qutebrowser.utils import message, log, utils, objreg
|
||||
from qutebrowser.misc import split
|
||||
|
||||
|
||||
ParseResult = collections.namedtuple('ParseResult', 'cmd, args, cmdline')
|
||||
|
||||
|
||||
def replace_variables(win_id, arglist):
|
||||
"""Utility function to replace variables like {url} in a list of args."""
|
||||
args = []
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
if '{url}' in arglist:
|
||||
try:
|
||||
url = tabbed_browser.current_url().toString(QUrl.FullyEncoded |
|
||||
QUrl.RemovePassword)
|
||||
except qtutils.QtValueError as e:
|
||||
msg = "Current URL is invalid"
|
||||
if e.reason:
|
||||
msg += " ({})".format(e.reason)
|
||||
msg += "!"
|
||||
raise cmdexc.CommandError(msg)
|
||||
for arg in arglist:
|
||||
if arg == '{url}':
|
||||
# Note we have to do this in here as the user gets an error message
|
||||
# by current_url if no URL is open yet.
|
||||
url = tabbed_browser.current_url().toString(QUrl.FullyEncoded |
|
||||
QUrl.RemovePassword)
|
||||
args.append(url)
|
||||
else:
|
||||
args.append(arg)
|
||||
return args
|
||||
|
||||
|
||||
class SearchRunner(QObject):
|
||||
|
||||
"""Run searches on webpages.
|
||||
|
||||
Attributes:
|
||||
_text: The text from the last search.
|
||||
_flags: The flags from the last search.
|
||||
|
||||
Signals:
|
||||
do_search: Emitted when a search should be started.
|
||||
arg 1: Search string.
|
||||
arg 2: Flags to use.
|
||||
"""
|
||||
|
||||
do_search = pyqtSignal(str, 'QWebPage::FindFlags')
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._text = None
|
||||
self._flags = 0
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, text=self._text, flags=self._flags)
|
||||
|
||||
def _search(self, text, rev=False):
|
||||
"""Search for a text on the current page.
|
||||
|
||||
Args:
|
||||
text: The text to search for.
|
||||
rev: Search direction, True if reverse, else False.
|
||||
"""
|
||||
if self._text is not None and self._text != text:
|
||||
# We first clear the marked text, then the highlights
|
||||
self.do_search.emit('', 0)
|
||||
self.do_search.emit('', QWebPage.HighlightAllOccurrences)
|
||||
self._text = text
|
||||
self._flags = 0
|
||||
ignore_case = config.get('general', 'ignore-case')
|
||||
if ignore_case == 'smart':
|
||||
if not text.islower():
|
||||
self._flags |= QWebPage.FindCaseSensitively
|
||||
elif not ignore_case:
|
||||
self._flags |= QWebPage.FindCaseSensitively
|
||||
if config.get('general', 'wrap-search'):
|
||||
self._flags |= QWebPage.FindWrapsAroundDocument
|
||||
if rev:
|
||||
self._flags |= QWebPage.FindBackward
|
||||
# We actually search *twice* - once to highlight everything, then again
|
||||
# to get a mark so we can navigate.
|
||||
self.do_search.emit(self._text, self._flags)
|
||||
self.do_search.emit(self._text, self._flags |
|
||||
QWebPage.HighlightAllOccurrences)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def search(self, text):
|
||||
"""Search for a text on a website.
|
||||
|
||||
Args:
|
||||
text: The text to search for.
|
||||
"""
|
||||
self._search(text)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def search_rev(self, text):
|
||||
"""Search for a text on a website in reverse direction.
|
||||
|
||||
Args:
|
||||
text: The text to search for.
|
||||
"""
|
||||
self._search(text, rev=True)
|
||||
|
||||
@cmdutils.register(instance='search-runner', hide=True, scope='window')
|
||||
def search_next(self, count: {'special': 'count'}=1):
|
||||
"""Continue the search to the ([count]th) next term.
|
||||
|
||||
Args:
|
||||
count: How many elements to ignore.
|
||||
"""
|
||||
if self._text is not None:
|
||||
for _ in range(count):
|
||||
self.do_search.emit(self._text, self._flags)
|
||||
|
||||
@cmdutils.register(instance='search-runner', hide=True, scope='window')
|
||||
def search_prev(self, count: {'special': 'count'}=1):
|
||||
"""Continue the search to the ([count]th) previous term.
|
||||
|
||||
Args:
|
||||
count: How many elements to ignore.
|
||||
"""
|
||||
if self._text is None:
|
||||
return
|
||||
# The int() here serves as a QFlags constructor to create a copy of the
|
||||
# QFlags instance rather as a reference. I don't know why it works this
|
||||
# way, but it does.
|
||||
flags = int(self._flags)
|
||||
if flags & QWebPage.FindBackward:
|
||||
flags &= ~QWebPage.FindBackward
|
||||
else:
|
||||
flags |= QWebPage.FindBackward
|
||||
for _ in range(count):
|
||||
self.do_search.emit(self._text, flags)
|
||||
|
||||
|
||||
class CommandRunner(QObject):
|
||||
|
||||
"""Parse and run qutebrowser commandline commands.
|
||||
|
||||
Attributes:
|
||||
_cmd: The command which was parsed.
|
||||
_args: The arguments which were parsed.
|
||||
_win_id: The window this CommandRunner is associated with.
|
||||
"""
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
self._cmd = None
|
||||
self._args = []
|
||||
self._win_id = win_id
|
||||
|
||||
def _get_alias(self, text):
|
||||
@@ -90,34 +189,7 @@ class CommandRunner(QObject):
|
||||
new_cmd += ' '
|
||||
return new_cmd
|
||||
|
||||
def parse_all(self, text, *args, **kwargs):
|
||||
"""Split a command on ;; and parse all parts.
|
||||
|
||||
If the first command in the commandline is a non-split one, it only
|
||||
returns that.
|
||||
|
||||
Args:
|
||||
text: Text to parse.
|
||||
*args/**kwargs: Passed to parse().
|
||||
|
||||
Yields:
|
||||
ParseResult tuples.
|
||||
"""
|
||||
if ';;' in text:
|
||||
# Get the first command and check if it doesn't want to have ;;
|
||||
# split.
|
||||
first = text.split(';;')[0]
|
||||
result = self.parse(first, *args, **kwargs)
|
||||
if result.cmd.no_cmd_split:
|
||||
sub_texts = [text]
|
||||
else:
|
||||
sub_texts = [e.strip() for e in text.split(';;')]
|
||||
else:
|
||||
sub_texts = [text]
|
||||
for sub in sub_texts:
|
||||
yield self.parse(sub, *args, **kwargs)
|
||||
|
||||
def parse(self, text, *, aliases=True, fallback=False, keep=False):
|
||||
def parse(self, text, aliases=True, fallback=False, keep=False):
|
||||
"""Split the commandline text into command and arguments.
|
||||
|
||||
Args:
|
||||
@@ -128,7 +200,7 @@ class CommandRunner(QObject):
|
||||
keep: Whether to keep special chars and whitespace
|
||||
|
||||
Return:
|
||||
A (cmd, args, cmdline) ParseResult tuple.
|
||||
A split string commandline, e.g ['open', 'www.google.com']
|
||||
"""
|
||||
cmdstr, sep, argstr = text.partition(' ')
|
||||
if not cmdstr and not fallback:
|
||||
@@ -140,34 +212,29 @@ class CommandRunner(QObject):
|
||||
return self.parse(new_cmd, aliases=False, fallback=fallback,
|
||||
keep=keep)
|
||||
try:
|
||||
cmd = cmdutils.cmd_dict[cmdstr]
|
||||
self._cmd = cmdutils.cmd_dict[cmdstr]
|
||||
except KeyError:
|
||||
if fallback:
|
||||
cmd = None
|
||||
args = None
|
||||
if keep:
|
||||
cmdstr, sep, argstr = text.partition(' ')
|
||||
cmdline = [cmdstr, sep] + argstr.split()
|
||||
else:
|
||||
cmdline = text.split()
|
||||
if fallback and keep:
|
||||
cmdstr, sep, argstr = text.partition(' ')
|
||||
return [cmdstr, sep] + argstr.split()
|
||||
elif fallback:
|
||||
return text.split()
|
||||
else:
|
||||
raise cmdexc.NoSuchCommandError('{}: no such command'.format(
|
||||
cmdstr))
|
||||
raise cmdexc.NoSuchCommandError(
|
||||
'{}: no such command'.format(cmdstr))
|
||||
self._split_args(argstr, keep)
|
||||
retargs = self._args[:]
|
||||
if keep and retargs:
|
||||
return [cmdstr, sep + retargs[0]] + retargs[1:]
|
||||
elif keep:
|
||||
return [cmdstr, sep]
|
||||
else:
|
||||
args = self._split_args(cmd, argstr, keep)
|
||||
if keep and args:
|
||||
cmdline = [cmdstr, sep + args[0]] + args[1:]
|
||||
elif keep:
|
||||
cmdline = [cmdstr, sep]
|
||||
else:
|
||||
cmdline = [cmdstr] + args[:]
|
||||
return ParseResult(cmd=cmd, args=args, cmdline=cmdline)
|
||||
return [cmdstr] + retargs
|
||||
|
||||
def _split_args(self, cmd, argstr, keep):
|
||||
def _split_args(self, argstr, keep):
|
||||
"""Split the arguments from an arg string.
|
||||
|
||||
Args:
|
||||
cmd: The command we're currently handling.
|
||||
argstr: An argument string.
|
||||
keep: Whether to keep special chars and whitespace
|
||||
|
||||
@@ -175,9 +242,9 @@ class CommandRunner(QObject):
|
||||
A list containing the splitted strings.
|
||||
"""
|
||||
if not argstr:
|
||||
return []
|
||||
elif cmd.maxsplit is None:
|
||||
return split.split(argstr, keep=keep)
|
||||
self._args = []
|
||||
elif self._cmd.maxsplit is None:
|
||||
self._args = split.split(argstr, keep=keep)
|
||||
else:
|
||||
# If split=False, we still want to split the flags, but not
|
||||
# everything after that.
|
||||
@@ -195,16 +262,26 @@ class CommandRunner(QObject):
|
||||
for i, arg in enumerate(split_args):
|
||||
arg = arg.strip()
|
||||
if arg.startswith('-'):
|
||||
if arg in cmd.flags_with_args:
|
||||
if arg.lstrip('-') in self._cmd.flags_with_args:
|
||||
flag_arg_count += 1
|
||||
else:
|
||||
maxsplit = i + cmd.maxsplit + flag_arg_count
|
||||
return split.simple_split(argstr, keep=keep,
|
||||
self._args = []
|
||||
maxsplit = i + self._cmd.maxsplit + flag_arg_count
|
||||
args = split.simple_split(argstr, keep=keep,
|
||||
maxsplit=maxsplit)
|
||||
else: # pylint: disable=useless-else-on-loop
|
||||
for s in args:
|
||||
# remove quotes and replace \" by "
|
||||
if s == '""' or s == "''":
|
||||
s = ''
|
||||
else:
|
||||
s = re.sub(r"""(^|[^\\])["']""", r'\1', s)
|
||||
s = re.sub(r"""\\(["'])""", r'\1', s)
|
||||
self._args.append(s)
|
||||
break
|
||||
else:
|
||||
# If there are only flags, we got it right on the first try
|
||||
# already.
|
||||
return split_args
|
||||
self._args = split_args
|
||||
|
||||
def run(self, text, count=None):
|
||||
"""Parse a command from a line of text and run it.
|
||||
@@ -213,12 +290,16 @@ class CommandRunner(QObject):
|
||||
text: The text to parse.
|
||||
count: The count to pass to the command.
|
||||
"""
|
||||
for result in self.parse_all(text):
|
||||
args = replace_variables(self._win_id, result.args)
|
||||
if count is not None:
|
||||
result.cmd.run(self._win_id, args, count=count)
|
||||
else:
|
||||
result.cmd.run(self._win_id, args)
|
||||
if ';;' in text:
|
||||
for sub in text.split(';;'):
|
||||
self.run(sub, count)
|
||||
return
|
||||
self.parse(text)
|
||||
args = replace_variables(self._win_id, self._args)
|
||||
if count is not None:
|
||||
self._cmd.run(self._win_id, args, count=count)
|
||||
else:
|
||||
self._cmd.run(self._win_id, args)
|
||||
|
||||
@pyqtSlot(str, int)
|
||||
def run_safely(self, text, count=None):
|
||||
|
||||
@@ -22,46 +22,67 @@
|
||||
import os
|
||||
import os.path
|
||||
import tempfile
|
||||
import select
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QSocketNotifier
|
||||
from PyQt5.QtCore import (pyqtSignal, QObject, QThread, QStandardPaths,
|
||||
QProcessEnvironment, QProcess, QUrl)
|
||||
|
||||
from qutebrowser.utils import message, log, objreg, standarddir
|
||||
from qutebrowser.commands import runners, cmdexc
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.misc import guiprocess
|
||||
|
||||
|
||||
class _QtFIFOReader(QObject):
|
||||
class _BlockingFIFOReader(QObject):
|
||||
|
||||
"""A FIFO reader based on a QSocketNotifier."""
|
||||
"""A worker which reads commands from a FIFO endlessly.
|
||||
|
||||
This is intended to be run in a separate QThread. It reads from the given
|
||||
FIFO even across EOF so an userscript can write to it multiple times.
|
||||
|
||||
It uses select() so it can timeout once per second, checking if termination
|
||||
was requested.
|
||||
|
||||
Attributes:
|
||||
_filepath: The filename of the FIFO to read.
|
||||
fifo: The file object which is being read.
|
||||
|
||||
Signals:
|
||||
got_line: Emitted when a new line arrived.
|
||||
finished: Emitted when the read loop realized it should terminate and
|
||||
is about to do so.
|
||||
"""
|
||||
|
||||
got_line = pyqtSignal(str)
|
||||
finished = pyqtSignal()
|
||||
|
||||
def __init__(self, filepath, parent=None):
|
||||
super().__init__(parent)
|
||||
self._filepath = filepath
|
||||
# We open as R/W so we never get EOF and have to reopen the pipe.
|
||||
# See http://www.outflux.net/blog/archives/2008/03/09/using-select-on-a-fifo/
|
||||
# We also use os.open and os.fdopen rather than built-in open so we
|
||||
# can add O_NONBLOCK.
|
||||
fd = os.open(filepath, os.O_RDWR |
|
||||
os.O_NONBLOCK) # pylint: disable=no-member
|
||||
self.fifo = os.fdopen(fd, 'r')
|
||||
self._notifier = QSocketNotifier(fd, QSocketNotifier.Read, self)
|
||||
self._notifier.activated.connect(self.read_line)
|
||||
self.fifo = None
|
||||
|
||||
@pyqtSlot()
|
||||
def read_line(self):
|
||||
"""(Try to) read a line from the FIFO."""
|
||||
log.procs.debug("QSocketNotifier triggered!")
|
||||
self._notifier.setEnabled(False)
|
||||
for line in self.fifo:
|
||||
self.got_line.emit(line.rstrip('\r\n'))
|
||||
self._notifier.setEnabled(True)
|
||||
|
||||
def cleanup(self):
|
||||
"""Clean up so the FIFO can be closed."""
|
||||
self._notifier.setEnabled(False)
|
||||
def read(self):
|
||||
"""Blocking read loop which emits got_line when a new line arrived."""
|
||||
try:
|
||||
# We open as R/W so we never get EOF and have to reopen the pipe.
|
||||
# See http://www.outflux.net/blog/archives/2008/03/09/using-select-on-a-fifo/
|
||||
# We also use os.open and os.fdopen rather than built-in open so we
|
||||
# can add O_NONBLOCK.
|
||||
fd = os.open(self._filepath, os.O_RDWR |
|
||||
os.O_NONBLOCK) # pylint: disable=no-member
|
||||
self.fifo = os.fdopen(fd, 'r')
|
||||
except OSError:
|
||||
log.procs.exception("Failed to read FIFO")
|
||||
self.finished.emit()
|
||||
return
|
||||
while True:
|
||||
log.procs.debug("thread loop")
|
||||
ready_r, _ready_w, _ready_e = select.select([self.fifo], [], [], 1)
|
||||
if ready_r:
|
||||
log.procs.debug("reading data")
|
||||
for line in self.fifo:
|
||||
self.got_line.emit(line.rstrip())
|
||||
if QThread.currentThread().isInterruptionRequested():
|
||||
self.finished.emit()
|
||||
return
|
||||
|
||||
|
||||
class _BaseUserscriptRunner(QObject):
|
||||
@@ -70,9 +91,13 @@ class _BaseUserscriptRunner(QObject):
|
||||
|
||||
Attributes:
|
||||
_filepath: The path of the file/FIFO which is being read.
|
||||
_proc: The GUIProcess which is being executed.
|
||||
_proc: The QProcess which is being executed.
|
||||
_win_id: The window ID this runner is associated with.
|
||||
|
||||
Class attributes:
|
||||
PROCESS_MESSAGES: A mapping of QProcess::ProcessError members to
|
||||
human-readable error strings.
|
||||
|
||||
Signals:
|
||||
got_cmd: Emitted when a new command arrived and should be executed.
|
||||
finished: Emitted when the userscript finished running.
|
||||
@@ -81,129 +106,149 @@ class _BaseUserscriptRunner(QObject):
|
||||
got_cmd = pyqtSignal(str)
|
||||
finished = pyqtSignal()
|
||||
|
||||
PROCESS_MESSAGES = {
|
||||
QProcess.FailedToStart: "The process failed to start.",
|
||||
QProcess.Crashed: "The process crashed.",
|
||||
QProcess.Timedout: "The last waitFor...() function timed out.",
|
||||
QProcess.WriteError: ("An error occurred when attempting to write to "
|
||||
"the process."),
|
||||
QProcess.ReadError: ("An error occurred when attempting to read from "
|
||||
"the process."),
|
||||
QProcess.UnknownError: "An unknown error occurred.",
|
||||
}
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
self._win_id = win_id
|
||||
self._filepath = None
|
||||
self._proc = None
|
||||
self._env = None
|
||||
|
||||
def _run_process(self, cmd, *args, env, verbose):
|
||||
"""Start the given command.
|
||||
def _run_process(self, cmd, *args, env):
|
||||
"""Start the given command via QProcess.
|
||||
|
||||
Args:
|
||||
cmd: The command to be started.
|
||||
*args: The arguments to hand to the command
|
||||
env: A dictionary of environment variables to add.
|
||||
verbose: Show notifications when the command started/exited.
|
||||
"""
|
||||
self._env = {'QUTE_FIFO': self._filepath}
|
||||
self._env.update(env)
|
||||
self._proc = guiprocess.GUIProcess(self._win_id, 'userscript',
|
||||
additional_env=self._env,
|
||||
verbose=verbose, parent=self)
|
||||
self._proc = QProcess(self)
|
||||
procenv = QProcessEnvironment.systemEnvironment()
|
||||
procenv.insert('QUTE_FIFO', self._filepath)
|
||||
if env is not None:
|
||||
for k, v in env.items():
|
||||
procenv.insert(k, v)
|
||||
self._proc.setProcessEnvironment(procenv)
|
||||
self._proc.error.connect(self.on_proc_error)
|
||||
self._proc.finished.connect(self.on_proc_finished)
|
||||
self._proc.start(cmd, args)
|
||||
|
||||
def _cleanup(self):
|
||||
"""Clean up temporary files."""
|
||||
tempfiles = [self._filepath]
|
||||
if 'QUTE_HTML' in self._env:
|
||||
tempfiles.append(self._env['QUTE_HTML'])
|
||||
if 'QUTE_TEXT' in self._env:
|
||||
tempfiles.append(self._env['QUTE_TEXT'])
|
||||
for fn in tempfiles:
|
||||
log.procs.debug("Deleting temporary file {}.".format(fn))
|
||||
try:
|
||||
os.remove(fn)
|
||||
except OSError as e:
|
||||
# NOTE: Do not replace this with "raise CommandError" as it's
|
||||
# executed async.
|
||||
message.error(
|
||||
self._win_id, "Failed to delete tempfile {} ({})!".format(
|
||||
fn, e))
|
||||
"""Clean up the temporary file."""
|
||||
try:
|
||||
os.remove(self._filepath)
|
||||
except OSError as e:
|
||||
# NOTE: Do not replace this with "raise CommandError" as it's
|
||||
# executed async.
|
||||
message.error(self._win_id,
|
||||
"Failed to delete tempfile... ({})".format(e))
|
||||
self._filepath = None
|
||||
self._proc = None
|
||||
self._env = None
|
||||
|
||||
def run(self, cmd, *args, env=None, verbose=False):
|
||||
def run(self, cmd, *args, env=None):
|
||||
"""Run the userscript given.
|
||||
|
||||
Needs to be overridden by subclasses.
|
||||
Needs to be overridden by superclasses.
|
||||
|
||||
Args:
|
||||
cmd: The command to be started.
|
||||
*args: The arguments to hand to the command
|
||||
env: A dictionary of environment variables to add.
|
||||
verbose: Show notifications when the command started/exited.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def on_proc_finished(self):
|
||||
"""Called when the process has finished.
|
||||
|
||||
Needs to be overridden by subclasses.
|
||||
Needs to be overridden by superclasses.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def on_proc_error(self, error):
|
||||
"""Called when the process encountered an error."""
|
||||
raise NotImplementedError
|
||||
msg = self.PROCESS_MESSAGES[error]
|
||||
# NOTE: Do not replace this with "raise CommandError" as it's
|
||||
# executed async.
|
||||
message.error(self._win_id,
|
||||
"Error while calling userscript: {}".format(msg))
|
||||
|
||||
|
||||
class _POSIXUserscriptRunner(_BaseUserscriptRunner):
|
||||
|
||||
"""Userscript runner to be used on POSIX. Uses _QtFIFOReader.
|
||||
"""Userscript runner to be used on POSIX. Uses _BlockingFIFOReader.
|
||||
|
||||
Commands are executed immediately when they arrive in the FIFO.
|
||||
The OS must have support for named pipes and select(). Commands are
|
||||
executed immediately when they arrive in the FIFO.
|
||||
|
||||
Attributes:
|
||||
_reader: The _QtFIFOReader instance.
|
||||
_reader: The _BlockingFIFOReader instance.
|
||||
_thread: The QThread where reader runs.
|
||||
"""
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(win_id, parent)
|
||||
self._reader = None
|
||||
self._thread = None
|
||||
|
||||
def run(self, cmd, *args, env=None, verbose=False):
|
||||
def run(self, cmd, *args, env=None):
|
||||
rundir = standarddir.get(QStandardPaths.RuntimeLocation)
|
||||
try:
|
||||
# tempfile.mktemp is deprecated and discouraged, but we use it here
|
||||
# to create a FIFO since the only other alternative would be to
|
||||
# create a directory and place the FIFO there, which sucks. Since
|
||||
# os.mkfifo will raise an exception anyways when the path doesn't
|
||||
# os.kfifo will raise an exception anyways when the path doesn't
|
||||
# exist, it shouldn't be a big issue.
|
||||
self._filepath = tempfile.mktemp(prefix='qutebrowser-userscript-',
|
||||
dir=standarddir.runtime())
|
||||
self._filepath = tempfile.mktemp(prefix='userscript-', dir=rundir)
|
||||
os.mkfifo(self._filepath) # pylint: disable=no-member
|
||||
except OSError as e:
|
||||
message.error(self._win_id, "Error while creating FIFO: {}".format(
|
||||
e))
|
||||
return
|
||||
|
||||
self._reader = _QtFIFOReader(self._filepath)
|
||||
self._reader = _BlockingFIFOReader(self._filepath)
|
||||
self._thread = QThread(self)
|
||||
self._reader.moveToThread(self._thread)
|
||||
self._reader.got_line.connect(self.got_cmd)
|
||||
self._thread.started.connect(self._reader.read)
|
||||
self._reader.finished.connect(self.on_reader_finished)
|
||||
self._thread.finished.connect(self.on_thread_finished)
|
||||
|
||||
self._run_process(cmd, *args, env=env, verbose=verbose)
|
||||
self._run_process(cmd, *args, env=env)
|
||||
self._thread.start()
|
||||
|
||||
def on_proc_finished(self):
|
||||
"""Interrupt the reader when the process finished."""
|
||||
self.finish()
|
||||
log.procs.debug("proc finished")
|
||||
self._thread.requestInterruption()
|
||||
|
||||
def on_proc_error(self, error):
|
||||
"""Interrupt the reader when the process had an error."""
|
||||
self.finish()
|
||||
super().on_proc_error(error)
|
||||
self._thread.requestInterruption()
|
||||
|
||||
def finish(self):
|
||||
def on_reader_finished(self):
|
||||
"""Quit the thread and clean up when the reader finished."""
|
||||
log.procs.debug("Cleaning up")
|
||||
self._reader.cleanup()
|
||||
log.procs.debug("reader finished")
|
||||
self._thread.quit()
|
||||
self._reader.fifo.close()
|
||||
self._reader.deleteLater()
|
||||
self._reader = None
|
||||
super()._cleanup()
|
||||
self.finished.emit()
|
||||
|
||||
def on_thread_finished(self):
|
||||
"""Clean up the QThread object when the thread finished."""
|
||||
log.procs.debug("thread finished")
|
||||
self._thread.deleteLater()
|
||||
|
||||
|
||||
class _WindowsUserscriptRunner(_BaseUserscriptRunner):
|
||||
|
||||
@@ -236,6 +281,7 @@ class _WindowsUserscriptRunner(_BaseUserscriptRunner):
|
||||
|
||||
def on_proc_finished(self):
|
||||
"""Read back the commands when the process finished."""
|
||||
log.procs.debug("proc finished")
|
||||
try:
|
||||
with open(self._filepath, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
@@ -247,17 +293,18 @@ class _WindowsUserscriptRunner(_BaseUserscriptRunner):
|
||||
|
||||
def on_proc_error(self, error):
|
||||
"""Clean up when the process had an error."""
|
||||
super().on_proc_error(error)
|
||||
self._cleanup()
|
||||
self.finished.emit()
|
||||
|
||||
def run(self, cmd, *args, env=None, verbose=False):
|
||||
def run(self, cmd, *args, env=None):
|
||||
try:
|
||||
self._oshandle, self._filepath = tempfile.mkstemp(text=True)
|
||||
except OSError as e:
|
||||
message.error(self._win_id, "Error while creating tempfile: "
|
||||
"{}".format(e))
|
||||
return
|
||||
self._run_process(cmd, *args, env=env, verbose=verbose)
|
||||
self._run_process(cmd, *args, env=env)
|
||||
|
||||
|
||||
class _DummyUserscriptRunner:
|
||||
@@ -273,9 +320,8 @@ class _DummyUserscriptRunner:
|
||||
|
||||
finished = pyqtSignal()
|
||||
|
||||
def run(self, cmd, *args, env=None, verbose=False):
|
||||
def run(self, _cmd, *_args, _env=None):
|
||||
"""Print an error as userscripts are not supported."""
|
||||
# pylint: disable=unused-argument,unused-variable
|
||||
self.finished.emit()
|
||||
raise cmdexc.CommandError(
|
||||
"Userscripts are not supported on this platform!")
|
||||
@@ -291,58 +337,16 @@ else:
|
||||
UserscriptRunner = _DummyUserscriptRunner
|
||||
|
||||
|
||||
def store_source(frame):
|
||||
"""Store HTML/plaintext in files.
|
||||
|
||||
This writes files containing the HTML/plaintext source of the page, and
|
||||
returns a dict with the paths as QUTE_HTML/QUTE_TEXT.
|
||||
|
||||
Args:
|
||||
frame: The QWebFrame to get the info from, or None to do nothing.
|
||||
|
||||
Return:
|
||||
A dictionary with the needed environment variables.
|
||||
|
||||
Warning:
|
||||
The caller is responsible to delete the files after using them!
|
||||
"""
|
||||
if frame is None:
|
||||
return {}
|
||||
env = {}
|
||||
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
|
||||
suffix='.html',
|
||||
delete=False) as html_file:
|
||||
html_file.write(frame.toHtml())
|
||||
env['QUTE_HTML'] = html_file.name
|
||||
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
|
||||
suffix='.txt',
|
||||
delete=False) as txt_file:
|
||||
txt_file.write(frame.toPlainText())
|
||||
env['QUTE_TEXT'] = txt_file.name
|
||||
return env
|
||||
|
||||
|
||||
def run(cmd, *args, win_id, env, verbose=False):
|
||||
"""Convenience method to run an userscript.
|
||||
|
||||
Args:
|
||||
cmd: The userscript binary to run.
|
||||
*args: The arguments to pass to the userscript.
|
||||
win_id: The window id the userscript is executed in.
|
||||
env: A dictionary of variables to add to the process environment.
|
||||
verbose: Show notifications when the command started/exited.
|
||||
"""
|
||||
def run(cmd, *args, url, win_id):
|
||||
"""Convenience method to run an userscript."""
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
# We don't remove the password in the URL here, as it's probably safe to
|
||||
# pass via env variable..
|
||||
urlstr = url.toString(QUrl.FullyEncoded)
|
||||
commandrunner = runners.CommandRunner(win_id, tabbed_browser)
|
||||
runner = UserscriptRunner(win_id, tabbed_browser)
|
||||
runner.got_cmd.connect(
|
||||
lambda cmd: log.commands.debug("Got userscript command: {}".format(
|
||||
cmd)))
|
||||
runner.got_cmd.connect(commandrunner.run_safely)
|
||||
user_agent = config.get('network', 'user-agent')
|
||||
if user_agent is not None:
|
||||
env['QUTE_USER_AGENT'] = user_agent
|
||||
runner.run(cmd, *args, env=env, verbose=verbose)
|
||||
runner.run(cmd, *args, env={'QUTE_URL': urlstr})
|
||||
runner.finished.connect(commandrunner.deleteLater)
|
||||
runner.finished.connect(runner.deleteLater)
|
||||
|
||||
@@ -19,12 +19,14 @@
|
||||
|
||||
"""Completer attached to a CompletionView."""
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer
|
||||
from PyQt5.QtCore import pyqtSlot, QObject, QTimer
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.config import config, configdata
|
||||
from qutebrowser.commands import cmdutils, runners
|
||||
from qutebrowser.utils import usertypes, log, objreg, utils
|
||||
from qutebrowser.completion.models import instances
|
||||
from qutebrowser.completion.models import completion as models
|
||||
from qutebrowser.completion.models.sortfilter import (
|
||||
CompletionFilterModel as CFM)
|
||||
|
||||
|
||||
class Completer(QObject):
|
||||
@@ -32,6 +34,7 @@ class Completer(QObject):
|
||||
"""Completer which manages completions in a CompletionView.
|
||||
|
||||
Attributes:
|
||||
models: dict of available completion models.
|
||||
_cmd: The statusbar Command object this completer belongs to.
|
||||
_ignore_change: Whether to ignore the next completion update.
|
||||
_win_id: The window ID this completer is in.
|
||||
@@ -40,24 +43,24 @@ class Completer(QObject):
|
||||
_last_cursor_pos: The old cursor position so we avoid double completion
|
||||
updates.
|
||||
_last_text: The old command text so we avoid double completion updates.
|
||||
_signals_connected: Whether the signals are connected to update the
|
||||
completion when the command widget requests that.
|
||||
|
||||
Signals:
|
||||
next_prev_item: Emitted to select the next/previous item in the
|
||||
completion.
|
||||
arg0: True for the previous item, False for the next.
|
||||
"""
|
||||
|
||||
next_prev_item = pyqtSignal(bool)
|
||||
|
||||
def __init__(self, cmd, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
self._win_id = win_id
|
||||
self._cmd = cmd
|
||||
self._signals_connected = False
|
||||
self._cmd.update_completion.connect(self.schedule_completion_update)
|
||||
self._cmd.textEdited.connect(self.on_text_edited)
|
||||
self._ignore_change = False
|
||||
self._empty_item_idx = None
|
||||
|
||||
self._models = {
|
||||
usertypes.Completion.option: {},
|
||||
usertypes.Completion.value: {},
|
||||
}
|
||||
self._init_static_completions()
|
||||
self._init_setting_completions()
|
||||
self.init_quickmark_completions()
|
||||
self._timer = QTimer()
|
||||
self._timer.setSingleShot(True)
|
||||
self._timer.setInterval(0)
|
||||
@@ -66,69 +69,51 @@ class Completer(QObject):
|
||||
self._last_cursor_pos = None
|
||||
self._last_text = None
|
||||
|
||||
objreg.get('config').changed.connect(self.on_auto_open_changed)
|
||||
self.handle_signal_connections()
|
||||
self._cmd.clear_completion_selection.connect(
|
||||
self.handle_signal_connections)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self)
|
||||
|
||||
@config.change_filter('completion', 'auto-open')
|
||||
def on_auto_open_changed(self):
|
||||
self.handle_signal_connections()
|
||||
|
||||
@pyqtSlot()
|
||||
def handle_signal_connections(self):
|
||||
self._connect_signals(config.get('completion', 'auto-open'))
|
||||
|
||||
def _connect_signals(self, connect=True):
|
||||
"""Connect or disconnect the completion signals.
|
||||
|
||||
Args:
|
||||
connect: Whether to connect (True) or disconnect (False) the
|
||||
signals.
|
||||
|
||||
Return:
|
||||
True if the signals were connected (connect=True and aren't
|
||||
connected yet) - otherwise False.
|
||||
"""
|
||||
connections = [
|
||||
(self._cmd.update_completion, self.schedule_completion_update),
|
||||
(self._cmd.textChanged, self.on_text_edited),
|
||||
]
|
||||
|
||||
if connect and not self._signals_connected:
|
||||
for sender, receiver in connections:
|
||||
sender.connect(receiver)
|
||||
self._signals_connected = True
|
||||
return True
|
||||
elif not connect:
|
||||
for sender, receiver in connections:
|
||||
try:
|
||||
sender.disconnect(receiver)
|
||||
except TypeError:
|
||||
# Don't fail if not connected
|
||||
pass
|
||||
self._signals_connected = False
|
||||
return False
|
||||
|
||||
def _open_completion_if_needed(self):
|
||||
"""If auto-open is false, temporarily connect signals.
|
||||
|
||||
Also opens the completion.
|
||||
"""
|
||||
if not config.get('completion', 'auto-open'):
|
||||
connected = self._connect_signals(True)
|
||||
if connected:
|
||||
self.update_completion()
|
||||
|
||||
def _model(self):
|
||||
"""Convienience method to get the current completion model."""
|
||||
completion = objreg.get('completion', scope='window',
|
||||
window=self._win_id)
|
||||
return completion.model()
|
||||
|
||||
def _init_static_completions(self):
|
||||
"""Initialize the static completion models."""
|
||||
self._models[usertypes.Completion.command] = CFM(
|
||||
models.CommandCompletionModel(self), self)
|
||||
self._models[usertypes.Completion.helptopic] = CFM(
|
||||
models.HelpCompletionModel(self), self)
|
||||
|
||||
def _init_setting_completions(self):
|
||||
"""Initialize setting completion models."""
|
||||
self._models[usertypes.Completion.section] = CFM(
|
||||
models.SettingSectionCompletionModel(self), self)
|
||||
self._models[usertypes.Completion.option] = {}
|
||||
self._models[usertypes.Completion.value] = {}
|
||||
for sectname in configdata.DATA:
|
||||
model = models.SettingOptionCompletionModel(sectname, self)
|
||||
self._models[usertypes.Completion.option][sectname] = CFM(
|
||||
model, self)
|
||||
self._models[usertypes.Completion.value][sectname] = {}
|
||||
for opt in configdata.DATA[sectname].keys():
|
||||
model = models.SettingValueCompletionModel(sectname, opt, self)
|
||||
self._models[usertypes.Completion.value][sectname][opt] = CFM(
|
||||
model, self)
|
||||
|
||||
@pyqtSlot()
|
||||
def init_quickmark_completions(self):
|
||||
"""Initialize quickmark completion models."""
|
||||
try:
|
||||
self._models[usertypes.Completion.quickmark_by_url].deleteLater()
|
||||
self._models[usertypes.Completion.quickmark_by_name].deleteLater()
|
||||
except KeyError:
|
||||
pass
|
||||
self._models[usertypes.Completion.quickmark_by_url] = CFM(
|
||||
models.QuickmarkCompletionModel('url', self), self)
|
||||
self._models[usertypes.Completion.quickmark_by_name] = CFM(
|
||||
models.QuickmarkCompletionModel('name', self), self)
|
||||
|
||||
def _get_completion_model(self, completion, parts, cursor_part):
|
||||
"""Get a completion model based on an enum member.
|
||||
|
||||
@@ -142,17 +127,17 @@ class Completer(QObject):
|
||||
"""
|
||||
if completion == usertypes.Completion.option:
|
||||
section = parts[cursor_part - 1]
|
||||
model = instances.get(completion).get(section)
|
||||
model = self._models[completion].get(section)
|
||||
elif completion == usertypes.Completion.value:
|
||||
section = parts[cursor_part - 2]
|
||||
option = parts[cursor_part - 1]
|
||||
try:
|
||||
model = instances.get(completion)[section][option]
|
||||
model = self._models[completion][section][option]
|
||||
except KeyError:
|
||||
# No completion model for this section/option.
|
||||
model = None
|
||||
else:
|
||||
model = instances.get(completion)
|
||||
model = self._models.get(completion)
|
||||
return model
|
||||
|
||||
def _filter_cmdline_parts(self, parts, cursor_part):
|
||||
@@ -202,7 +187,7 @@ class Completer(QObject):
|
||||
"{}".format(parts, cursor_part))
|
||||
if cursor_part == 0:
|
||||
# '|' or 'set|'
|
||||
return instances.get(usertypes.Completion.command)
|
||||
return self._models[usertypes.Completion.command]
|
||||
# delegate completion to command
|
||||
try:
|
||||
completions = cmdutils.cmd_dict[parts[0]].completion
|
||||
@@ -258,13 +243,7 @@ class Completer(QObject):
|
||||
data = model.data(indexes[0])
|
||||
if data is None:
|
||||
return
|
||||
parts = self.split()
|
||||
try:
|
||||
needs_quoting = cmdutils.cmd_dict[parts[0]].maxsplit is None
|
||||
except KeyError:
|
||||
needs_quoting = True
|
||||
if needs_quoting:
|
||||
data = self._quote(data)
|
||||
data = self._quote(data)
|
||||
if model.count() == 1 and config.get('completion', 'quick-complete'):
|
||||
# If we only have one item, we want to apply it immediately
|
||||
# and go on to the next part.
|
||||
@@ -313,7 +292,7 @@ class Completer(QObject):
|
||||
if self._cmd.prefix() != ':':
|
||||
# This is a search or gibberish, so we don't need to complete
|
||||
# anything (yet)
|
||||
# FIXME complete searches
|
||||
# FIXME complete searchs
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/32
|
||||
completion.hide()
|
||||
return
|
||||
@@ -334,7 +313,7 @@ class Completer(QObject):
|
||||
pattern = parts[self._cursor_part].strip()
|
||||
except IndexError:
|
||||
pattern = ''
|
||||
completion.set_pattern(pattern)
|
||||
self._model().set_pattern(pattern)
|
||||
|
||||
log.completion.debug(
|
||||
"New completion for {}: {}, with pattern '{}'".format(
|
||||
@@ -364,8 +343,7 @@ class Completer(QObject):
|
||||
# the whitespace.
|
||||
return [text]
|
||||
runner = runners.CommandRunner(self._win_id)
|
||||
result = runner.parse(text, fallback=True, aliases=aliases, keep=keep)
|
||||
parts = result.cmdline
|
||||
parts = runner.parse(text, fallback=True, aliases=aliases, keep=keep)
|
||||
if self._empty_item_idx is not None:
|
||||
log.completion.debug("Empty element queued at {}, "
|
||||
"inserting.".format(self._empty_item_idx))
|
||||
@@ -390,7 +368,7 @@ class Completer(QObject):
|
||||
cursor_pos))
|
||||
skip = 0
|
||||
for i, part in enumerate(parts):
|
||||
log.completion.vdebug("Checking part {}: {!r}".format(i, parts[i]))
|
||||
log.completion.vdebug("Checking part {}: {}".format(i, parts[i]))
|
||||
if not part:
|
||||
skip += 1
|
||||
continue
|
||||
@@ -412,11 +390,7 @@ class Completer(QObject):
|
||||
"Removing len({!r}) -> {} from cursor_pos -> {}".format(
|
||||
part, len(part), cursor_pos))
|
||||
else:
|
||||
if i == 0:
|
||||
# Initial `:` press without any text.
|
||||
self._cursor_part = 0
|
||||
else:
|
||||
self._cursor_part = i - skip
|
||||
self._cursor_part = i - skip
|
||||
if spaces:
|
||||
self._empty_item_idx = i - skip
|
||||
else:
|
||||
@@ -467,17 +441,3 @@ class Completer(QObject):
|
||||
# We also want to update the cursor part and emit update_completion
|
||||
# here, but that's already done for us by cursorPositionChanged
|
||||
# anyways, so we don't need to do it twice.
|
||||
|
||||
@cmdutils.register(instance='completer', hide=True,
|
||||
modes=[usertypes.KeyMode.command], scope='window')
|
||||
def completion_item_prev(self):
|
||||
"""Select the previous completion item."""
|
||||
self._open_completion_if_needed()
|
||||
self.next_prev_item.emit(True)
|
||||
|
||||
@cmdutils.register(instance='completer', hide=True,
|
||||
modes=[usertypes.KeyMode.command], scope='window')
|
||||
def completion_item_next(self):
|
||||
"""Select the next completion item."""
|
||||
self._open_completion_if_needed()
|
||||
self.next_prev_item.emit(False)
|
||||
|
||||
@@ -22,9 +22,7 @@
|
||||
We use this to be able to highlight parts of the text.
|
||||
"""
|
||||
|
||||
import re
|
||||
import html
|
||||
|
||||
from PyQt5.QtWidgets import QStyle, QStyleOptionViewItem, QStyledItemDelegate
|
||||
from PyQt5.QtCore import QRectF, QSize, Qt
|
||||
from PyQt5.QtGui import (QIcon, QPalette, QTextDocument, QTextOption,
|
||||
@@ -145,6 +143,7 @@ class CompletionItemDelegate(QStyledItemDelegate):
|
||||
rect: The QRect to clip the drawing to.
|
||||
"""
|
||||
# We can't use drawContents because then the color would be ignored.
|
||||
# See: https://qt-project.org/forums/viewthread/21492
|
||||
clip = QRectF(0, 0, rect.width(), rect.height())
|
||||
self._painter.save()
|
||||
if self._opt.state & QStyle.State_Selected:
|
||||
@@ -196,9 +195,9 @@ class CompletionItemDelegate(QStyledItemDelegate):
|
||||
if index.parent().isValid():
|
||||
pattern = index.model().pattern
|
||||
if index.column() == 0 and pattern:
|
||||
repl = r'<span class="highlight">\g<0></span>'
|
||||
text = re.sub(re.escape(pattern), repl, self._opt.text,
|
||||
flags=re.IGNORECASE)
|
||||
text = self._opt.text.replace(
|
||||
pattern,
|
||||
'<span class="highlight">{}</span>'.format(pattern))
|
||||
self._doc.setHtml(text)
|
||||
else:
|
||||
self._doc.setPlainText(self._opt.text)
|
||||
|
||||
@@ -26,9 +26,10 @@ subclasses to provide completions.
|
||||
from PyQt5.QtWidgets import QStyle, QTreeView, QSizePolicy
|
||||
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel
|
||||
|
||||
from qutebrowser.commands import cmdutils
|
||||
from qutebrowser.config import config, style
|
||||
from qutebrowser.completion import completiondelegate, completer
|
||||
from qutebrowser.utils import qtutils, objreg, utils
|
||||
from qutebrowser.utils import usertypes, qtutils, objreg, utils
|
||||
|
||||
|
||||
class CompletionView(QTreeView):
|
||||
@@ -58,7 +59,6 @@ class CompletionView(QTreeView):
|
||||
QTreeView {
|
||||
{{ font['completion'] }}
|
||||
{{ color['completion.bg'] }}
|
||||
alternate-background-color: {{ color['completion.alternate-bg'] }};
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
@@ -95,20 +95,18 @@ class CompletionView(QTreeView):
|
||||
objreg.register('completion', self, scope='window', window=win_id)
|
||||
cmd = objreg.get('status-command', scope='window', window=win_id)
|
||||
completer_obj = completer.Completer(cmd, win_id, self)
|
||||
completer_obj.next_prev_item.connect(self.on_next_prev_item)
|
||||
objreg.register('completer', completer_obj, scope='window',
|
||||
window=win_id)
|
||||
self.enabled = config.get('completion', 'show')
|
||||
objreg.get('config').changed.connect(self.set_enabled)
|
||||
# FIXME handle new aliases.
|
||||
# objreg.get('config').changed.connect(self.init_command_completion)
|
||||
#objreg.get('config').changed.connect(self.init_command_completion)
|
||||
|
||||
self._delegate = completiondelegate.CompletionItemDelegate(self)
|
||||
self.setItemDelegate(self._delegate)
|
||||
style.set_register_stylesheet(self)
|
||||
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Minimum)
|
||||
self.setHeaderHidden(True)
|
||||
self.setAlternatingRowColors(True)
|
||||
self.setIndentation(0)
|
||||
self.setItemsExpandable(False)
|
||||
self.setExpandsOnDoubleClick(False)
|
||||
@@ -151,10 +149,7 @@ class CompletionView(QTreeView):
|
||||
idx = self.selectionModel().currentIndex()
|
||||
if not idx.isValid():
|
||||
# No item selected yet
|
||||
if upwards:
|
||||
return self.model().last_item()
|
||||
else:
|
||||
return self.model().first_item()
|
||||
return self.model().first_item()
|
||||
while True:
|
||||
idx = self.indexAbove(idx) if upwards else self.indexBelow(idx)
|
||||
# wrap around if we arrived at beginning/end
|
||||
@@ -168,15 +163,12 @@ class CompletionView(QTreeView):
|
||||
# Item is a real item, not a category header -> success
|
||||
return idx
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def on_next_prev_item(self, prev):
|
||||
def _next_prev_item(self, prev):
|
||||
"""Handle a tab press for the CompletionView.
|
||||
|
||||
Select the previous/next item and write the new text to the
|
||||
statusbar.
|
||||
|
||||
Called from the Completer's next_prev_item signal.
|
||||
|
||||
Args:
|
||||
prev: True for prev item, False for next one.
|
||||
"""
|
||||
@@ -201,20 +193,10 @@ class CompletionView(QTreeView):
|
||||
self.setModel(model)
|
||||
if sel_model is not None:
|
||||
sel_model.deleteLater()
|
||||
for i in range(model.rowCount()):
|
||||
self.expand(model.index(i, 0))
|
||||
self.expandAll()
|
||||
self._resize_columns()
|
||||
self.maybe_resize_completion()
|
||||
|
||||
def set_pattern(self, pattern):
|
||||
"""Set the completion pattern for the current model.
|
||||
|
||||
Called from on_update_completion().
|
||||
|
||||
Args:
|
||||
pattern: The filter pattern to set (what the user entered).
|
||||
"""
|
||||
self.model().set_pattern(pattern)
|
||||
model.rowsRemoved.connect(self.maybe_resize_completion)
|
||||
model.rowsInserted.connect(self.maybe_resize_completion)
|
||||
self.maybe_resize_completion()
|
||||
|
||||
@pyqtSlot()
|
||||
@@ -236,6 +218,18 @@ class CompletionView(QTreeView):
|
||||
selmod.clearSelection()
|
||||
selmod.clearCurrentIndex()
|
||||
|
||||
@cmdutils.register(instance='completion', hide=True,
|
||||
modes=[usertypes.KeyMode.command], scope='window')
|
||||
def completion_item_prev(self):
|
||||
"""Select the previous completion item."""
|
||||
self._next_prev_item(prev=True)
|
||||
|
||||
@cmdutils.register(instance='completion', hide=True,
|
||||
modes=[usertypes.KeyMode.command], scope='window')
|
||||
def completion_item_next(self):
|
||||
"""Select the next completion item."""
|
||||
self._next_prev_item(prev=False)
|
||||
|
||||
def selectionChanged(self, selected, deselected):
|
||||
"""Extend selectionChanged to call completers selection_changed."""
|
||||
super().selectionChanged(selected, deselected)
|
||||
@@ -247,11 +241,3 @@ class CompletionView(QTreeView):
|
||||
"""Extend resizeEvent to adjust column size."""
|
||||
super().resizeEvent(e)
|
||||
self._resize_columns()
|
||||
|
||||
def showEvent(self, e):
|
||||
"""Adjust the completion size and scroll when it's freshly shown."""
|
||||
self.resize_completion.emit()
|
||||
scrollbar = self.verticalScrollBar()
|
||||
if scrollbar is not None:
|
||||
scrollbar.setValue(scrollbar.minimum())
|
||||
super().showEvent(e)
|
||||
|
||||
@@ -29,8 +29,7 @@ from PyQt5.QtGui import QStandardItemModel, QStandardItem
|
||||
from qutebrowser.utils import usertypes, qtutils
|
||||
|
||||
|
||||
Role = usertypes.enum('Role', ['sort', 'userdata'], start=Qt.UserRole,
|
||||
is_int=True)
|
||||
Role = usertypes.enum('Role', ['sort'], start=Qt.UserRole, is_int=True)
|
||||
|
||||
|
||||
class BaseCompletionModel(QStandardItemModel):
|
||||
@@ -61,8 +60,7 @@ class BaseCompletionModel(QStandardItemModel):
|
||||
self.appendRow(cat)
|
||||
return cat
|
||||
|
||||
def new_item(self, cat, name, desc='', misc=None, sort=None,
|
||||
userdata=None):
|
||||
def new_item(self, cat, name, desc='', misc=None):
|
||||
"""Add a new item to a category.
|
||||
|
||||
Args:
|
||||
@@ -70,15 +68,10 @@ class BaseCompletionModel(QStandardItemModel):
|
||||
name: The name of the item.
|
||||
desc: The description of the item.
|
||||
misc: Misc text to display.
|
||||
sort: Data for the sort role (int).
|
||||
userdata: User data to be added for the first column.
|
||||
|
||||
Return:
|
||||
A (nameitem, descitem, miscitem) tuple.
|
||||
"""
|
||||
assert not isinstance(name, int)
|
||||
assert not isinstance(desc, int)
|
||||
assert not isinstance(misc, int)
|
||||
nameitem = QStandardItem(name)
|
||||
descitem = QStandardItem(desc)
|
||||
if misc is None:
|
||||
@@ -89,10 +82,6 @@ class BaseCompletionModel(QStandardItemModel):
|
||||
cat.setChild(idx, 0, nameitem)
|
||||
cat.setChild(idx, 1, descitem)
|
||||
cat.setChild(idx, 2, miscitem)
|
||||
if sort is not None:
|
||||
nameitem.setData(sort, Role.sort)
|
||||
if userdata is not None:
|
||||
nameitem.setData(userdata, Role.userdata)
|
||||
return nameitem, descitem, miscitem
|
||||
|
||||
def flags(self, index):
|
||||
@@ -109,8 +98,7 @@ class BaseCompletionModel(QStandardItemModel):
|
||||
qtutils.ensure_valid(index)
|
||||
if index.parent().isValid():
|
||||
# item
|
||||
return (Qt.ItemIsEnabled | Qt.ItemIsSelectable |
|
||||
Qt.ItemNeverHasChildren)
|
||||
return Qt.ItemIsEnabled | Qt.ItemIsSelectable
|
||||
else:
|
||||
# category
|
||||
return Qt.NoItemFlags
|
||||
|
||||
@@ -17,12 +17,13 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""CompletionModels for the config."""
|
||||
"""CompletionModels for different usages."""
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, Qt
|
||||
|
||||
from qutebrowser.config import config, configdata
|
||||
from qutebrowser.utils import log, qtutils, objreg
|
||||
from qutebrowser.commands import cmdutils
|
||||
from qutebrowser.completion.models import base
|
||||
|
||||
|
||||
@@ -109,16 +110,12 @@ class SettingValueCompletionModel(base.BaseCompletionModel):
|
||||
self._section = section
|
||||
self._option = option
|
||||
objreg.get('config').changed.connect(self.update_current_value)
|
||||
cur_cat = self.new_category("Current/Default", sort=0)
|
||||
cur_cat = self.new_category("Current", sort=0)
|
||||
value = config.get(section, option, raw=True)
|
||||
if not value:
|
||||
value = '""'
|
||||
self.cur_item, _descitem, _miscitem = self.new_item(cur_cat, value,
|
||||
"Current value")
|
||||
default_value = configdata.DATA[section][option].default()
|
||||
if not default_value:
|
||||
default_value = '""'
|
||||
self.new_item(cur_cat, default_value, "Default value")
|
||||
if hasattr(configdata.DATA[section], 'valtype'):
|
||||
# Same type for all values (ValueList)
|
||||
vals = configdata.DATA[section].valtype.complete()
|
||||
@@ -129,7 +126,7 @@ class SettingValueCompletionModel(base.BaseCompletionModel):
|
||||
# Different type for each value (KeyValue)
|
||||
vals = configdata.DATA[section][option].typ.complete()
|
||||
if vals is not None:
|
||||
cat = self.new_category("Completions", sort=1)
|
||||
cat = self.new_category("Allowed", sort=1)
|
||||
for (val, desc) in vals:
|
||||
self.new_item(cat, val, desc)
|
||||
|
||||
@@ -147,3 +144,89 @@ class SettingValueCompletionModel(base.BaseCompletionModel):
|
||||
if not ok:
|
||||
raise ValueError("Setting data failed! (section: {}, option: {}, "
|
||||
"value: {})".format(section, option, value))
|
||||
|
||||
|
||||
class CommandCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
"""A CompletionModel filled with all commands and descriptions."""
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
assert cmdutils.cmd_dict
|
||||
cmdlist = []
|
||||
for obj in set(cmdutils.cmd_dict.values()):
|
||||
if obj.hide or (obj.debug and not objreg.get('args').debug):
|
||||
pass
|
||||
else:
|
||||
cmdlist.append((obj.name, obj.desc))
|
||||
for name, cmd in config.section('aliases').items():
|
||||
cmdlist.append((name, "Alias for '{}'".format(cmd)))
|
||||
cat = self.new_category("Commands")
|
||||
for (name, desc) in sorted(cmdlist):
|
||||
self.new_item(cat, name, desc)
|
||||
|
||||
|
||||
class HelpCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
"""A CompletionModel filled with help topics."""
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._init_commands()
|
||||
self._init_settings()
|
||||
|
||||
def _init_commands(self):
|
||||
"""Fill completion with :command entries."""
|
||||
assert cmdutils.cmd_dict
|
||||
cmdlist = []
|
||||
for obj in set(cmdutils.cmd_dict.values()):
|
||||
if obj.hide or (obj.debug and not objreg.get('args').debug):
|
||||
pass
|
||||
else:
|
||||
cmdlist.append((':' + obj.name, obj.desc))
|
||||
cat = self.new_category("Commands")
|
||||
for (name, desc) in sorted(cmdlist):
|
||||
self.new_item(cat, name, desc)
|
||||
|
||||
def _init_settings(self):
|
||||
"""Fill completion with section->option entries."""
|
||||
cat = self.new_category("Settings")
|
||||
for sectname, sectdata in configdata.DATA.items():
|
||||
for optname in sectdata.keys():
|
||||
try:
|
||||
desc = sectdata.descriptions[optname]
|
||||
except (KeyError, AttributeError):
|
||||
# Some stuff (especially ValueList items) don't have a
|
||||
# description.
|
||||
desc = ""
|
||||
else:
|
||||
desc = desc.splitlines()[0]
|
||||
name = '{}->{}'.format(sectname, optname)
|
||||
self.new_item(cat, name, desc)
|
||||
|
||||
|
||||
class QuickmarkCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
"""A CompletionModel filled with all quickmarks."""
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
def __init__(self, match_field='url', parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
cat = self.new_category("Quickmarks")
|
||||
quickmarks = objreg.get('quickmark-manager').marks.items()
|
||||
|
||||
if match_field == 'url':
|
||||
for qm_name, qm_url in quickmarks:
|
||||
self.new_item(cat, qm_url, qm_name)
|
||||
elif match_field == 'name':
|
||||
for qm_name, qm_url in quickmarks:
|
||||
self.new_item(cat, qm_name, qm_url)
|
||||
else:
|
||||
raise ValueError("Invalid value '{}' for match_field!".format(
|
||||
match_field))
|
||||
@@ -1,175 +0,0 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Global instances of the completion models.
|
||||
|
||||
Module attributes:
|
||||
_instances: An dict of available completions.
|
||||
INITIALIZERS: A {usertypes.Completion: callable} dict of functions to
|
||||
initialize completions.
|
||||
"""
|
||||
|
||||
import functools
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, Qt
|
||||
|
||||
from qutebrowser.completion.models import miscmodels, urlmodel, configmodel
|
||||
from qutebrowser.completion.models.sortfilter import CompletionFilterModel
|
||||
from qutebrowser.utils import objreg, usertypes, log, debug
|
||||
from qutebrowser.config import configdata
|
||||
|
||||
|
||||
_instances = {}
|
||||
|
||||
|
||||
def _init_model(klass, *args, dumb_sort=None, **kwargs):
|
||||
"""Helper to initialize a model.
|
||||
|
||||
Args:
|
||||
klass: The class of the model to initialize.
|
||||
*args: Arguments to pass to the model.
|
||||
**kwargs: Keyword arguments to pass to the model.
|
||||
dumb_sort: Passed to CompletionFilterModel.
|
||||
"""
|
||||
app = objreg.get('app')
|
||||
return CompletionFilterModel(klass(*args, parent=app, **kwargs),
|
||||
dumb_sort=dumb_sort, parent=app)
|
||||
|
||||
|
||||
def _init_command_completion():
|
||||
"""Initialize the command completion model."""
|
||||
log.completion.debug("Initializing command completion.")
|
||||
model = _init_model(miscmodels.CommandCompletionModel)
|
||||
_instances[usertypes.Completion.command] = model
|
||||
|
||||
|
||||
def _init_helptopic_completion():
|
||||
"""Initialize the helptopic completion model."""
|
||||
log.completion.debug("Initializing helptopic completion.")
|
||||
model = _init_model(miscmodels.HelpCompletionModel)
|
||||
_instances[usertypes.Completion.helptopic] = model
|
||||
|
||||
|
||||
def _init_url_completion():
|
||||
"""Initialize the URL completion model."""
|
||||
log.completion.debug("Initializing URL completion.")
|
||||
with debug.log_time(log.completion, 'URL completion init'):
|
||||
model = _init_model(urlmodel.UrlCompletionModel,
|
||||
dumb_sort=Qt.DescendingOrder)
|
||||
_instances[usertypes.Completion.url] = model
|
||||
|
||||
|
||||
def _init_setting_completions():
|
||||
"""Initialize setting completion models."""
|
||||
log.completion.debug("Initializing setting completion.")
|
||||
_instances[usertypes.Completion.section] = _init_model(
|
||||
configmodel.SettingSectionCompletionModel)
|
||||
_instances[usertypes.Completion.option] = {}
|
||||
_instances[usertypes.Completion.value] = {}
|
||||
for sectname in configdata.DATA:
|
||||
model = _init_model(configmodel.SettingOptionCompletionModel, sectname)
|
||||
_instances[usertypes.Completion.option][sectname] = model
|
||||
_instances[usertypes.Completion.value][sectname] = {}
|
||||
for opt in configdata.DATA[sectname].keys():
|
||||
model = _init_model(configmodel.SettingValueCompletionModel,
|
||||
sectname, opt)
|
||||
_instances[usertypes.Completion.value][sectname][opt] = model
|
||||
|
||||
|
||||
@pyqtSlot()
|
||||
def init_quickmark_completions():
|
||||
"""Initialize quickmark completion models."""
|
||||
log.completion.debug("Initializing quickmark completion.")
|
||||
try:
|
||||
_instances[usertypes.Completion.quickmark_by_url].deleteLater()
|
||||
_instances[usertypes.Completion.quickmark_by_name].deleteLater()
|
||||
except KeyError:
|
||||
pass
|
||||
model = _init_model(miscmodels.QuickmarkCompletionModel, 'url')
|
||||
_instances[usertypes.Completion.quickmark_by_url] = model
|
||||
model = _init_model(miscmodels.QuickmarkCompletionModel, 'name')
|
||||
_instances[usertypes.Completion.quickmark_by_name] = model
|
||||
|
||||
|
||||
@pyqtSlot()
|
||||
def init_session_completion():
|
||||
"""Initialize session completion model."""
|
||||
log.completion.debug("Initializing session completion.")
|
||||
try:
|
||||
_instances[usertypes.Completion.sessions].deleteLater()
|
||||
except KeyError:
|
||||
pass
|
||||
model = _init_model(miscmodels.SessionCompletionModel)
|
||||
_instances[usertypes.Completion.sessions] = model
|
||||
|
||||
|
||||
INITIALIZERS = {
|
||||
usertypes.Completion.command: _init_command_completion,
|
||||
usertypes.Completion.helptopic: _init_helptopic_completion,
|
||||
usertypes.Completion.url: _init_url_completion,
|
||||
usertypes.Completion.section: _init_setting_completions,
|
||||
usertypes.Completion.option: _init_setting_completions,
|
||||
usertypes.Completion.value: _init_setting_completions,
|
||||
usertypes.Completion.quickmark_by_url: init_quickmark_completions,
|
||||
usertypes.Completion.quickmark_by_name: init_quickmark_completions,
|
||||
usertypes.Completion.sessions: init_session_completion,
|
||||
}
|
||||
|
||||
|
||||
def get(completion):
|
||||
"""Get a certain completion. Initializes the completion if needed."""
|
||||
try:
|
||||
return _instances[completion]
|
||||
except KeyError:
|
||||
if completion in INITIALIZERS:
|
||||
INITIALIZERS[completion]()
|
||||
return _instances[completion]
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
def update(completions):
|
||||
"""Update an already existing completion.
|
||||
|
||||
Args:
|
||||
completions: An iterable of usertypes.Completions.
|
||||
"""
|
||||
did_run = []
|
||||
for completion in completions:
|
||||
if completion in _instances:
|
||||
func = INITIALIZERS[completion]
|
||||
if func not in did_run:
|
||||
func()
|
||||
did_run.append(func)
|
||||
|
||||
|
||||
def init():
|
||||
"""Initialize completions. Note this only connects signals."""
|
||||
quickmark_manager = objreg.get('quickmark-manager')
|
||||
quickmark_manager.changed.connect(
|
||||
functools.partial(update, [usertypes.Completion.quickmark_by_url,
|
||||
usertypes.Completion.quickmark_by_name]))
|
||||
|
||||
session_manager = objreg.get('session-manager')
|
||||
session_manager.update_completion.connect(
|
||||
functools.partial(update, [usertypes.Completion.sessions]))
|
||||
|
||||
history = objreg.get('web-history')
|
||||
history.async_read_done.connect(
|
||||
functools.partial(update, [usertypes.Completion.url]))
|
||||
@@ -1,128 +0,0 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Misc. CompletionModels."""
|
||||
|
||||
from qutebrowser.config import config, configdata
|
||||
from qutebrowser.utils import objreg, log
|
||||
from qutebrowser.commands import cmdutils
|
||||
from qutebrowser.completion.models import base
|
||||
|
||||
|
||||
class CommandCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
"""A CompletionModel filled with all commands and descriptions."""
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
assert cmdutils.cmd_dict
|
||||
cmdlist = []
|
||||
for obj in set(cmdutils.cmd_dict.values()):
|
||||
if (obj.hide or (obj.debug and not objreg.get('args').debug) or
|
||||
obj.deprecated):
|
||||
pass
|
||||
else:
|
||||
cmdlist.append((obj.name, obj.desc))
|
||||
for name, cmd in config.section('aliases').items():
|
||||
cmdlist.append((name, "Alias for '{}'".format(cmd)))
|
||||
cat = self.new_category("Commands")
|
||||
for (name, desc) in sorted(cmdlist):
|
||||
self.new_item(cat, name, desc)
|
||||
|
||||
|
||||
class HelpCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
"""A CompletionModel filled with help topics."""
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._init_commands()
|
||||
self._init_settings()
|
||||
|
||||
def _init_commands(self):
|
||||
"""Fill completion with :command entries."""
|
||||
assert cmdutils.cmd_dict
|
||||
cmdlist = []
|
||||
for obj in set(cmdutils.cmd_dict.values()):
|
||||
if (obj.hide or (obj.debug and not objreg.get('args').debug) or
|
||||
obj.deprecated):
|
||||
pass
|
||||
else:
|
||||
cmdlist.append((':' + obj.name, obj.desc))
|
||||
cat = self.new_category("Commands")
|
||||
for (name, desc) in sorted(cmdlist):
|
||||
self.new_item(cat, name, desc)
|
||||
|
||||
def _init_settings(self):
|
||||
"""Fill completion with section->option entries."""
|
||||
cat = self.new_category("Settings")
|
||||
for sectname, sectdata in configdata.DATA.items():
|
||||
for optname in sectdata.keys():
|
||||
try:
|
||||
desc = sectdata.descriptions[optname]
|
||||
except (KeyError, AttributeError):
|
||||
# Some stuff (especially ValueList items) don't have a
|
||||
# description.
|
||||
desc = ""
|
||||
else:
|
||||
desc = desc.splitlines()[0]
|
||||
name = '{}->{}'.format(sectname, optname)
|
||||
self.new_item(cat, name, desc)
|
||||
|
||||
|
||||
class QuickmarkCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
"""A CompletionModel filled with all quickmarks."""
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
def __init__(self, match_field='url', parent=None):
|
||||
super().__init__(parent)
|
||||
cat = self.new_category("Quickmarks")
|
||||
quickmarks = objreg.get('quickmark-manager').marks.items()
|
||||
if match_field == 'url':
|
||||
for qm_name, qm_url in quickmarks:
|
||||
self.new_item(cat, qm_url, qm_name)
|
||||
elif match_field == 'name':
|
||||
for qm_name, qm_url in quickmarks:
|
||||
self.new_item(cat, qm_name, qm_url)
|
||||
else:
|
||||
raise ValueError("Invalid value '{}' for match_field!".format(
|
||||
match_field))
|
||||
|
||||
|
||||
class SessionCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
"""A CompletionModel filled with session names."""
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
cat = self.new_category("Sessions")
|
||||
try:
|
||||
for name in objreg.get('session-manager').list_sessions():
|
||||
if not name.startswith('_'):
|
||||
self.new_item(cat, name)
|
||||
except OSError:
|
||||
log.completion.exception("Failed to list sessions!")
|
||||
@@ -23,9 +23,9 @@ Contains:
|
||||
CompletionFilterModel -- A QSortFilterProxyModel subclass for completions.
|
||||
"""
|
||||
|
||||
from PyQt5.QtCore import QSortFilterProxyModel, QModelIndex, Qt
|
||||
from PyQt5.QtCore import QSortFilterProxyModel, QModelIndex
|
||||
|
||||
from qutebrowser.utils import log, qtutils, debug
|
||||
from qutebrowser.utils import log, qtutils
|
||||
from qutebrowser.completion.models import base as completion
|
||||
|
||||
|
||||
@@ -38,21 +38,13 @@ class CompletionFilterModel(QSortFilterProxyModel):
|
||||
srcmodel: The current source model.
|
||||
Kept as attribute because calling `sourceModel` takes quite
|
||||
a long time for some reason.
|
||||
_sort_order: The order to use for sorting if using dumb_sort.
|
||||
"""
|
||||
|
||||
def __init__(self, source, parent=None, *, dumb_sort=None):
|
||||
def __init__(self, source, parent=None):
|
||||
super().__init__(parent)
|
||||
super().setSourceModel(source)
|
||||
self.srcmodel = source
|
||||
self.pattern = ''
|
||||
if dumb_sort is None:
|
||||
# pylint: disable=invalid-name
|
||||
self.lessThan = self.intelligentLessThan
|
||||
self._sort_order = Qt.AscendingOrder
|
||||
else:
|
||||
self.setSortRole(completion.Role.sort)
|
||||
self._sort_order = dumb_sort
|
||||
|
||||
def set_pattern(self, val):
|
||||
"""Setter for pattern.
|
||||
@@ -65,15 +57,14 @@ class CompletionFilterModel(QSortFilterProxyModel):
|
||||
Args:
|
||||
val: The value to set.
|
||||
"""
|
||||
with debug.log_time(log.completion, 'Setting filter pattern'):
|
||||
self.pattern = val
|
||||
self.invalidateFilter()
|
||||
sortcol = 0
|
||||
try:
|
||||
self.srcmodel.sort(sortcol)
|
||||
except NotImplementedError:
|
||||
self.sort(sortcol)
|
||||
self.invalidate()
|
||||
self.pattern = val
|
||||
self.invalidateFilter()
|
||||
sortcol = 0
|
||||
try:
|
||||
self.srcmodel.sort(sortcol)
|
||||
except NotImplementedError:
|
||||
self.sort(sortcol)
|
||||
self.invalidate()
|
||||
|
||||
def count(self):
|
||||
"""Get the count of non-toplevel items currently visible.
|
||||
@@ -133,18 +124,14 @@ class CompletionFilterModel(QSortFilterProxyModel):
|
||||
if parent == QModelIndex():
|
||||
return True
|
||||
idx = self.srcmodel.index(row, 0, parent)
|
||||
if not idx.isValid():
|
||||
# No entries in parent model
|
||||
return False
|
||||
qtutils.ensure_valid(idx)
|
||||
data = self.srcmodel.data(idx)
|
||||
# TODO more sophisticated filtering
|
||||
if not self.pattern:
|
||||
return True
|
||||
if not data:
|
||||
return False
|
||||
return self.pattern.casefold() in data.casefold()
|
||||
|
||||
def intelligentLessThan(self, lindex, rindex):
|
||||
def lessThan(self, lindex, rindex):
|
||||
"""Custom sorting implementation.
|
||||
|
||||
Prefers all items which start with self.pattern. Other than that, uses
|
||||
@@ -180,9 +167,3 @@ class CompletionFilterModel(QSortFilterProxyModel):
|
||||
return False
|
||||
else:
|
||||
return left < right
|
||||
|
||||
def sort(self, column, order=None):
|
||||
"""Extend sort to respect self._sort_order if no order was given."""
|
||||
if order is None:
|
||||
order = self._sort_order
|
||||
super().sort(column, order)
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""CompletionModels for URLs."""
|
||||
|
||||
import datetime
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, Qt
|
||||
|
||||
from qutebrowser.utils import objreg, utils
|
||||
from qutebrowser.completion.models import base
|
||||
from qutebrowser.config import config
|
||||
|
||||
|
||||
class UrlCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
"""A model which combines quickmarks and web history URLs.
|
||||
|
||||
Used for the `open` command."""
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self._quickmark_cat = self.new_category("Quickmarks")
|
||||
self._history_cat = self.new_category("History")
|
||||
|
||||
quickmark_manager = objreg.get('quickmark-manager')
|
||||
quickmarks = quickmark_manager.marks.items()
|
||||
for qm_name, qm_url in quickmarks:
|
||||
self._add_quickmark_entry(qm_name, qm_url)
|
||||
quickmark_manager.added.connect(self.on_quickmark_added)
|
||||
quickmark_manager.removed.connect(self.on_quickmark_removed)
|
||||
|
||||
self._history = objreg.get('web-history')
|
||||
max_history = config.get('completion', 'web-history-max-items')
|
||||
history = utils.newest_slice(self._history, max_history)
|
||||
for entry in history:
|
||||
self._add_history_entry(entry)
|
||||
self._history.add_completion_item.connect(
|
||||
self.on_history_item_added)
|
||||
|
||||
objreg.get('config').changed.connect(self.reformat_timestamps)
|
||||
|
||||
def _fmt_atime(self, atime):
|
||||
"""Format an atime to a human-readable string."""
|
||||
fmt = config.get('completion', 'timestamp-format')
|
||||
if fmt is None:
|
||||
return ''
|
||||
return datetime.datetime.fromtimestamp(atime).strftime(fmt)
|
||||
|
||||
def _add_history_entry(self, entry):
|
||||
"""Add a new history entry to the completion."""
|
||||
self.new_item(self._history_cat, entry.url.toDisplayString(), "",
|
||||
self._fmt_atime(entry.atime), sort=int(entry.atime),
|
||||
userdata=entry.url)
|
||||
|
||||
def _add_quickmark_entry(self, name, url):
|
||||
"""Add a new quickmark entry to the completion.
|
||||
|
||||
Args:
|
||||
name: The name of the new quickmark.
|
||||
url: The URL of the new quickmark.
|
||||
"""
|
||||
self.new_item(self._quickmark_cat, url, name)
|
||||
|
||||
@config.change_filter('completion', 'timestamp-format')
|
||||
def reformat_timestamps(self):
|
||||
"""Reformat the timestamps if the config option was changed."""
|
||||
for i in range(self._history_cat.rowCount()):
|
||||
name_item = self._history_cat.child(i, 0)
|
||||
atime_item = self._history_cat.child(i, 2)
|
||||
atime = name_item.data(base.Role.sort)
|
||||
atime_item.setText(self._fmt_atime(atime))
|
||||
|
||||
@pyqtSlot(object)
|
||||
def on_history_item_added(self, entry):
|
||||
"""Slot called when a new history item was added."""
|
||||
for i in range(self._history_cat.rowCount()):
|
||||
name_item = self._history_cat.child(i, 0)
|
||||
atime_item = self._history_cat.child(i, 2)
|
||||
url = name_item.data(base.Role.userdata)
|
||||
if url == entry.url:
|
||||
atime_item.setText(self._fmt_atime(entry.atime))
|
||||
name_item.setData(int(entry.atime), base.Role.sort)
|
||||
break
|
||||
else:
|
||||
self._add_history_entry(entry)
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def on_quickmark_added(self, name, url):
|
||||
"""Called when a quickmark has been added by the user.
|
||||
|
||||
Args:
|
||||
name: The name of the new quickmark.
|
||||
url: The url of the new quickmark, as string.
|
||||
"""
|
||||
self._add_quickmark_entry(name, url)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def on_quickmark_removed(self, name):
|
||||
"""Called when a quickmark has been removed by the user.
|
||||
|
||||
Args:
|
||||
name: The name of the quickmark which has been removed.
|
||||
"""
|
||||
for i in range(self._quickmark_cat.rowCount()):
|
||||
name_item = self._quickmark_cat.child(i, 1)
|
||||
if name_item.data(Qt.DisplayRole) == name:
|
||||
self._quickmark_cat.removeRow(i)
|
||||
break
|
||||
@@ -20,8 +20,8 @@
|
||||
"""Configuration storage and config-related utilities.
|
||||
|
||||
This borrows a lot of ideas from configparser, but also has some things that
|
||||
are fundamentally different. This is why nothing inherits from configparser,
|
||||
but we borrow some methods and classes from there where it makes sense.
|
||||
are fundamentally different. This is why nothing inherts from configparser, but
|
||||
we borrow some methods and classes from there where it makes sense.
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -32,19 +32,19 @@ import configparser
|
||||
import collections
|
||||
import collections.abc
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl, QSettings
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QStandardPaths, QUrl
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
from qutebrowser.config import configdata, configexc, textwrapper
|
||||
from qutebrowser.config.parsers import ini, keyconf
|
||||
from qutebrowser.commands import cmdexc, cmdutils
|
||||
from qutebrowser.utils import (message, objreg, utils, standarddir, log,
|
||||
qtutils, error, usertypes)
|
||||
from qutebrowser.utils import message, objreg, utils, standarddir, log, qtutils
|
||||
from qutebrowser.utils.usertypes import Completion
|
||||
|
||||
|
||||
class change_filter: # pylint: disable=invalid-name
|
||||
|
||||
"""Decorator to filter calls based on a config section/option matching.
|
||||
"""Decorator to register a new command handler.
|
||||
|
||||
This could also be a function, but as a class (with a "wrong" name) it's
|
||||
much cleaner to implement.
|
||||
@@ -52,18 +52,15 @@ class change_filter: # pylint: disable=invalid-name
|
||||
Attributes:
|
||||
_sectname: The section to be filtered.
|
||||
_optname: The option to be filtered.
|
||||
_function: Whether a function rather than a method is decorated.
|
||||
"""
|
||||
|
||||
def __init__(self, sectname, optname=None, function=False):
|
||||
def __init__(self, sectname, optname=None):
|
||||
"""Save decorator arguments.
|
||||
|
||||
Gets called on parse-time with the decorator arguments.
|
||||
|
||||
Args:
|
||||
sectname: The section to be filtered.
|
||||
optname: The option to be filtered.
|
||||
function: Whether a function rather than a method is decorated.
|
||||
See class attributes.
|
||||
"""
|
||||
if sectname not in configdata.DATA:
|
||||
raise configexc.NoSectionError(sectname)
|
||||
@@ -71,10 +68,9 @@ class change_filter: # pylint: disable=invalid-name
|
||||
raise configexc.NoOptionError(optname, sectname)
|
||||
self._sectname = sectname
|
||||
self._optname = optname
|
||||
self._function = function
|
||||
|
||||
def __call__(self, func):
|
||||
"""Filter calls to the decorated function.
|
||||
"""Register the command before running the function.
|
||||
|
||||
Gets called when a function should be decorated.
|
||||
|
||||
@@ -89,34 +85,20 @@ class change_filter: # pylint: disable=invalid-name
|
||||
Return:
|
||||
The decorated function.
|
||||
"""
|
||||
if self._function:
|
||||
@pyqtSlot(str, str)
|
||||
@functools.wraps(func)
|
||||
def wrapper(sectname=None, optname=None):
|
||||
# pylint: disable=missing-docstring
|
||||
if sectname is None and optname is None:
|
||||
# Called directly, not from a config change event.
|
||||
return func()
|
||||
elif sectname != self._sectname:
|
||||
return
|
||||
elif self._optname is not None and optname != self._optname:
|
||||
return
|
||||
else:
|
||||
return func()
|
||||
else:
|
||||
@pyqtSlot(str, str)
|
||||
@functools.wraps(func)
|
||||
def wrapper(wrapper_self, sectname=None, optname=None):
|
||||
# pylint: disable=missing-docstring
|
||||
if sectname is None and optname is None:
|
||||
# Called directly, not from a config change event.
|
||||
return func(wrapper_self)
|
||||
elif sectname != self._sectname:
|
||||
return
|
||||
elif self._optname is not None and optname != self._optname:
|
||||
return
|
||||
else:
|
||||
return func(wrapper_self)
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
@functools.wraps(func)
|
||||
def wrapper(wrapper_self, sectname=None, optname=None):
|
||||
# pylint: disable=missing-docstring
|
||||
if sectname is None and optname is None:
|
||||
# Called directly, not from a config change event.
|
||||
return func(wrapper_self)
|
||||
elif sectname != self._sectname:
|
||||
return
|
||||
elif self._optname is not None and optname != self._optname:
|
||||
return
|
||||
else:
|
||||
return func(wrapper_self)
|
||||
|
||||
return wrapper
|
||||
|
||||
@@ -131,17 +113,17 @@ def section(sect):
|
||||
return objreg.get('config')[sect]
|
||||
|
||||
|
||||
def _init_main_config(parent=None):
|
||||
"""Initialize the main config.
|
||||
def init(args):
|
||||
"""Initialize the config.
|
||||
|
||||
Args:
|
||||
parent: The parent to pass to ConfigManager.
|
||||
args: The argparse namespace.
|
||||
"""
|
||||
args = objreg.get('args')
|
||||
confdir = standarddir.get(QStandardPaths.ConfigLocation, args)
|
||||
try:
|
||||
config_obj = ConfigManager(standarddir.config(), 'qutebrowser.conf',
|
||||
args.relaxed_config, parent=parent)
|
||||
except (configexc.Error, configparser.Error, UnicodeDecodeError) as e:
|
||||
app = objreg.get('app')
|
||||
config_obj = ConfigManager(confdir, 'qutebrowser.conf', app)
|
||||
except (configexc.Error, configparser.Error) as e:
|
||||
log.init.exception(e)
|
||||
errstr = "Error while reading config:"
|
||||
try:
|
||||
@@ -149,126 +131,38 @@ def _init_main_config(parent=None):
|
||||
e.section, e.option) # pylint: disable=no-member
|
||||
except AttributeError:
|
||||
pass
|
||||
errstr += "\n"
|
||||
error.handle_fatal_exc(e, args, "Error while reading config!",
|
||||
pre_text=errstr)
|
||||
errstr += "\n{}".format(e)
|
||||
msgbox = QMessageBox(QMessageBox.Critical,
|
||||
"Error while reading config!", errstr)
|
||||
msgbox.exec_()
|
||||
# We didn't really initialize much so far, so we just quit hard.
|
||||
sys.exit(usertypes.Exit.err_config)
|
||||
sys.exit(1)
|
||||
else:
|
||||
objreg.register('config', config_obj)
|
||||
if standarddir.config() is not None:
|
||||
filename = os.path.join(standarddir.config(), 'qutebrowser.conf')
|
||||
save_manager = objreg.get('save-manager')
|
||||
save_manager.add_saveable(
|
||||
'config', config_obj.save, config_obj.changed,
|
||||
config_opt=('general', 'auto-save-config'), filename=filename)
|
||||
for sect in config_obj.sections.values():
|
||||
for opt in sect.values.values():
|
||||
if opt.values['conf'] is None:
|
||||
# Option added to built-in defaults but not in user's
|
||||
# config yet
|
||||
save_manager.save('config', explicit=True, force=True)
|
||||
return
|
||||
|
||||
|
||||
def _init_key_config(parent):
|
||||
"""Initialize the key config.
|
||||
|
||||
Args:
|
||||
parent: The parent to use for the KeyConfigParser.
|
||||
"""
|
||||
args = objreg.get('args')
|
||||
try:
|
||||
key_config = keyconf.KeyConfigParser(standarddir.config(), 'keys.conf',
|
||||
args.relaxed_config,
|
||||
parent=parent)
|
||||
except (keyconf.KeyConfigError, UnicodeDecodeError) as e:
|
||||
key_config = keyconf.KeyConfigParser(confdir, 'keys.conf')
|
||||
except keyconf.KeyConfigError as e:
|
||||
log.init.exception(e)
|
||||
errstr = "Error while reading key config:\n"
|
||||
if e.lineno is not None:
|
||||
errstr += "In line {}: ".format(e.lineno)
|
||||
error.handle_fatal_exc(e, args, "Error while reading key config!",
|
||||
pre_text=errstr)
|
||||
errstr += str(e)
|
||||
msgbox = QMessageBox(QMessageBox.Critical,
|
||||
"Error while reading key config!", errstr)
|
||||
msgbox.exec_()
|
||||
# We didn't really initialize much so far, so we just quit hard.
|
||||
sys.exit(usertypes.Exit.err_key_config)
|
||||
sys.exit(1)
|
||||
else:
|
||||
objreg.register('key-config', key_config)
|
||||
if standarddir.config() is not None:
|
||||
save_manager = objreg.get('save-manager')
|
||||
filename = os.path.join(standarddir.config(), 'keys.conf')
|
||||
save_manager.add_saveable(
|
||||
'key-config', key_config.save, key_config.config_dirty,
|
||||
config_opt=('general', 'auto-save-config'), filename=filename,
|
||||
dirty=key_config.is_dirty)
|
||||
|
||||
|
||||
def _init_misc():
|
||||
"""Initialize misc. config-related files."""
|
||||
save_manager = objreg.get('save-manager')
|
||||
state_config = ini.ReadWriteConfigParser(standarddir.data(), 'state')
|
||||
for sect in ('general', 'geometry'):
|
||||
try:
|
||||
state_config.add_section(sect)
|
||||
except configparser.DuplicateSectionError:
|
||||
pass
|
||||
# See commit a98060e020a4ba83b663813a4b9404edb47f28ad.
|
||||
state_config['general'].pop('fooled', None)
|
||||
datadir = standarddir.get(QStandardPaths.DataLocation, args)
|
||||
state_config = ini.ReadWriteConfigParser(datadir, 'state')
|
||||
objreg.register('state-config', state_config)
|
||||
save_manager.add_saveable('state-config', state_config.save)
|
||||
|
||||
# We need to import this here because lineparser needs config.
|
||||
from qutebrowser.misc import lineparser
|
||||
command_history = lineparser.LimitLineParser(
|
||||
standarddir.data(), 'cmd-history',
|
||||
limit=('completion', 'cmd-history-max-items'),
|
||||
parent=objreg.get('config'))
|
||||
from qutebrowser.config.parsers import line
|
||||
command_history = line.LineConfigParser(datadir, 'cmd-history',
|
||||
('completion', 'history-length'))
|
||||
objreg.register('command-history', command_history)
|
||||
save_manager.add_saveable('command-history', command_history.save,
|
||||
command_history.changed)
|
||||
|
||||
# Set the QSettings path to something like
|
||||
# ~/.config/qutebrowser/qsettings/qutebrowser/qutebrowser.conf so it
|
||||
# doesn't overwrite our config.
|
||||
#
|
||||
# This fixes one of the corruption issues here:
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/515
|
||||
|
||||
if standarddir.config() is None:
|
||||
path = os.devnull
|
||||
else:
|
||||
path = os.path.join(standarddir.config(), 'qsettings')
|
||||
for fmt in (QSettings.NativeFormat, QSettings.IniFormat):
|
||||
QSettings.setPath(fmt, QSettings.UserScope, path)
|
||||
|
||||
|
||||
def init(parent=None):
|
||||
"""Initialize the config.
|
||||
|
||||
Args:
|
||||
parent: The parent to pass to QObjects which get initialized.
|
||||
"""
|
||||
_init_main_config(parent)
|
||||
_init_key_config(parent)
|
||||
_init_misc()
|
||||
|
||||
|
||||
def _get_value_transformer(old, new):
|
||||
"""Get a function which transforms a value for CHANGED_OPTIONS.
|
||||
|
||||
Args:
|
||||
old: The old value - if the supplied value doesn't match this, it's
|
||||
returned untransformed.
|
||||
new: The new value.
|
||||
|
||||
Return:
|
||||
A function which takes a value and transforms it.
|
||||
"""
|
||||
def transformer(val):
|
||||
if val == old:
|
||||
return new
|
||||
else:
|
||||
return val
|
||||
return transformer
|
||||
|
||||
|
||||
class ConfigManager(QObject):
|
||||
@@ -282,11 +176,6 @@ class ConfigManager(QObject):
|
||||
RENAMED_SECTIONS: A mapping of renamed sections, {'oldname': 'newname'}
|
||||
RENAMED_OPTIONS: A mapping of renamed options,
|
||||
{('section', 'oldname'): 'newname'}
|
||||
CHANGED_OPTIONS: A mapping of arbitrarily changed options,
|
||||
{('section', 'option'): callable}.
|
||||
The callable takes the old value and returns the new
|
||||
one.
|
||||
DELETED_OPTIONS: A (section, option) list of deleted options.
|
||||
|
||||
Attributes:
|
||||
sections: The configuration data as an OrderedDict.
|
||||
@@ -319,27 +208,16 @@ class ConfigManager(QObject):
|
||||
('colors', 'tab.indicator.stop'): 'tabs.indicator.stop',
|
||||
('colors', 'tab.indicator.error'): 'tabs.indicator.error',
|
||||
('colors', 'tab.indicator.system'): 'tabs.indicator.system',
|
||||
('tabs', 'auto-hide'): 'hide-auto',
|
||||
('completion', 'history-length'): 'cmd-history-max-items',
|
||||
('colors', 'downloads.fg'): 'downloads.fg.start',
|
||||
}
|
||||
DELETED_OPTIONS = [
|
||||
('colors', 'tab.separator'),
|
||||
('colors', 'tabs.separator'),
|
||||
('colors', 'completion.item.bg'),
|
||||
]
|
||||
CHANGED_OPTIONS = {
|
||||
('content', 'cookies-accept'):
|
||||
_get_value_transformer('default', 'no-3rdparty'),
|
||||
('colors', 'tab.seperator'): 'tabs.seperator',
|
||||
}
|
||||
|
||||
changed = pyqtSignal(str, str)
|
||||
style_changed = pyqtSignal(str, str)
|
||||
|
||||
def __init__(self, configdir, fname, relaxed=False, parent=None):
|
||||
def __init__(self, configdir, fname, parent=None):
|
||||
super().__init__(parent)
|
||||
self._initialized = False
|
||||
self.sections = configdata.data()
|
||||
self.sections = configdata.DATA
|
||||
self._interpolation = configparser.ExtendedInterpolation()
|
||||
self._proxies = {}
|
||||
for sectname in self.sections.keys():
|
||||
@@ -351,7 +229,7 @@ class ConfigManager(QObject):
|
||||
else:
|
||||
self._configdir = configdir
|
||||
parser = ini.ReadConfigParser(configdir, fname)
|
||||
self._from_cp(parser, relaxed)
|
||||
self._from_cp(parser)
|
||||
self._initialized = True
|
||||
self._validate_all()
|
||||
|
||||
@@ -401,7 +279,7 @@ class ConfigManager(QObject):
|
||||
try:
|
||||
desc = self.sections[sectname].descriptions[optname]
|
||||
except KeyError:
|
||||
log.config.exception("No description for {}.{}!".format(
|
||||
log.misc.exception("No description for {}.{}!".format(
|
||||
sectname, optname))
|
||||
continue
|
||||
for descline in desc.splitlines():
|
||||
@@ -460,52 +338,27 @@ class ConfigManager(QObject):
|
||||
else:
|
||||
return None
|
||||
|
||||
def _from_cp(self, cp, relaxed=False):
|
||||
def _from_cp(self, cp):
|
||||
"""Read the config from a configparser instance.
|
||||
|
||||
Args:
|
||||
cp: The configparser instance to read the values from.
|
||||
relaxed: Whether to ignore inexistent sections/options.
|
||||
"""
|
||||
for sectname in cp:
|
||||
if sectname in self.RENAMED_SECTIONS:
|
||||
sectname = self.RENAMED_SECTIONS[sectname]
|
||||
if sectname is not 'DEFAULT' and sectname not in self.sections:
|
||||
if not relaxed:
|
||||
raise configexc.NoSectionError(sectname)
|
||||
raise configexc.NoSectionError(sectname)
|
||||
for sectname in self.sections:
|
||||
self._from_cp_section(sectname, cp, relaxed)
|
||||
|
||||
def _from_cp_section(self, sectname, cp, relaxed):
|
||||
"""Read a single section from a configparser instance.
|
||||
|
||||
Args:
|
||||
sectname: The name of the section to read.
|
||||
cp: The configparser instance to read the values from.
|
||||
relaxed: Whether to ignore inexistent options.
|
||||
"""
|
||||
real_sectname = self._get_real_sectname(cp, sectname)
|
||||
if real_sectname is None:
|
||||
return
|
||||
for k, v in cp[real_sectname].items():
|
||||
if k.startswith(self.ESCAPE_CHAR):
|
||||
k = k[1:]
|
||||
|
||||
if (sectname, k) in self.DELETED_OPTIONS:
|
||||
return
|
||||
if (sectname, k) in self.RENAMED_OPTIONS:
|
||||
k = self.RENAMED_OPTIONS[sectname, k]
|
||||
if (sectname, k) in self.CHANGED_OPTIONS:
|
||||
func = self.CHANGED_OPTIONS[(sectname, k)]
|
||||
v = func(v)
|
||||
|
||||
try:
|
||||
real_sectname = self._get_real_sectname(cp, sectname)
|
||||
if real_sectname is None:
|
||||
continue
|
||||
for k, v in cp[real_sectname].items():
|
||||
if k.startswith(self.ESCAPE_CHAR):
|
||||
k = k[1:]
|
||||
if (sectname, k) in self.RENAMED_OPTIONS:
|
||||
k = self.RENAMED_OPTIONS[sectname, k]
|
||||
self.set('conf', sectname, k, v, validate=False)
|
||||
except configexc.NoOptionError:
|
||||
if relaxed:
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
|
||||
def _validate_all(self):
|
||||
"""Validate all values set in self._from_cp."""
|
||||
@@ -523,7 +376,7 @@ class ConfigManager(QObject):
|
||||
|
||||
def _changed(self, sectname, optname):
|
||||
"""Notify other objects the config has changed."""
|
||||
log.config.debug("Config option changed: {} -> {}".format(
|
||||
log.misc.debug("Config option changed: {} -> {}".format(
|
||||
sectname, optname))
|
||||
if sectname in ('colors', 'fonts'):
|
||||
self.style_changed.emit(sectname, optname)
|
||||
@@ -547,7 +400,7 @@ class ConfigManager(QObject):
|
||||
def items(self, sectname, raw=True):
|
||||
"""Get a list of (optname, value) tuples for a section.
|
||||
|
||||
Implemented for configparser interpolation compatibility
|
||||
Implemented for configparser interpolation compatbility.
|
||||
|
||||
Args:
|
||||
sectname: The name of the section to get.
|
||||
@@ -610,7 +463,7 @@ class ConfigManager(QObject):
|
||||
The value of the option.
|
||||
"""
|
||||
if not self._initialized:
|
||||
raise Exception("get got called before initialization was "
|
||||
raise Exception("get got called before initialisation was "
|
||||
"complete!")
|
||||
try:
|
||||
sect = self.sections[sectname]
|
||||
@@ -629,66 +482,50 @@ class ConfigManager(QObject):
|
||||
newval = val.typ.transform(newval)
|
||||
return newval
|
||||
|
||||
@cmdutils.register(name='set', instance='config', win_id='win_id',
|
||||
@cmdutils.register(name='set', instance='config',
|
||||
completion=[Completion.section, Completion.option,
|
||||
Completion.value])
|
||||
def set_command(self, win_id, section_=None, option=None, value=None,
|
||||
temp=False, print_=False):
|
||||
def set_command(self, win_id: {'special': 'win_id'},
|
||||
sectname: {'name': 'section'}=None,
|
||||
optname: {'name': 'option'}=None, value=None, temp=False):
|
||||
"""Set an option.
|
||||
|
||||
If the option name ends with '?', the value of the option is shown
|
||||
instead.
|
||||
|
||||
If the option name ends with '!' and it is a boolean value, toggle it.
|
||||
|
||||
//
|
||||
|
||||
Wrapper for self.set() to output exceptions in the status bar.
|
||||
|
||||
Args:
|
||||
section_: The section where the option is in.
|
||||
option: The name of the option.
|
||||
sectname: The section where the option is in.
|
||||
optname: The name of the option.
|
||||
value: The value to set.
|
||||
temp: Set value temporarily.
|
||||
print_: Print the value after setting.
|
||||
"""
|
||||
if section_ is not None and option is None:
|
||||
if sectname is not None and optname is None:
|
||||
raise cmdexc.CommandError(
|
||||
"set: Either both section and option have to be given, or "
|
||||
"neither!")
|
||||
if section_ is None and option is None:
|
||||
if sectname is None and optname is None:
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
tabbed_browser.openurl(QUrl('qute:settings'), newtab=False)
|
||||
return
|
||||
|
||||
if option.endswith('?'):
|
||||
option = option[:-1]
|
||||
print_ = True
|
||||
else:
|
||||
try:
|
||||
if option.endswith('!') and value is None:
|
||||
val = self.get(section_, option[:-1])
|
||||
layer = 'temp' if temp else 'conf'
|
||||
if isinstance(val, bool):
|
||||
self.set(layer, section_, option[:-1], str(not val))
|
||||
else:
|
||||
raise cmdexc.CommandError(
|
||||
"set: Attempted inversion of non-boolean value.")
|
||||
elif value is not None:
|
||||
layer = 'temp' if temp else 'conf'
|
||||
self.set(layer, section_, option, value)
|
||||
else:
|
||||
try:
|
||||
if optname.endswith('?'):
|
||||
val = self.get(sectname, optname[:-1], transformed=False)
|
||||
message.info(win_id, "{} {} = {}".format(
|
||||
sectname, optname[:-1], val), immediately=True)
|
||||
else:
|
||||
if value is None:
|
||||
raise cmdexc.CommandError("set: The following arguments "
|
||||
"are required: value")
|
||||
except (configexc.Error, configparser.Error) as e:
|
||||
raise cmdexc.CommandError("set: {} - {}".format(
|
||||
e.__class__.__name__, e))
|
||||
|
||||
if print_:
|
||||
val = self.get(section_, option, transformed=False)
|
||||
message.info(win_id, "{} {} = {}".format(
|
||||
section_, option, val), immediately=True)
|
||||
layer = 'temp' if temp else 'conf'
|
||||
self.set(layer, sectname, optname, value)
|
||||
except (configexc.Error, configparser.Error) as e:
|
||||
raise cmdexc.CommandError("set: {} - {}".format(
|
||||
e.__class__.__name__, e))
|
||||
|
||||
def set(self, layer, sectname, optname, value, validate=True):
|
||||
"""Set an option.
|
||||
@@ -723,6 +560,14 @@ class ConfigManager(QObject):
|
||||
if self._initialized:
|
||||
self._after_set(sectname, optname)
|
||||
|
||||
@cmdutils.register(instance='config', name='save')
|
||||
def save_command(self):
|
||||
"""Save the config file."""
|
||||
try:
|
||||
self.save()
|
||||
except OSError as e:
|
||||
raise cmdexc.CommandError("Could not save config: {}".format(e))
|
||||
|
||||
def save(self):
|
||||
"""Save the config file."""
|
||||
if self._configdir is None:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -32,9 +32,9 @@ class ValidationError(Error):
|
||||
"""Raised when a value for a config type was invalid.
|
||||
|
||||
Attributes:
|
||||
section: Section in which the error occurred (added when catching and
|
||||
section: Section in which the error occured (added when catching and
|
||||
re-raising the exception).
|
||||
option: Option in which the error occurred.
|
||||
option: Option in which the error occured.
|
||||
"""
|
||||
|
||||
def __init__(self, value, msg):
|
||||
|
||||
@@ -25,7 +25,6 @@ import base64
|
||||
import codecs
|
||||
import os.path
|
||||
import sre_constants
|
||||
import itertools
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtGui import QColor, QFont
|
||||
@@ -34,15 +33,10 @@ from PyQt5.QtWidgets import QTabWidget, QTabBar
|
||||
|
||||
from qutebrowser.commands import cmdutils
|
||||
from qutebrowser.config import configexc
|
||||
from qutebrowser.utils import standarddir
|
||||
|
||||
|
||||
SYSTEM_PROXY = object() # Return value for Proxy type
|
||||
|
||||
# Taken from configparser
|
||||
BOOLEAN_STATES = {'1': True, 'yes': True, 'true': True, 'on': True,
|
||||
'0': False, 'no': False, 'false': False, 'off': False}
|
||||
|
||||
|
||||
class ValidValues:
|
||||
|
||||
@@ -226,17 +220,25 @@ class List(BaseType):
|
||||
|
||||
class Bool(BaseType):
|
||||
|
||||
"""Base class for a boolean setting."""
|
||||
"""Base class for a boolean setting.
|
||||
|
||||
Class attributes:
|
||||
_BOOLEAN_STATES: A dictionary of strings mapped to their bool meanings.
|
||||
"""
|
||||
|
||||
typestr = 'bool'
|
||||
|
||||
# Taken from configparser
|
||||
_BOOLEAN_STATES = {'1': True, 'yes': True, 'true': True, 'on': True,
|
||||
'0': False, 'no': False, 'false': False, 'off': False}
|
||||
|
||||
valid_values = ValidValues('true', 'false')
|
||||
|
||||
def transform(self, value):
|
||||
if not value:
|
||||
return None
|
||||
else:
|
||||
return BOOLEAN_STATES[value.lower()]
|
||||
return Bool._BOOLEAN_STATES[value.lower()]
|
||||
|
||||
def validate(self, value):
|
||||
if not value:
|
||||
@@ -244,7 +246,7 @@ class Bool(BaseType):
|
||||
return
|
||||
else:
|
||||
raise configexc.ValidationError(value, "may not be empty!")
|
||||
if value.lower() not in BOOLEAN_STATES:
|
||||
if value.lower() not in Bool._BOOLEAN_STATES:
|
||||
raise configexc.ValidationError(value, "must be a boolean!")
|
||||
|
||||
|
||||
@@ -659,9 +661,9 @@ class Font(BaseType):
|
||||
) |
|
||||
# size (<float>pt | <int>px)
|
||||
(?P<size>[0-9]+((\.[0-9]+)?[pP][tT]|[pP][xX]))
|
||||
)\ # size/weight/style are space-separated
|
||||
)* # 0-inf size/weight/style tags
|
||||
(?P<family>[A-Za-z0-9, "-]*)$ # mandatory font family""", re.VERBOSE)
|
||||
)\ # size/weight/style are space-separated
|
||||
)* # 0-inf size/weight/style tags
|
||||
(?P<family>[A-Za-z, "-]*)$ # mandatory font family""", re.VERBOSE)
|
||||
|
||||
def validate(self, value):
|
||||
if not value:
|
||||
@@ -673,28 +675,9 @@ class Font(BaseType):
|
||||
raise configexc.ValidationError(value, "must be a valid font")
|
||||
|
||||
|
||||
class FontFamily(Font):
|
||||
|
||||
"""A Qt font family."""
|
||||
|
||||
def validate(self, value):
|
||||
if not value:
|
||||
if self._none_ok:
|
||||
return
|
||||
else:
|
||||
raise configexc.ValidationError(value, "may not be empty!")
|
||||
match = self.font_regex.match(value)
|
||||
if not match:
|
||||
raise configexc.ValidationError(value, "must be a valid font")
|
||||
for group in 'style', 'weight', 'namedweight', 'size':
|
||||
if match.group(group):
|
||||
raise configexc.ValidationError(value, "may not include a "
|
||||
"{}!".format(group))
|
||||
|
||||
|
||||
class QtFont(Font):
|
||||
|
||||
"""A Font which gets converted to a QFont."""
|
||||
"""A Font which gets converted to q QFont."""
|
||||
|
||||
def transform(self, value):
|
||||
if not value:
|
||||
@@ -799,17 +782,6 @@ class File(BaseType):
|
||||
|
||||
typestr = 'file'
|
||||
|
||||
def transform(self, value):
|
||||
if not value:
|
||||
return None
|
||||
value = os.path.expanduser(value)
|
||||
value = os.path.expandvars(value)
|
||||
if not os.path.isabs(value):
|
||||
cfgdir = standarddir.config()
|
||||
if cfgdir is not None:
|
||||
return os.path.join(cfgdir, value)
|
||||
return value
|
||||
|
||||
def validate(self, value):
|
||||
if not value:
|
||||
if self._none_ok:
|
||||
@@ -817,25 +789,15 @@ class File(BaseType):
|
||||
else:
|
||||
raise configexc.ValidationError(value, "may not be empty!")
|
||||
value = os.path.expanduser(value)
|
||||
value = os.path.expandvars(value)
|
||||
try:
|
||||
if not os.path.isabs(value):
|
||||
cfgdir = standarddir.config()
|
||||
if cfgdir is None:
|
||||
raise configexc.ValidationError(
|
||||
value, "must be an absolute path when not using a "
|
||||
"config directory!")
|
||||
elif not os.path.isfile(os.path.join(cfgdir, value)):
|
||||
raise configexc.ValidationError(
|
||||
value, "must be a valid path relative to the config "
|
||||
"directory!")
|
||||
else:
|
||||
return
|
||||
elif not os.path.isfile(value):
|
||||
raise configexc.ValidationError(
|
||||
value, "must be a valid file!")
|
||||
except UnicodeEncodeError as e:
|
||||
raise configexc.ValidationError(value, e)
|
||||
if not os.path.isfile(value):
|
||||
raise configexc.ValidationError(value, "must be a valid file!")
|
||||
if not os.path.isabs(value):
|
||||
raise configexc.ValidationError(value, "must be an absolute path!")
|
||||
|
||||
def transform(self, value):
|
||||
if not value:
|
||||
return None
|
||||
return os.path.expanduser(value)
|
||||
|
||||
|
||||
class Directory(BaseType):
|
||||
@@ -850,51 +812,19 @@ class Directory(BaseType):
|
||||
return
|
||||
else:
|
||||
raise configexc.ValidationError(value, "may not be empty!")
|
||||
value = os.path.expandvars(value)
|
||||
value = os.path.expanduser(value)
|
||||
try:
|
||||
if not os.path.isdir(value):
|
||||
raise configexc.ValidationError(
|
||||
value, "must be a valid directory!")
|
||||
if not os.path.isabs(value):
|
||||
raise configexc.ValidationError(
|
||||
value, "must be an absolute path!")
|
||||
except UnicodeEncodeError as e:
|
||||
raise configexc.ValidationError(value, e)
|
||||
if not os.path.isdir(value):
|
||||
raise configexc.ValidationError(value, "must be a valid "
|
||||
"directory!")
|
||||
if not os.path.isabs(value):
|
||||
raise configexc.ValidationError(value, "must be an absolute path!")
|
||||
|
||||
def transform(self, value):
|
||||
if not value:
|
||||
return None
|
||||
value = os.path.expandvars(value)
|
||||
return os.path.expanduser(value)
|
||||
|
||||
|
||||
class FormatString(BaseType):
|
||||
|
||||
"""A string with '{foo}'-placeholders."""
|
||||
|
||||
typestr = 'format-string'
|
||||
|
||||
def __init__(self, fields, none_ok=False):
|
||||
super().__init__(none_ok)
|
||||
self.fields = fields
|
||||
|
||||
def validate(self, value):
|
||||
if not value:
|
||||
if self._none_ok:
|
||||
return
|
||||
else:
|
||||
raise configexc.ValidationError(value, "may not be empty!")
|
||||
s = self.transform(value)
|
||||
try:
|
||||
return s.format(**{k: '' for k in self.fields})
|
||||
except KeyError as e:
|
||||
raise configexc.ValidationError(value, "Invalid placeholder "
|
||||
"{}".format(e))
|
||||
except ValueError as e:
|
||||
raise configexc.ValidationError(value, str(e))
|
||||
|
||||
|
||||
class WebKitBytes(BaseType):
|
||||
|
||||
"""A size with an optional suffix.
|
||||
@@ -1110,45 +1040,14 @@ class SearchEngineUrl(BaseType):
|
||||
return
|
||||
else:
|
||||
raise configexc.ValidationError(value, "may not be empty!")
|
||||
|
||||
if '{}' not in value:
|
||||
raise configexc.ValidationError(value, "must contain \"{}\"")
|
||||
try:
|
||||
value.format("")
|
||||
except KeyError:
|
||||
raise configexc.ValidationError(
|
||||
value, "may not contain {...} (use {{ and }} for literal {/})")
|
||||
|
||||
url = QUrl(value.replace('{}', 'foobar'))
|
||||
if not url.isValid():
|
||||
raise configexc.ValidationError(value, "invalid url, {}".format(
|
||||
url.errorString()))
|
||||
|
||||
|
||||
class FuzzyUrl(BaseType):
|
||||
|
||||
"""A single URL."""
|
||||
|
||||
def validate(self, value):
|
||||
from qutebrowser.utils import urlutils
|
||||
if not value:
|
||||
if self._none_ok:
|
||||
return
|
||||
else:
|
||||
raise configexc.ValidationError(value, "may not be empty!")
|
||||
try:
|
||||
self.transform(value)
|
||||
except urlutils.FuzzyUrlError as e:
|
||||
raise configexc.ValidationError(value, str(e))
|
||||
|
||||
def transform(self, value):
|
||||
from qutebrowser.utils import urlutils
|
||||
if not value:
|
||||
return None
|
||||
else:
|
||||
return urlutils.fuzzy_url(value, do_search=False)
|
||||
|
||||
|
||||
class Encoding(BaseType):
|
||||
|
||||
"""Setting for a python encoding."""
|
||||
@@ -1176,36 +1075,35 @@ class UserStyleSheet(File):
|
||||
def __init__(self):
|
||||
super().__init__(none_ok=True)
|
||||
|
||||
def transform(self, value):
|
||||
if not value:
|
||||
return None
|
||||
path = super().transform(value)
|
||||
if os.path.exists(path):
|
||||
return QUrl.fromLocalFile(path)
|
||||
else:
|
||||
data = base64.b64encode(value.encode('utf-8')).decode('ascii')
|
||||
return QUrl("data:text/css;charset=utf-8;base64,{}".format(data))
|
||||
|
||||
def validate(self, value):
|
||||
if not value:
|
||||
if self._none_ok:
|
||||
return
|
||||
else:
|
||||
raise configexc.ValidationError(value, "may not be empty!")
|
||||
value = os.path.expandvars(value)
|
||||
value = os.path.expanduser(value)
|
||||
try:
|
||||
super().validate(value)
|
||||
except configexc.ValidationError:
|
||||
if not os.path.isabs(value):
|
||||
# probably a CSS, so we don't handle it as filename.
|
||||
# FIXME We just try if it is encodable, maybe we should validate
|
||||
# CSS?
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/115
|
||||
try:
|
||||
if not os.path.isabs(value):
|
||||
# probably a CSS, so we don't handle it as filename.
|
||||
# FIXME We just try if it is encodable, maybe we should
|
||||
# validate CSS?
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/115
|
||||
value.encode('utf-8')
|
||||
value.encode('utf-8')
|
||||
except UnicodeEncodeError as e:
|
||||
raise configexc.ValidationError(value, str(e))
|
||||
return
|
||||
elif not os.path.isfile(value):
|
||||
raise configexc.ValidationError(value, "must be a valid file!")
|
||||
|
||||
def transform(self, value):
|
||||
path = os.path.expanduser(value)
|
||||
if not value:
|
||||
return None
|
||||
elif os.path.isabs(path):
|
||||
return QUrl.fromLocalFile(path)
|
||||
else:
|
||||
data = base64.b64encode(value.encode('utf-8')).decode('ascii')
|
||||
return QUrl("data:text/css;charset=utf-8;base64,{}".format(data))
|
||||
|
||||
|
||||
class AutoSearch(BaseType):
|
||||
@@ -1257,13 +1155,6 @@ class Position(BaseType):
|
||||
return self.MAPPING[value]
|
||||
|
||||
|
||||
class VerticalPosition(BaseType):
|
||||
|
||||
"""The position of the download bar."""
|
||||
|
||||
valid_values = ValidValues('north', 'south')
|
||||
|
||||
|
||||
class UrlList(List):
|
||||
|
||||
"""A list of URLs."""
|
||||
@@ -1294,22 +1185,6 @@ class UrlList(List):
|
||||
"{}".format(val.errorString()))
|
||||
|
||||
|
||||
class SessionName(BaseType):
|
||||
|
||||
"""The name of a session."""
|
||||
|
||||
typestr = 'session'
|
||||
|
||||
def validate(self, value):
|
||||
if not value:
|
||||
if self._none_ok:
|
||||
return
|
||||
else:
|
||||
raise configexc.ValidationError(value, "may not be empty!")
|
||||
if value.startswith('_'):
|
||||
raise configexc.ValidationError(value, "may not start with '_'!")
|
||||
|
||||
|
||||
class SelectOnRemove(BaseType):
|
||||
|
||||
"""Which tab to select when the focused tab is removed."""
|
||||
@@ -1333,77 +1208,29 @@ class SelectOnRemove(BaseType):
|
||||
|
||||
class LastClose(BaseType):
|
||||
|
||||
"""Behavior when the last tab is closed."""
|
||||
"""Behaviour when the last tab is closed."""
|
||||
|
||||
valid_values = ValidValues(('ignore', "Don't do anything."),
|
||||
('blank', "Load a blank page."),
|
||||
('startpage', "Load the start page."),
|
||||
('default-page', "Load the default page."),
|
||||
('close', "Close the window."))
|
||||
|
||||
|
||||
class AcceptCookies(BaseType):
|
||||
|
||||
"""Control which cookies to accept."""
|
||||
"""Whether to accept a cookie."""
|
||||
|
||||
valid_values = ValidValues(('all', "Accept all cookies."),
|
||||
('no-3rdparty', "Accept cookies from the same"
|
||||
" origin only."),
|
||||
('no-unknown-3rdparty', "Accept cookies from "
|
||||
"the same origin only, unless a cookie is "
|
||||
"already set for the domain."),
|
||||
valid_values = ValidValues(('default', "Default QtWebKit behaviour."),
|
||||
('never', "Don't accept cookies at all."))
|
||||
|
||||
|
||||
class ConfirmQuit(List):
|
||||
class ConfirmQuit(BaseType):
|
||||
|
||||
"""Whether to display a confirmation when the window is closed."""
|
||||
|
||||
typestr = 'string-list'
|
||||
|
||||
valid_values = ValidValues(('always', "Always show a confirmation."),
|
||||
('multiple-tabs', "Show a confirmation if "
|
||||
"multiple tabs are opened."),
|
||||
('downloads', "Show a confirmation if "
|
||||
"downloads are running"),
|
||||
('never', "Never show a confirmation."))
|
||||
# Values that can be combined with commas
|
||||
combinable_values = ('multiple-tabs', 'downloads')
|
||||
|
||||
def validate(self, value):
|
||||
values = self.transform(value)
|
||||
# Never can't be set with other options
|
||||
if 'never' in values and len(values) > 1:
|
||||
raise configexc.ValidationError(
|
||||
value, "List cannot contain never!")
|
||||
# Always can't be set with other options
|
||||
elif 'always' in values and len(values) > 1:
|
||||
raise configexc.ValidationError(
|
||||
value, "List cannot contain always!")
|
||||
# Values have to be valid
|
||||
elif not set(values).issubset(set(self.valid_values.values)):
|
||||
raise configexc.ValidationError(
|
||||
value, "List contains invalid values!")
|
||||
# List can't have duplicates
|
||||
elif len(set(values)) != len(values):
|
||||
raise configexc.ValidationError(
|
||||
value, "List contains duplicate values!")
|
||||
|
||||
def complete(self):
|
||||
combinations = []
|
||||
# Generate combinations of the options that can be combined
|
||||
for size in range(2, len(self.combinable_values) + 1):
|
||||
combinations += list(
|
||||
itertools.combinations(self.combinable_values, size))
|
||||
out = []
|
||||
# Add valid single values
|
||||
for val in self.valid_values:
|
||||
out.append((val, self.valid_values.descriptions[val]))
|
||||
# Add combinations to list of options
|
||||
for val in combinations:
|
||||
desc = ''
|
||||
out.append((','.join(val), desc))
|
||||
return out
|
||||
|
||||
|
||||
class ForwardUnboundKeys(BaseType):
|
||||
@@ -1462,95 +1289,8 @@ class NewInstanceOpenTarget(BaseType):
|
||||
"""How to open links in an existing instance if a new one is launched."""
|
||||
|
||||
valid_values = ValidValues(('tab', "Open a new tab in the existing "
|
||||
"window and activate the window."),
|
||||
('tab-bg', "Open a new background tab in the "
|
||||
"existing window and activate the "
|
||||
"window."),
|
||||
"window and activate it."),
|
||||
('tab-silent', "Open a new tab in the existing "
|
||||
"window without activating "
|
||||
"the window."),
|
||||
('tab-bg-silent', "Open a new background tab "
|
||||
"in the existing window "
|
||||
"without activating the "
|
||||
"window."),
|
||||
"it."),
|
||||
('window', "Open in a new window."))
|
||||
|
||||
|
||||
class DownloadPathSuggestion(BaseType):
|
||||
|
||||
"""How to format the question when downloading."""
|
||||
|
||||
valid_values = ValidValues(('path', "Show only the download path."),
|
||||
('filename', "Show only download filename."),
|
||||
('both', "Show download path and filename."))
|
||||
|
||||
|
||||
class UserAgent(BaseType):
|
||||
|
||||
"""The user agent to use."""
|
||||
|
||||
typestr = 'user-agent'
|
||||
|
||||
def __init__(self, none_ok=False):
|
||||
super().__init__(none_ok)
|
||||
|
||||
def validate(self, value):
|
||||
if not value:
|
||||
if self._none_ok:
|
||||
return
|
||||
else:
|
||||
raise configexc.ValidationError(value, "may not be empty!")
|
||||
|
||||
def complete(self):
|
||||
"""Complete a list of common user agents."""
|
||||
out = [
|
||||
('Mozilla/5.0 (Windows NT 6.1; WOW64; rv:35.0) Gecko/20100101 '
|
||||
'Firefox/35.0',
|
||||
"Firefox 35.0 Win7 64-bit"),
|
||||
('Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:35.0) Gecko/20100101 '
|
||||
'Firefox/35.0',
|
||||
"Firefox 35.0 Ubuntu"),
|
||||
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:35.0) '
|
||||
'Gecko/20100101 Firefox/35.0',
|
||||
"Firefox 35.0 MacOSX"),
|
||||
|
||||
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) '
|
||||
'AppleWebKit/600.3.18 (KHTML, like Gecko) Version/8.0.3 '
|
||||
'Safari/600.3.18',
|
||||
"Safari 8.0 MacOSX"),
|
||||
|
||||
('Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, '
|
||||
'like Gecko) Chrome/40.0.2214.111 Safari/537.36',
|
||||
"Chrome 40.0 Win7 64-bit"),
|
||||
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) '
|
||||
'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.111 '
|
||||
'Safari/537.36',
|
||||
"Chrome 40.0 MacOSX"),
|
||||
('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 '
|
||||
'(KHTML, like Gecko) Chrome/40.0.2214.111 Safari/537.36',
|
||||
"Chrome 40.0 Linux"),
|
||||
|
||||
('Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like '
|
||||
'Gecko',
|
||||
"IE 11.0 Win7 64-bit"),
|
||||
|
||||
('Mozilla/5.0 (iPhone; CPU iPhone OS 8_1_2 like Mac OS X) '
|
||||
'AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 '
|
||||
'Mobile/12B440 Safari/600.1.4',
|
||||
"Mobile Safari 8.0 iOS"),
|
||||
('Mozilla/5.0 (Android; Mobile; rv:35.0) Gecko/35.0 Firefox/35.0',
|
||||
"Firefox 35, Android"),
|
||||
('Mozilla/5.0 (Linux; Android 5.0.2; One Build/KTU84L.H4) '
|
||||
'AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 '
|
||||
'Chrome/37.0.0.0 Mobile Safari/537.36',
|
||||
"Android Browser"),
|
||||
|
||||
('Mozilla/5.0 (compatible; Googlebot/2.1; '
|
||||
'+http://www.google.com/bot.html',
|
||||
"Google Bot"),
|
||||
('Wget/1.16.1 (linux-gnu)',
|
||||
"wget 1.16.1"),
|
||||
('curl/7.40.0',
|
||||
"curl 7.40.0")
|
||||
]
|
||||
return out
|
||||
|
||||
@@ -47,15 +47,11 @@ class ReadConfigParser(configparser.ConfigParser):
|
||||
self.optionxform = lambda opt: opt # be case-insensitive
|
||||
self._configdir = configdir
|
||||
self._fname = fname
|
||||
if self._configdir is None:
|
||||
self._configfile = None
|
||||
return
|
||||
self._configfile = os.path.join(self._configdir, fname)
|
||||
if not os.path.isfile(self._configfile):
|
||||
return
|
||||
log.init.debug("Reading config from {}".format(self._configfile))
|
||||
if self._configfile is not None:
|
||||
self.read(self._configfile, encoding='utf-8')
|
||||
self.read(self._configfile, encoding='utf-8')
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, constructor=True,
|
||||
@@ -64,12 +60,10 @@ class ReadConfigParser(configparser.ConfigParser):
|
||||
|
||||
class ReadWriteConfigParser(ReadConfigParser):
|
||||
|
||||
"""ConfigParser subclass used for auxiliary config files."""
|
||||
"""ConfigParser subclass used for auxillary config files."""
|
||||
|
||||
def save(self):
|
||||
"""Save the config file."""
|
||||
if self._configdir is None:
|
||||
return
|
||||
if not os.path.exists(self._configdir):
|
||||
os.makedirs(self._configdir, 0o755)
|
||||
log.destroy.debug("Saving config to {}".format(self._configfile))
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
|
||||
import collections
|
||||
import os.path
|
||||
import itertools
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, QObject
|
||||
|
||||
@@ -35,7 +34,7 @@ class KeyConfigError(Exception):
|
||||
"""Raised on errors with the key config.
|
||||
|
||||
Attributes:
|
||||
lineno: The config line in which the exception occurred.
|
||||
lineno: The config line in which the exception occured.
|
||||
"""
|
||||
|
||||
def __init__(self, msg=None):
|
||||
@@ -43,15 +42,6 @@ class KeyConfigError(Exception):
|
||||
self.lineno = None
|
||||
|
||||
|
||||
class DuplicateKeychainError(KeyConfigError):
|
||||
|
||||
"""Error raised when there's a duplicate key binding."""
|
||||
|
||||
def __init__(self, keychain):
|
||||
super().__init__("Duplicate key chain {}!".format(keychain))
|
||||
self.keychain = keychain
|
||||
|
||||
|
||||
class KeyConfigParser(QObject):
|
||||
|
||||
"""Parser for the keybind config.
|
||||
@@ -60,34 +50,25 @@ class KeyConfigParser(QObject):
|
||||
_configfile: The filename of the config or None.
|
||||
_cur_section: The section currently being processed by _read().
|
||||
_cur_command: The command currently being processed by _read().
|
||||
is_dirty: Whether the config is currently dirty.
|
||||
|
||||
Class attributes:
|
||||
UNBOUND_COMMAND: The special command used for unbound keybindings.
|
||||
|
||||
Signals:
|
||||
changed: Emitted when the internal data has changed.
|
||||
changed: Emitted when the config has changed.
|
||||
arg: Name of the mode which was changed.
|
||||
config_dirty: Emitted when the config should be re-saved.
|
||||
"""
|
||||
|
||||
changed = pyqtSignal(str)
|
||||
config_dirty = pyqtSignal()
|
||||
UNBOUND_COMMAND = '<unbound>'
|
||||
|
||||
def __init__(self, configdir, fname, relaxed=False, parent=None):
|
||||
def __init__(self, configdir, fname, parent=None):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
configdir: The directory to save the configs in.
|
||||
fname: The filename of the config.
|
||||
relaxed: If given, unknwon commands are ignored.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.is_dirty = False
|
||||
self._cur_section = None
|
||||
self._cur_command = None
|
||||
# Mapping of section name(s) to key binding -> command dicts.
|
||||
# Mapping of section name(s) to keybinding -> command dicts.
|
||||
self.keybindings = collections.OrderedDict()
|
||||
if configdir is None:
|
||||
self._configfile = None
|
||||
@@ -96,8 +77,7 @@ class KeyConfigParser(QObject):
|
||||
if self._configfile is None or not os.path.exists(self._configfile):
|
||||
self._load_default()
|
||||
else:
|
||||
self._read(relaxed)
|
||||
self._load_default(only_new=True)
|
||||
self._read()
|
||||
log.init.debug("Loaded bindings: {}".format(self.keybindings))
|
||||
|
||||
def __str__(self):
|
||||
@@ -150,8 +130,8 @@ class KeyConfigParser(QObject):
|
||||
data = str(self)
|
||||
f.write(data)
|
||||
|
||||
@cmdutils.register(instance='key-config', maxsplit=1, no_cmd_split=True)
|
||||
def bind(self, key, command, *, mode=None, force=False):
|
||||
@cmdutils.register(instance='key-config', maxsplit=1)
|
||||
def bind(self, key, command, *, mode=None):
|
||||
"""Bind a key to a command.
|
||||
|
||||
Args:
|
||||
@@ -159,7 +139,6 @@ class KeyConfigParser(QObject):
|
||||
command: The command to execute, with optional args.
|
||||
mode: A comma-separated list of modes to bind the key in
|
||||
(default: `normal`).
|
||||
force: Rebind the key if it is already bound.
|
||||
"""
|
||||
if mode is None:
|
||||
mode = 'normal'
|
||||
@@ -167,20 +146,16 @@ class KeyConfigParser(QObject):
|
||||
for m in mode.split(','):
|
||||
if m not in configdata.KEY_DATA:
|
||||
raise cmdexc.CommandError("Invalid mode {}!".format(m))
|
||||
split_cmd = command.split()
|
||||
if split_cmd[0] not in cmdutils.cmd_dict:
|
||||
raise cmdexc.CommandError("Invalid command {}!".format(
|
||||
split_cmd[0]))
|
||||
try:
|
||||
self._validate_command(command)
|
||||
except KeyConfigError as e:
|
||||
raise cmdexc.CommandError(str(e))
|
||||
try:
|
||||
self._add_binding(mode, key, command, force=force)
|
||||
except DuplicateKeychainError as e:
|
||||
raise cmdexc.CommandError("Duplicate keychain {} - use --force to "
|
||||
"override!".format(str(e.keychain)))
|
||||
self._add_binding(mode, key, command)
|
||||
except KeyConfigError as e:
|
||||
raise cmdexc.CommandError(e)
|
||||
for m in mode.split(','):
|
||||
self.changed.emit(m)
|
||||
self._mark_config_dirty()
|
||||
|
||||
@cmdutils.register(instance='key-config')
|
||||
def unbind(self, key, mode=None):
|
||||
@@ -208,15 +183,8 @@ class KeyConfigParser(QObject):
|
||||
raise cmdexc.CommandError("Can't find binding '{}' in section "
|
||||
"'{}'!".format(key, mode))
|
||||
else:
|
||||
if key in itertools.chain.from_iterable(
|
||||
configdata.KEY_DATA[mode].values()):
|
||||
try:
|
||||
self._add_binding(mode, key, self.UNBOUND_COMMAND)
|
||||
except DuplicateKeychainError:
|
||||
pass
|
||||
for m in mode.split(','):
|
||||
self.changed.emit(m)
|
||||
self._mark_config_dirty()
|
||||
|
||||
def _normalize_sectname(self, s):
|
||||
"""Normalize a section string like 'foo, bar,baz' to 'bar,baz,foo'."""
|
||||
@@ -230,60 +198,20 @@ class KeyConfigParser(QObject):
|
||||
sections = '!' + sections
|
||||
return sections
|
||||
|
||||
def _load_default(self, *, only_new=False):
|
||||
"""Load the built-in default key bindings.
|
||||
|
||||
Args:
|
||||
only_new: If set, only keybindings which are completely unused
|
||||
(same command/key not bound) are added.
|
||||
"""
|
||||
# {'sectname': {'keychain1': 'command', 'keychain2': 'command'}, ...}
|
||||
bindings_to_add = collections.OrderedDict()
|
||||
|
||||
def _load_default(self):
|
||||
"""Load the built-in default keybindings."""
|
||||
for sectname, sect in configdata.KEY_DATA.items():
|
||||
sectname = self._normalize_sectname(sectname)
|
||||
bindings_to_add[sectname] = collections.OrderedDict()
|
||||
for command, keychains in sect.items():
|
||||
for e in keychains:
|
||||
if not only_new or self._is_new(sectname, command, e):
|
||||
assert e not in bindings_to_add[sectname]
|
||||
bindings_to_add[sectname][e] = command
|
||||
|
||||
for sectname, sect in bindings_to_add.items():
|
||||
if not sect:
|
||||
if not only_new:
|
||||
self.keybindings[sectname] = collections.OrderedDict()
|
||||
self.keybindings[sectname] = collections.OrderedDict()
|
||||
else:
|
||||
for keychain, command in sect.items():
|
||||
self._add_binding(sectname, keychain, command)
|
||||
for command, keychains in sect.items():
|
||||
for e in keychains:
|
||||
self._add_binding(sectname, e, command)
|
||||
self.changed.emit(sectname)
|
||||
|
||||
if bindings_to_add:
|
||||
self._mark_config_dirty()
|
||||
|
||||
def _is_new(self, sectname, command, keychain):
|
||||
"""Check if a given binding is new.
|
||||
|
||||
A binding is considered new if both the command is not bound to any key
|
||||
yet, and the key isn't used anywhere else in the same section.
|
||||
"""
|
||||
try:
|
||||
bindings = self.keybindings[sectname]
|
||||
except KeyError:
|
||||
return True
|
||||
if keychain in bindings:
|
||||
return False
|
||||
elif command in bindings.values():
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def _read(self, relaxed=False):
|
||||
"""Read the config file from disk and parse it.
|
||||
|
||||
Args:
|
||||
relaxed: Ignore unknown commands.
|
||||
"""
|
||||
def _read(self):
|
||||
"""Read the config file from disk and parse it."""
|
||||
try:
|
||||
with open(self._configfile, 'r', encoding='utf-8') as f:
|
||||
for i, line in enumerate(f):
|
||||
@@ -302,86 +230,45 @@ class KeyConfigParser(QObject):
|
||||
line = line.strip()
|
||||
self._read_command(line)
|
||||
except KeyConfigError as e:
|
||||
if relaxed:
|
||||
continue
|
||||
else:
|
||||
e.lineno = i
|
||||
raise
|
||||
e.lineno = i
|
||||
raise
|
||||
except OSError:
|
||||
log.keyboard.exception("Failed to read key bindings!")
|
||||
log.keyboard.exception("Failed to read keybindings!")
|
||||
for sectname in self.keybindings:
|
||||
self.changed.emit(sectname)
|
||||
|
||||
def _mark_config_dirty(self):
|
||||
"""Mark the config as dirty."""
|
||||
self.is_dirty = True
|
||||
self.config_dirty.emit()
|
||||
|
||||
def _validate_command(self, line):
|
||||
"""Check if a given command is valid."""
|
||||
if line == self.UNBOUND_COMMAND:
|
||||
return
|
||||
commands = line.split(';;')
|
||||
try:
|
||||
first_cmd = commands[0].split(maxsplit=1)[0].strip()
|
||||
cmd = cmdutils.cmd_dict[first_cmd]
|
||||
if cmd.no_cmd_split:
|
||||
commands = [line]
|
||||
except (KeyError, IndexError):
|
||||
pass
|
||||
|
||||
for cmd in commands:
|
||||
if not cmd.strip():
|
||||
raise KeyConfigError("Got empty command (line: {!r})!".format(
|
||||
line))
|
||||
commands = [c.split(maxsplit=1)[0].strip() for c in commands]
|
||||
for cmd in commands:
|
||||
if cmd not in cmdutils.cmd_dict:
|
||||
raise KeyConfigError("Invalid command '{}'!".format(cmd))
|
||||
|
||||
def _read_command(self, line):
|
||||
"""Read a command from a line."""
|
||||
if self._cur_section is None:
|
||||
raise KeyConfigError("Got command '{}' without getting a "
|
||||
"section!".format(line))
|
||||
else:
|
||||
self._validate_command(line)
|
||||
for rgx, repl in configdata.CHANGED_KEY_COMMANDS:
|
||||
if rgx.match(line):
|
||||
line = rgx.sub(repl, line)
|
||||
self._mark_config_dirty()
|
||||
break
|
||||
command = line.split(maxsplit=1)[0]
|
||||
if command not in cmdutils.cmd_dict:
|
||||
raise KeyConfigError("Invalid command '{}'!".format(command))
|
||||
self._cur_command = line
|
||||
|
||||
def _read_keybinding(self, line):
|
||||
"""Read a key binding from a line."""
|
||||
"""Read a keybinding from a line."""
|
||||
if self._cur_command is None:
|
||||
raise KeyConfigError("Got key binding '{}' without getting a "
|
||||
raise KeyConfigError("Got keybinding '{}' without getting a "
|
||||
"command!".format(line))
|
||||
else:
|
||||
assert self._cur_section is not None
|
||||
self._add_binding(self._cur_section, line, self._cur_command)
|
||||
|
||||
def _add_binding(self, sectname, keychain, command, *, force=False):
|
||||
def _add_binding(self, sectname, keychain, command):
|
||||
"""Add a new binding from keychain to command in section sectname."""
|
||||
log.keyboard.debug("Adding binding {} -> {} in mode {}.".format(
|
||||
keychain, command, sectname))
|
||||
if sectname not in self.keybindings:
|
||||
self.keybindings[sectname] = collections.OrderedDict()
|
||||
if keychain in self.get_bindings_for(sectname):
|
||||
if force or command == self.UNBOUND_COMMAND:
|
||||
self.unbind(keychain, mode=sectname)
|
||||
else:
|
||||
raise DuplicateKeychainError(keychain)
|
||||
section = self.keybindings[sectname]
|
||||
if (command != self.UNBOUND_COMMAND and
|
||||
section.get(keychain, None) == self.UNBOUND_COMMAND):
|
||||
# re-binding an unbound keybinding
|
||||
del section[keychain]
|
||||
raise KeyConfigError("Duplicate keychain '{}'!".format(keychain))
|
||||
self.keybindings[sectname][keychain] = command
|
||||
|
||||
def get_bindings_for(self, section):
|
||||
"""Get a dict with all merged key bindings for a section."""
|
||||
"""Get a dict with all merged keybindings for a section."""
|
||||
bindings = {}
|
||||
for sectstring, d in self.keybindings.items():
|
||||
if sectstring.startswith('!'):
|
||||
@@ -397,6 +284,4 @@ class KeyConfigParser(QObject):
|
||||
bindings.update(self.keybindings['all'])
|
||||
except KeyError:
|
||||
pass
|
||||
bindings = {k: v for k, v in bindings.items()
|
||||
if v != self.UNBOUND_COMMAND}
|
||||
return bindings
|
||||
|
||||
118
qutebrowser/config/parsers/line.py
Normal file
118
qutebrowser/config/parsers/line.py
Normal file
@@ -0,0 +1,118 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Parser for line-based configurations like histories."""
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import collections
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot
|
||||
|
||||
from qutebrowser.utils import log, utils, objreg, qtutils
|
||||
from qutebrowser.config import config
|
||||
|
||||
|
||||
class LineConfigParser(collections.UserList):
|
||||
|
||||
"""Parser for configuration files which are simply line-based.
|
||||
|
||||
Attributes:
|
||||
data: A list of lines.
|
||||
_configdir: The directory to read the config from.
|
||||
_configfile: The config file path.
|
||||
_fname: Filename of the config.
|
||||
_binary: Whether to open the file in binary mode.
|
||||
_limit: The config section/option used to limit the maximum number of
|
||||
lines.
|
||||
"""
|
||||
|
||||
def __init__(self, configdir, fname, limit=None, binary=False):
|
||||
"""Config constructor.
|
||||
|
||||
Args:
|
||||
configdir: Directory to read the config from.
|
||||
fname: Filename of the config file.
|
||||
limit: Config tuple (section, option) which contains a limit.
|
||||
binary: Whether to open the file in binary mode.
|
||||
"""
|
||||
super().__init__()
|
||||
self._configdir = configdir
|
||||
self._configfile = os.path.join(self._configdir, fname)
|
||||
self._fname = fname
|
||||
self._limit = limit
|
||||
self._binary = binary
|
||||
if not os.path.isfile(self._configfile):
|
||||
self.data = []
|
||||
else:
|
||||
log.init.debug("Reading config from {}".format(self._configfile))
|
||||
self.read(self._configfile)
|
||||
if limit is not None:
|
||||
objreg.get('config').changed.connect(self.cleanup_file)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, constructor=True,
|
||||
configdir=self._configdir, fname=self._fname,
|
||||
limit=self._limit, binary=self._binary)
|
||||
|
||||
def read(self, filename):
|
||||
"""Read the data from a file."""
|
||||
if self._binary:
|
||||
with open(filename, 'rb') as f:
|
||||
self.data = [line.rstrip(b'\n') for line in f.readlines()]
|
||||
else:
|
||||
with open(filename, 'r', encoding='utf-8') as f:
|
||||
self.data = [line.rstrip('\n') for line in f.readlines()]
|
||||
|
||||
def write(self, fp, limit=-1):
|
||||
"""Write the data to a file.
|
||||
|
||||
Args:
|
||||
fp: A file object to write the data to.
|
||||
limit: How many lines to write, or -1 for no limit.
|
||||
"""
|
||||
if limit == -1:
|
||||
data = self.data
|
||||
else:
|
||||
data = self.data[-limit:]
|
||||
if self._binary:
|
||||
fp.write(b'\n'.join(data))
|
||||
else:
|
||||
fp.write('\n'.join(data))
|
||||
|
||||
def save(self):
|
||||
"""Save the config file."""
|
||||
limit = -1 if self._limit is None else config.get(*self._limit)
|
||||
if limit == 0:
|
||||
return
|
||||
if not os.path.exists(self._configdir):
|
||||
os.makedirs(self._configdir, 0o755)
|
||||
log.destroy.debug("Saving config to {}".format(self._configfile))
|
||||
with qtutils.savefile_open(self._configfile, self._binary) as f:
|
||||
self.write(f, limit)
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def cleanup_file(self, section, option):
|
||||
"""Delete the file if the limit was changed to 0."""
|
||||
if (section, option) != self._limit:
|
||||
return
|
||||
value = config.get(section, option)
|
||||
if value == 0:
|
||||
if os.path.exists(self._configfile):
|
||||
os.remove(self._configfile)
|
||||
@@ -29,7 +29,6 @@ class Section:
|
||||
"""Base class for KeyValue/ValueList sections.
|
||||
|
||||
Attributes:
|
||||
_readonly: Whether this section is read-only.
|
||||
values: An OrderedDict with key as index and value as value.
|
||||
key: string
|
||||
value: SettingValue
|
||||
@@ -39,7 +38,6 @@ class Section:
|
||||
def __init__(self):
|
||||
self.values = None
|
||||
self.descriptions = {}
|
||||
self._readonly = False
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Get the value for key.
|
||||
@@ -101,15 +99,13 @@ class KeyValue(Section):
|
||||
set of keys.
|
||||
"""
|
||||
|
||||
def __init__(self, *defaults, readonly=False):
|
||||
def __init__(self, *defaults):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
*defaults: A (key, value, description) list of defaults.
|
||||
readonly: Whether this config is readonly.
|
||||
"""
|
||||
super().__init__()
|
||||
self._readonly = readonly
|
||||
if not defaults:
|
||||
return
|
||||
self.values = collections.OrderedDict()
|
||||
@@ -119,8 +115,6 @@ class KeyValue(Section):
|
||||
self.descriptions[k] = desc
|
||||
|
||||
def setv(self, layer, key, value, interpolated):
|
||||
if self._readonly:
|
||||
raise ValueError("Trying to modify a read-only config!")
|
||||
self.values[key].setv(layer, value, interpolated)
|
||||
|
||||
def dump_userconfig(self):
|
||||
@@ -139,7 +133,7 @@ class ValueList(Section):
|
||||
"""This class represents a section with a list key-value settings.
|
||||
|
||||
These are settings inside sections which don't have fixed keys, but instead
|
||||
have a dynamic list of "key = value" pairs, like key bindings or
|
||||
have a dynamic list of "key = value" pairs, like keybindings or
|
||||
searchengines.
|
||||
|
||||
They basically consist of two different SettingValues.
|
||||
@@ -149,20 +143,17 @@ class ValueList(Section):
|
||||
keytype: The type to use for the key (only used for validating)
|
||||
valtype: The type to use for the value.
|
||||
_ordered_value_cache: A ChainMap-like OrderedDict of all values.
|
||||
_readonly: Whether this section is read-only.
|
||||
"""
|
||||
|
||||
def __init__(self, keytype, valtype, *defaults, readonly=False):
|
||||
def __init__(self, keytype, valtype, *defaults):
|
||||
"""Wrap types over default values. Take care when overriding this.
|
||||
|
||||
Args:
|
||||
keytype: The type instance to be used for keys.
|
||||
valtype: The type instance to be used for values.
|
||||
*defaults: A (key, value) list of default values.
|
||||
readonly: Whether this config is readonly.
|
||||
"""
|
||||
super().__init__()
|
||||
self._readonly = readonly
|
||||
self._ordered_value_cache = None
|
||||
self.keytype = keytype
|
||||
self.valtype = valtype
|
||||
@@ -191,8 +182,6 @@ class ValueList(Section):
|
||||
return self._ordered_value_cache
|
||||
|
||||
def setv(self, layer, key, value, interpolated):
|
||||
if self._readonly:
|
||||
raise ValueError("Trying to modify a read-only config!")
|
||||
self.keytype.validate(key)
|
||||
if key in self.layers[layer]:
|
||||
self.layers[layer][key].setv(layer, value, interpolated)
|
||||
|
||||
@@ -56,7 +56,7 @@ def set_register_stylesheet(obj):
|
||||
Must have a STYLESHEET attribute.
|
||||
"""
|
||||
qss = get_stylesheet(obj.STYLESHEET)
|
||||
log.config.vdebug("stylesheet for {}: {}".format(
|
||||
log.style.vdebug("stylesheet for {}: {}".format(
|
||||
obj.__class__.__name__, qss))
|
||||
obj.setStyleSheet(qss)
|
||||
objreg.get('config').changed.connect(
|
||||
@@ -91,10 +91,10 @@ class ColorDict(dict):
|
||||
try:
|
||||
val = super().__getitem__(key)
|
||||
except KeyError:
|
||||
log.config.exception("No color defined for {}!")
|
||||
log.style.exception("No color defined for {}!")
|
||||
return ''
|
||||
if isinstance(val, QColor):
|
||||
# This could happen when accidentally declaring something as
|
||||
# This could happen when accidentaly declarding something as
|
||||
# QtColor instead of Color in the config, and it'd go unnoticed as
|
||||
# the CSS is invalid then.
|
||||
raise TypeError("QColor passed to ColorDict!")
|
||||
|
||||
@@ -26,7 +26,7 @@ class SettingValue:
|
||||
|
||||
"""Base class for setting values.
|
||||
|
||||
Intended to be sub-classed by config value "types".
|
||||
Intended to be subclassed by config value "types".
|
||||
|
||||
Attributes:
|
||||
typ: A BaseType subclass instance.
|
||||
@@ -79,7 +79,6 @@ class SettingValue:
|
||||
if val is not None:
|
||||
return val
|
||||
else: # pylint: disable=useless-else-on-loop
|
||||
# https://bitbucket.org/logilab/pylint/issue/489/
|
||||
raise ValueError("No valid config value found!")
|
||||
|
||||
def transformed(self):
|
||||
|
||||
@@ -22,421 +22,202 @@
|
||||
Module attributes:
|
||||
ATTRIBUTES: A mapping from internal setting names to QWebSetting enum
|
||||
constants.
|
||||
SETTERS: A mapping from setting names to QWebSetting setter method names.
|
||||
settings: The global QWebSettings singleton instance.
|
||||
"""
|
||||
|
||||
import os.path
|
||||
|
||||
from PyQt5.QtWebKit import QWebSettings
|
||||
from PyQt5.QtCore import QStandardPaths, QUrl
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import standarddir, objreg, log, utils, debug
|
||||
from qutebrowser.utils import usertypes, standarddir, objreg
|
||||
|
||||
UNSET = object()
|
||||
|
||||
|
||||
class Base:
|
||||
|
||||
"""Base class for QWebSetting wrappers.
|
||||
|
||||
Attributes:
|
||||
_default: The default value of this setting.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._default = UNSET
|
||||
|
||||
def _get_qws(self, qws):
|
||||
"""Get the QWebSettings object to use.
|
||||
|
||||
Args:
|
||||
qws: The QWebSettings instance to use, or None to use the global
|
||||
instance.
|
||||
"""
|
||||
if qws is None:
|
||||
return QWebSettings.globalSettings()
|
||||
else:
|
||||
return qws
|
||||
|
||||
def save_default(self, qws=None):
|
||||
"""Save the default value based on the currently set one.
|
||||
|
||||
This does nothing if no getter is configured for this setting.
|
||||
|
||||
Args:
|
||||
qws: The QWebSettings instance to use, or None to use the global
|
||||
instance.
|
||||
|
||||
Return:
|
||||
The saved default value.
|
||||
"""
|
||||
try:
|
||||
self._default = self.get(qws)
|
||||
return self._default
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
def restore_default(self, qws=None):
|
||||
"""Restore the default value from the saved one.
|
||||
|
||||
This does nothing if the default has never been set.
|
||||
|
||||
Args:
|
||||
qws: The QWebSettings instance to use, or None to use the global
|
||||
instance.
|
||||
"""
|
||||
if self._default is not UNSET:
|
||||
log.config.vdebug("Restoring default {!r}.".format(self._default))
|
||||
self._set(self._default, qws=qws)
|
||||
|
||||
def get(self, qws=None):
|
||||
"""Get the value of this setting.
|
||||
|
||||
Must be overridden by subclasses.
|
||||
|
||||
Args:
|
||||
qws: The QWebSettings instance to use, or None to use the global
|
||||
instance.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def set(self, value, qws=None):
|
||||
"""Set the value of this setting.
|
||||
|
||||
Args:
|
||||
value: The value to set.
|
||||
qws: The QWebSettings instance to use, or None to use the global
|
||||
instance.
|
||||
"""
|
||||
if value is None:
|
||||
self.restore_default(qws)
|
||||
else:
|
||||
self._set(value, qws=qws)
|
||||
|
||||
def _set(self, value, qws):
|
||||
"""Inner function to set the value of this setting.
|
||||
|
||||
Must be overridden by subclasses.
|
||||
|
||||
Args:
|
||||
value: The value to set.
|
||||
qws: The QWebSettings instance to use, or None to use the global
|
||||
instance.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class Attribute(Base):
|
||||
|
||||
"""A setting set via QWebSettings::setAttribute.
|
||||
|
||||
Attributes:
|
||||
self._attribute: A QWebSettings::WebAttribute instance.
|
||||
"""
|
||||
|
||||
def __init__(self, attribute):
|
||||
super().__init__()
|
||||
self._attribute = attribute
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(
|
||||
self, attribute=debug.qenum_key(QWebSettings, self._attribute),
|
||||
constructor=True)
|
||||
|
||||
def get(self, qws=None):
|
||||
return self._get_qws(qws).attribute(self._attribute)
|
||||
|
||||
def _set(self, value, qws=None):
|
||||
self._get_qws(qws).setAttribute(self._attribute, value)
|
||||
|
||||
|
||||
class Setter(Base):
|
||||
|
||||
"""A setting set via QWebSettings getter/setter methods.
|
||||
|
||||
This will pass the QWebSettings instance ("self") as first argument to the
|
||||
methods, so self._getter/self._setter are the *unbound* methods.
|
||||
|
||||
Attributes:
|
||||
_getter: The unbound QWebSettings method to get this value, or None.
|
||||
_setter: The unbound QWebSettings method to set this value.
|
||||
_args: An iterable of the arguments to pass to the setter/getter
|
||||
(before the value, for the setter).
|
||||
_unpack: Whether to unpack args (True) or pass them directly (False).
|
||||
"""
|
||||
|
||||
def __init__(self, getter, setter, args=(), unpack=False):
|
||||
super().__init__()
|
||||
self._getter = getter
|
||||
self._setter = setter
|
||||
self._args = args
|
||||
self._unpack = unpack
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, getter=self._getter, setter=self._setter,
|
||||
args=self._args, unpack=self._unpack,
|
||||
constructor=True)
|
||||
|
||||
def get(self, qws=None):
|
||||
if self._getter is None:
|
||||
raise AttributeError("No getter set!")
|
||||
return self._getter(self._get_qws(qws), *self._args)
|
||||
|
||||
def _set(self, value, qws=None):
|
||||
args = [self._get_qws(qws)]
|
||||
args.extend(self._args)
|
||||
if self._unpack:
|
||||
args.extend(value)
|
||||
else:
|
||||
args.append(value)
|
||||
self._setter(*args)
|
||||
|
||||
|
||||
class NullStringSetter(Setter):
|
||||
|
||||
"""A setter for settings requiring a null QString as default.
|
||||
|
||||
This overrides save_default so None is saved for an empty string. This is
|
||||
needed for the CSS media type, because it returns an empty Python string
|
||||
when getting the value, but setting it to the default requires passing None
|
||||
(a null QString) instead of an empty string.
|
||||
"""
|
||||
|
||||
def save_default(self, qws=None):
|
||||
try:
|
||||
val = self.get(qws)
|
||||
except AttributeError:
|
||||
return None
|
||||
if val == '':
|
||||
self._set(None, qws=qws)
|
||||
else:
|
||||
self._set(val, qws=qws)
|
||||
return val
|
||||
|
||||
|
||||
class GlobalSetter(Setter):
|
||||
|
||||
"""A setting set via static QWebSettings getter/setter methods.
|
||||
|
||||
self._getter/self._setter are the *bound* methods.
|
||||
"""
|
||||
|
||||
def get(self, qws=None):
|
||||
if qws is not None:
|
||||
raise ValueError("qws may not be set with GlobalSetters!")
|
||||
if self._getter is None:
|
||||
raise AttributeError("No getter set!")
|
||||
return self._getter(*self._args)
|
||||
|
||||
def _set(self, value, qws=None):
|
||||
if qws is not None:
|
||||
raise ValueError("qws may not be set with GlobalSetters!")
|
||||
args = list(self._args)
|
||||
if self._unpack:
|
||||
args.extend(value)
|
||||
else:
|
||||
args.append(value)
|
||||
self._setter(*args)
|
||||
|
||||
|
||||
class CookiePolicy(Base):
|
||||
|
||||
"""The ThirdPartyCookiePolicy setting is different from other settings."""
|
||||
|
||||
MAPPING = {
|
||||
'all': QWebSettings.AlwaysAllowThirdPartyCookies,
|
||||
'no-3rdparty': QWebSettings.AlwaysBlockThirdPartyCookies,
|
||||
'never': QWebSettings.AlwaysBlockThirdPartyCookies,
|
||||
'no-unknown-3rdparty': QWebSettings.AllowThirdPartyWithExistingCookies,
|
||||
}
|
||||
|
||||
def get(self, qws=None):
|
||||
return config.get('content', 'cookies-accept')
|
||||
|
||||
def _set(self, value, qws=None):
|
||||
QWebSettings.globalSettings().setThirdPartyCookiePolicy(
|
||||
self.MAPPING[value])
|
||||
MapType = usertypes.enum('MapType', ['attribute', 'setter', 'static_setter'])
|
||||
|
||||
|
||||
MAPPINGS = {
|
||||
'content': {
|
||||
'allow-images':
|
||||
Attribute(QWebSettings.AutoLoadImages),
|
||||
(MapType.attribute, QWebSettings.AutoLoadImages),
|
||||
'allow-javascript':
|
||||
Attribute(QWebSettings.JavascriptEnabled),
|
||||
(MapType.attribute, QWebSettings.JavascriptEnabled),
|
||||
'javascript-can-open-windows':
|
||||
Attribute(QWebSettings.JavascriptCanOpenWindows),
|
||||
(MapType.attribute, QWebSettings.JavascriptCanOpenWindows),
|
||||
'javascript-can-close-windows':
|
||||
Attribute(QWebSettings.JavascriptCanCloseWindows),
|
||||
(MapType.attribute, QWebSettings.JavascriptCanCloseWindows),
|
||||
'javascript-can-access-clipboard':
|
||||
Attribute(QWebSettings.JavascriptCanAccessClipboard),
|
||||
(MapType.attribute, QWebSettings.JavascriptCanAccessClipboard),
|
||||
#'allow-java':
|
||||
# Attribute(QWebSettings.JavaEnabled),
|
||||
# (MapType.attribute, QWebSettings.JavaEnabled),
|
||||
'allow-plugins':
|
||||
Attribute(QWebSettings.PluginsEnabled),
|
||||
'webgl':
|
||||
Attribute(QWebSettings.WebGLEnabled),
|
||||
'css-regions':
|
||||
Attribute(QWebSettings.CSSRegionsEnabled),
|
||||
'hyperlink-auditing':
|
||||
Attribute(QWebSettings.HyperlinkAuditingEnabled),
|
||||
(MapType.attribute, QWebSettings.PluginsEnabled),
|
||||
'local-content-can-access-remote-urls':
|
||||
Attribute(QWebSettings.LocalContentCanAccessRemoteUrls),
|
||||
(MapType.attribute, QWebSettings.LocalContentCanAccessRemoteUrls),
|
||||
'local-content-can-access-file-urls':
|
||||
Attribute(QWebSettings.LocalContentCanAccessFileUrls),
|
||||
'cookies-accept':
|
||||
CookiePolicy(),
|
||||
(MapType.attribute, QWebSettings.LocalContentCanAccessFileUrls),
|
||||
},
|
||||
'network': {
|
||||
'dns-prefetch':
|
||||
Attribute(QWebSettings.DnsPrefetchEnabled),
|
||||
(MapType.attribute, QWebSettings.DnsPrefetchEnabled),
|
||||
},
|
||||
'input': {
|
||||
'spatial-navigation':
|
||||
Attribute(QWebSettings.SpatialNavigationEnabled),
|
||||
(MapType.attribute, QWebSettings.SpatialNavigationEnabled),
|
||||
'links-included-in-focus-chain':
|
||||
Attribute(QWebSettings.LinksIncludedInFocusChain),
|
||||
(MapType.attribute, QWebSettings.LinksIncludedInFocusChain),
|
||||
},
|
||||
'fonts': {
|
||||
'web-family-standard':
|
||||
Setter(getter=QWebSettings.fontFamily,
|
||||
setter=QWebSettings.setFontFamily,
|
||||
args=[QWebSettings.StandardFont]),
|
||||
(MapType.setter, lambda qws, v:
|
||||
qws.setFontFamily(QWebSettings.StandardFont, v),
|
||||
""),
|
||||
'web-family-fixed':
|
||||
Setter(getter=QWebSettings.fontFamily,
|
||||
setter=QWebSettings.setFontFamily,
|
||||
args=[QWebSettings.FixedFont]),
|
||||
(MapType.setter, lambda qws, v:
|
||||
qws.setFontFamily(QWebSettings.FixedFont, v),
|
||||
""),
|
||||
'web-family-serif':
|
||||
Setter(getter=QWebSettings.fontFamily,
|
||||
setter=QWebSettings.setFontFamily,
|
||||
args=[QWebSettings.SerifFont]),
|
||||
(MapType.setter, lambda qws, v:
|
||||
qws.setFontFamily(QWebSettings.SerifFont, v),
|
||||
""),
|
||||
'web-family-sans-serif':
|
||||
Setter(getter=QWebSettings.fontFamily,
|
||||
setter=QWebSettings.setFontFamily,
|
||||
args=[QWebSettings.SansSerifFont]),
|
||||
(MapType.setter, lambda qws, v:
|
||||
qws.setFontFamily(QWebSettings.SansSerifFont, v),
|
||||
""),
|
||||
'web-family-cursive':
|
||||
Setter(getter=QWebSettings.fontFamily,
|
||||
setter=QWebSettings.setFontFamily,
|
||||
args=[QWebSettings.CursiveFont]),
|
||||
(MapType.setter, lambda qws, v:
|
||||
qws.setFontFamily(QWebSettings.CursiveFont, v),
|
||||
""),
|
||||
'web-family-fantasy':
|
||||
Setter(getter=QWebSettings.fontFamily,
|
||||
setter=QWebSettings.setFontFamily,
|
||||
args=[QWebSettings.FantasyFont]),
|
||||
(MapType.setter, lambda qws, v:
|
||||
qws.setFontFamily(QWebSettings.FantasyFont, v),
|
||||
""),
|
||||
'web-size-minimum':
|
||||
Setter(getter=QWebSettings.fontSize,
|
||||
setter=QWebSettings.setFontSize,
|
||||
args=[QWebSettings.MinimumFontSize]),
|
||||
(MapType.setter, lambda qws, v:
|
||||
qws.setFontSize(QWebSettings.MinimumFontSize, v)),
|
||||
'web-size-minimum-logical':
|
||||
Setter(getter=QWebSettings.fontSize,
|
||||
setter=QWebSettings.setFontSize,
|
||||
args=[QWebSettings.MinimumLogicalFontSize]),
|
||||
(MapType.setter, lambda qws, v:
|
||||
qws.setFontSize(QWebSettings.MinimumLogicalFontSize, v)),
|
||||
'web-size-default':
|
||||
Setter(getter=QWebSettings.fontSize,
|
||||
setter=QWebSettings.setFontSize,
|
||||
args=[QWebSettings.DefaultFontSize]),
|
||||
(MapType.setter, lambda qws, v:
|
||||
qws.setFontSize(QWebSettings.DefaultFontSize, v)),
|
||||
'web-size-default-fixed':
|
||||
Setter(getter=QWebSettings.fontSize,
|
||||
setter=QWebSettings.setFontSize,
|
||||
args=[QWebSettings.DefaultFixedFontSize]),
|
||||
(MapType.setter, lambda qws, v:
|
||||
qws.setFontSize(QWebSettings.DefaultFixedFontSize, v)),
|
||||
},
|
||||
'ui': {
|
||||
'zoom-text-only':
|
||||
Attribute(QWebSettings.ZoomTextOnly),
|
||||
(MapType.attribute, QWebSettings.ZoomTextOnly),
|
||||
'frame-flattening':
|
||||
Attribute(QWebSettings.FrameFlatteningEnabled),
|
||||
(MapType.attribute, QWebSettings.FrameFlatteningEnabled),
|
||||
'user-stylesheet':
|
||||
Setter(getter=QWebSettings.userStyleSheetUrl,
|
||||
setter=QWebSettings.setUserStyleSheetUrl),
|
||||
(MapType.setter, lambda qws, v:
|
||||
qws.setUserStyleSheetUrl(v),
|
||||
QUrl()),
|
||||
'css-media-type':
|
||||
NullStringSetter(getter=QWebSettings.cssMediaType,
|
||||
setter=QWebSettings.setCSSMediaType),
|
||||
'smooth-scrolling':
|
||||
Attribute(QWebSettings.ScrollAnimatorEnabled),
|
||||
(MapType.setter, lambda qws, v:
|
||||
qws.setCSSMediaType(v)),
|
||||
#'accelerated-compositing':
|
||||
# Attribute(QWebSettings.AcceleratedCompositingEnabled),
|
||||
# (MapType.attribute, QWebSettings.AcceleratedCompositingEnabled),
|
||||
#'tiled-backing-store':
|
||||
# Attribute(QWebSettings.TiledBackingStoreEnabled),
|
||||
# (MapType.attribute, QWebSettings.TiledBackingStoreEnabled),
|
||||
},
|
||||
'storage': {
|
||||
'offline-storage-database':
|
||||
Attribute(QWebSettings.OfflineStorageDatabaseEnabled),
|
||||
(MapType.attribute, QWebSettings.OfflineStorageDatabaseEnabled),
|
||||
'offline-web-application-storage':
|
||||
Attribute(QWebSettings.OfflineWebApplicationCacheEnabled),
|
||||
(MapType.attribute,
|
||||
QWebSettings.OfflineWebApplicationCacheEnabled),
|
||||
'local-storage':
|
||||
Attribute(QWebSettings.LocalStorageEnabled),
|
||||
(MapType.attribute, QWebSettings.LocalStorageEnabled),
|
||||
'maximum-pages-in-cache':
|
||||
GlobalSetter(getter=QWebSettings.maximumPagesInCache,
|
||||
setter=QWebSettings.setMaximumPagesInCache),
|
||||
(MapType.static_setter, lambda v:
|
||||
QWebSettings.setMaximumPagesInCache(v)),
|
||||
'object-cache-capacities':
|
||||
GlobalSetter(getter=None,
|
||||
setter=QWebSettings.setObjectCacheCapacities,
|
||||
unpack=True),
|
||||
(MapType.static_setter, lambda v:
|
||||
QWebSettings.setObjectCacheCapacities(*v)),
|
||||
'offline-storage-default-quota':
|
||||
GlobalSetter(getter=QWebSettings.offlineStorageDefaultQuota,
|
||||
setter=QWebSettings.setOfflineStorageDefaultQuota),
|
||||
(MapType.static_setter, lambda v:
|
||||
QWebSettings.setOfflineStorageDefaultQuota(v)),
|
||||
'offline-web-application-cache-quota':
|
||||
GlobalSetter(
|
||||
getter=QWebSettings.offlineWebApplicationCacheQuota,
|
||||
setter=QWebSettings.setOfflineWebApplicationCacheQuota),
|
||||
(MapType.static_setter, lambda v:
|
||||
QWebSettings.setOfflineWebApplicationCacheQuota(v)),
|
||||
},
|
||||
'general': {
|
||||
'private-browsing':
|
||||
Attribute(QWebSettings.PrivateBrowsingEnabled),
|
||||
(MapType.attribute, QWebSettings.PrivateBrowsingEnabled),
|
||||
'developer-extras':
|
||||
Attribute(QWebSettings.DeveloperExtrasEnabled),
|
||||
(MapType.attribute, QWebSettings.DeveloperExtrasEnabled),
|
||||
'print-element-backgrounds':
|
||||
Attribute(QWebSettings.PrintElementBackgrounds),
|
||||
(MapType.attribute, QWebSettings.PrintElementBackgrounds),
|
||||
'xss-auditing':
|
||||
Attribute(QWebSettings.XSSAuditingEnabled),
|
||||
(MapType.attribute, QWebSettings.XSSAuditingEnabled),
|
||||
'site-specific-quirks':
|
||||
Attribute(QWebSettings.SiteSpecificQuirksEnabled),
|
||||
(MapType.attribute, QWebSettings.SiteSpecificQuirksEnabled),
|
||||
'default-encoding':
|
||||
Setter(getter=QWebSettings.defaultTextEncoding,
|
||||
setter=QWebSettings.setDefaultTextEncoding),
|
||||
(MapType.setter, lambda qws, v: qws.setDefaultTextEncoding(v), ""),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
settings = None
|
||||
UNSET = object()
|
||||
|
||||
|
||||
def _set_setting(typ, arg, default=UNSET, value=UNSET):
|
||||
"""Set a QWebSettings setting.
|
||||
|
||||
Args:
|
||||
typ: The type of the item.
|
||||
arg: The argument (attribute/handler)
|
||||
default: The value to use if the user set an empty string.
|
||||
value: The value to set.
|
||||
"""
|
||||
if not isinstance(typ, MapType):
|
||||
raise TypeError("Type {} is no MapType member!".format(typ))
|
||||
if value is UNSET:
|
||||
raise TypeError("No value given!")
|
||||
if value is None:
|
||||
if default is UNSET:
|
||||
return
|
||||
else:
|
||||
value = default
|
||||
|
||||
if typ == MapType.attribute:
|
||||
settings.setAttribute(arg, value)
|
||||
elif typ == MapType.setter:
|
||||
arg(settings, value)
|
||||
elif typ == MapType.static_setter:
|
||||
arg(value)
|
||||
|
||||
|
||||
def init():
|
||||
"""Initialize the global QWebSettings."""
|
||||
cache_path = standarddir.cache()
|
||||
data_path = standarddir.data()
|
||||
if config.get('general', 'private-browsing') or cache_path is None:
|
||||
QWebSettings.setIconDatabasePath('')
|
||||
else:
|
||||
QWebSettings.setIconDatabasePath(cache_path)
|
||||
if cache_path is not None:
|
||||
QWebSettings.setOfflineWebApplicationCachePath(
|
||||
os.path.join(cache_path, 'application-cache'))
|
||||
if data_path is not None:
|
||||
QWebSettings.globalSettings().setLocalStoragePath(
|
||||
os.path.join(data_path, 'local-storage'))
|
||||
QWebSettings.setOfflineStoragePath(
|
||||
os.path.join(data_path, 'offline-storage'))
|
||||
cachedir = standarddir.get(QStandardPaths.CacheLocation)
|
||||
QWebSettings.setIconDatabasePath(cachedir)
|
||||
QWebSettings.setOfflineWebApplicationCachePath(
|
||||
os.path.join(cachedir, 'application-cache'))
|
||||
datadir = standarddir.get(QStandardPaths.DataLocation)
|
||||
QWebSettings.globalSettings().setLocalStoragePath(
|
||||
os.path.join(datadir, 'local-storage'))
|
||||
QWebSettings.setOfflineStoragePath(
|
||||
os.path.join(datadir, 'offline-storage'))
|
||||
|
||||
global settings
|
||||
settings = QWebSettings.globalSettings()
|
||||
for sectname, section in MAPPINGS.items():
|
||||
for optname, mapping in section.items():
|
||||
default = mapping.save_default()
|
||||
log.config.vdebug("Saved default for {} -> {}: {!r}".format(
|
||||
sectname, optname, default))
|
||||
value = config.get(sectname, optname)
|
||||
log.config.vdebug("Setting {} -> {} to {!r}".format(
|
||||
sectname, optname, value))
|
||||
mapping.set(value)
|
||||
_set_setting(*mapping, value=value)
|
||||
objreg.get('config').changed.connect(update_settings)
|
||||
|
||||
|
||||
def update_settings(section, option):
|
||||
"""Update global settings when qwebsettings changed."""
|
||||
cache_path = standarddir.cache()
|
||||
if (section, option) == ('general', 'private-browsing'):
|
||||
if config.get('general', 'private-browsing') or cache_path is None:
|
||||
QWebSettings.setIconDatabasePath('')
|
||||
else:
|
||||
QWebSettings.setIconDatabasePath(cache_path)
|
||||
else:
|
||||
try:
|
||||
mapping = MAPPINGS[section][option]
|
||||
except KeyError:
|
||||
return
|
||||
value = config.get(section, option)
|
||||
mapping.set(value)
|
||||
try:
|
||||
mapping = MAPPINGS[section][option]
|
||||
except KeyError:
|
||||
return
|
||||
value = config.get(section, option)
|
||||
_set_setting(*mapping, value=value)
|
||||
|
||||
@@ -14,23 +14,21 @@ pre { margin: 2px; }
|
||||
th, td { border: 1px solid grey; padding: 0px 5px; }
|
||||
th { background: lightgrey; }
|
||||
th pre { color: grey; text-align: left; }
|
||||
.noscript, .noscript-text { color:red; }
|
||||
.noscript-text { margin-bottom: 5cm; }
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<noscript><h1 class="noscript">View Only</h1><p class="noscript-text">Changing settings requires javascript to be enabled!</p></noscript>
|
||||
<noscript><h1>View Only</h1><p>Changing settings requires javascript to be enabled</p></noscript>
|
||||
<header><h1>{{ title }}</h1></header>
|
||||
<table>
|
||||
{% for section in config.DATA %}
|
||||
<tr><th colspan="2"><h3>{{ section }}</h3><pre>{{ config.SECTION_DESC.get(section)|wordwrap(width=120) }}</pre></th></tr>
|
||||
{% for d, e in config.DATA.get(section).items() %}
|
||||
<tr>
|
||||
<td>{{ d }} (Current: {{ confget(section, d)|truncate(100) }})</td>
|
||||
<td>{{ d }} (Current: {{ e.value()|truncate(100) }})</td>
|
||||
<td>
|
||||
<input type="input"
|
||||
onblur="cset('{{ section }}', '{{ d }}', this)"
|
||||
value="{{ confget(section, d) }}">
|
||||
value="{{ e.value() }}">
|
||||
</input>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
/**
|
||||
* Copyright 2015 Artur Shaik <ashaihullin@gmail.com>
|
||||
* Copyright 2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
|
||||
/**
|
||||
* Snippet to position caret at top of the page when caret mode is enabled.
|
||||
* Some code was borrowed from:
|
||||
*
|
||||
* https://github.com/1995eaton/chromium-vim/blob/master/content_scripts/dom.js
|
||||
* https://github.com/1995eaton/chromium-vim/blob/master/content_scripts/visual.js
|
||||
*/
|
||||
|
||||
/* eslint-enable max-len */
|
||||
|
||||
"use strict";
|
||||
|
||||
function isElementInViewport(node) {
|
||||
var i;
|
||||
var boundingRect = (node.getClientRects()[0] ||
|
||||
node.getBoundingClientRect());
|
||||
if (boundingRect.width <= 1 && boundingRect.height <= 1) {
|
||||
var 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) {
|
||||
var children = node.children;
|
||||
var visibleChildNode = false;
|
||||
var l = children.length;
|
||||
for (i = 0; i < l; ++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;
|
||||
}
|
||||
var 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() {
|
||||
var walker = document.createTreeWalker(document.body, 4, null);
|
||||
var node;
|
||||
var textNodes = [];
|
||||
var el;
|
||||
while ((node = walker.nextNode())) {
|
||||
if (node.nodeType === 3 && node.data.trim() !== '') {
|
||||
textNodes.push(node);
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < textNodes.length; i++) {
|
||||
var element = textNodes[i].parentElement;
|
||||
if (isElementInViewport(element.parentElement)) {
|
||||
el = element;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (el !== undefined) {
|
||||
var range = document.createRange();
|
||||
range.setStart(el, 0);
|
||||
range.setEnd(el, 0);
|
||||
var sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
}
|
||||
})();
|
||||
@@ -17,13 +17,13 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Base class for vim-like key sequence parser."""
|
||||
"""Base class for vim-like keysequence parser."""
|
||||
|
||||
import re
|
||||
import functools
|
||||
import unicodedata
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QObject
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import usertypes, log, utils, objreg
|
||||
@@ -44,22 +44,20 @@ class BaseKeyParser(QObject):
|
||||
ambiguous: There are both a partial and a definitive match.
|
||||
none: No more matches possible.
|
||||
|
||||
Types: type of a key binding.
|
||||
chain: execute() was called via a chain-like key binding
|
||||
special: execute() was called via a special key binding
|
||||
Types: type of a keybinding.
|
||||
chain: execute() was called via a chain-like keybinding
|
||||
special: execute() was called via a special keybinding
|
||||
|
||||
do_log: Whether to log keypresses or not.
|
||||
passthrough: Whether unbound keys should be passed through with this
|
||||
handler.
|
||||
|
||||
Attributes:
|
||||
bindings: Bound key bindings
|
||||
bindings: Bound keybindings
|
||||
special_bindings: Bound special bindings (<Foo>).
|
||||
_win_id: The window ID this keyparser is associated with.
|
||||
_warn_on_keychains: Whether a warning should be logged when binding
|
||||
keychains in a section which does not support them.
|
||||
_keystring: The currently entered key sequence
|
||||
_ambiguous_timer: Timer for delayed execution with ambiguous bindings.
|
||||
_timer: Timer for delayed execution.
|
||||
_modename: The name of the input mode associated with this keyparser.
|
||||
_supports_count: Whether count is supported
|
||||
_supports_chains: Whether keychains are supported
|
||||
@@ -71,18 +69,16 @@ class BaseKeyParser(QObject):
|
||||
|
||||
keystring_updated = pyqtSignal(str)
|
||||
do_log = True
|
||||
passthrough = False
|
||||
|
||||
Match = usertypes.enum('Match', ['partial', 'definitive', 'ambiguous',
|
||||
'other', 'none'])
|
||||
'none'])
|
||||
Type = usertypes.enum('Type', ['chain', 'special'])
|
||||
|
||||
def __init__(self, win_id, parent=None, supports_count=None,
|
||||
supports_chains=False):
|
||||
super().__init__(parent)
|
||||
self._win_id = win_id
|
||||
self._ambiguous_timer = usertypes.Timer(self, 'ambiguous-match')
|
||||
self._ambiguous_timer.setSingleShot(True)
|
||||
self._timer = None
|
||||
self._modename = None
|
||||
self._keystring = ''
|
||||
if supports_count is None:
|
||||
@@ -140,9 +136,6 @@ class BaseKeyParser(QObject):
|
||||
(countstr, cmd_input) = re.match(r'^(\d*)(.*)',
|
||||
self._keystring).groups()
|
||||
count = int(countstr) if countstr else None
|
||||
if count == 0 and not cmd_input:
|
||||
cmd_input = self._keystring
|
||||
count = None
|
||||
else:
|
||||
cmd_input = self._keystring
|
||||
count = None
|
||||
@@ -159,30 +152,30 @@ class BaseKeyParser(QObject):
|
||||
e: the KeyPressEvent from Qt.
|
||||
|
||||
Return:
|
||||
A self.Match member.
|
||||
True if event has been handled, False otherwise.
|
||||
"""
|
||||
txt = e.text()
|
||||
key = e.key()
|
||||
self._debug_log("Got key: 0x{:x} / text: '{}'".format(key, txt))
|
||||
|
||||
if len(txt) == 1:
|
||||
category = unicodedata.category(txt)
|
||||
is_control_char = (category == 'Cc')
|
||||
else:
|
||||
is_control_char = False
|
||||
if key == Qt.Key_Escape:
|
||||
self._debug_log("Escape pressed, discarding '{}'.".format(
|
||||
self._keystring))
|
||||
self._keystring = ''
|
||||
return
|
||||
|
||||
if (not txt) or is_control_char:
|
||||
if (not txt) or unicodedata.category(txt) == 'Cc': # control chars
|
||||
self._debug_log("Ignoring, no text char")
|
||||
return self.Match.none
|
||||
return False
|
||||
|
||||
self._stop_timers()
|
||||
self._stop_delayed_exec()
|
||||
self._keystring += txt
|
||||
|
||||
count, cmd_input = self._split_count()
|
||||
|
||||
if not cmd_input:
|
||||
# Only a count, no command yet, but we handled it
|
||||
return self.Match.other
|
||||
return True
|
||||
|
||||
match, binding = self._match_key(cmd_input)
|
||||
|
||||
@@ -195,7 +188,7 @@ class BaseKeyParser(QObject):
|
||||
self._keystring = ''
|
||||
self.execute(binding, self.Type.chain, count)
|
||||
elif match == self.Match.ambiguous:
|
||||
self._debug_log("Ambiguous match for '{}'.".format(
|
||||
self._debug_log("Ambigious match for '{}'.".format(
|
||||
self._keystring))
|
||||
self._handle_ambiguous_match(binding, count)
|
||||
elif match == self.Match.partial:
|
||||
@@ -205,7 +198,8 @@ class BaseKeyParser(QObject):
|
||||
self._debug_log("Giving up with '{}', no matches".format(
|
||||
self._keystring))
|
||||
self._keystring = ''
|
||||
return match
|
||||
return False
|
||||
return True
|
||||
|
||||
def _match_key(self, cmd_input):
|
||||
"""Try to match a given keystring with any bound keychain.
|
||||
@@ -246,16 +240,13 @@ class BaseKeyParser(QObject):
|
||||
else:
|
||||
return (self.Match.none, None)
|
||||
|
||||
def _stop_timers(self):
|
||||
def _stop_delayed_exec(self):
|
||||
"""Stop a delayed execution if any is running."""
|
||||
if self._ambiguous_timer.isActive() and self.do_log:
|
||||
log.keyboard.debug("Stopping delayed execution.")
|
||||
self._ambiguous_timer.stop()
|
||||
try:
|
||||
self._ambiguous_timer.timeout.disconnect()
|
||||
except TypeError:
|
||||
# no connections
|
||||
pass
|
||||
if self._timer is not None:
|
||||
if self.do_log:
|
||||
log.keyboard.debug("Stopping delayed execution.")
|
||||
self._timer.stop()
|
||||
self._timer = None
|
||||
|
||||
def _handle_ambiguous_match(self, binding, count):
|
||||
"""Handle an ambiguous match.
|
||||
@@ -274,10 +265,12 @@ class BaseKeyParser(QObject):
|
||||
# execute in `time' ms
|
||||
self._debug_log("Scheduling execution of {} in {}ms".format(
|
||||
binding, time))
|
||||
self._ambiguous_timer.setInterval(time)
|
||||
self._ambiguous_timer.timeout.connect(
|
||||
self._timer = usertypes.Timer(self, 'ambigious_match')
|
||||
self._timer.setSingleShot(True)
|
||||
self._timer.setInterval(time)
|
||||
self._timer.timeout.connect(
|
||||
functools.partial(self.delayed_exec, binding, count))
|
||||
self._ambiguous_timer.start()
|
||||
self._timer.start()
|
||||
|
||||
def delayed_exec(self, command, count):
|
||||
"""Execute a delayed command.
|
||||
@@ -286,6 +279,7 @@ class BaseKeyParser(QObject):
|
||||
command/count: As if passed to self.execute()
|
||||
"""
|
||||
self._debug_log("Executing delayed command now!")
|
||||
self._timer = None
|
||||
self._keystring = ''
|
||||
self.keystring_updated.emit(self._keystring)
|
||||
self.execute(command, self.Type.chain, count)
|
||||
@@ -295,17 +289,13 @@ class BaseKeyParser(QObject):
|
||||
|
||||
Args:
|
||||
e: the KeyPressEvent from Qt
|
||||
|
||||
Return:
|
||||
True if the event was handled, False otherwise.
|
||||
"""
|
||||
handled = self._handle_special_key(e)
|
||||
|
||||
if handled or not self._supports_chains:
|
||||
return handled
|
||||
match = self._handle_single_key(e)
|
||||
handled = self._handle_single_key(e)
|
||||
self.keystring_updated.emit(self._keystring)
|
||||
return match != self.Match.none
|
||||
return handled
|
||||
|
||||
def read_config(self, modename=None):
|
||||
"""Read the configuration.
|
||||
@@ -351,15 +341,9 @@ class BaseKeyParser(QObject):
|
||||
|
||||
@pyqtSlot(str)
|
||||
def on_keyconfig_changed(self, mode):
|
||||
"""Re-read the config if a key binding was changed."""
|
||||
"""Re-read the config if a keybinding was changed."""
|
||||
if self._modename is None:
|
||||
raise AttributeError("on_keyconfig_changed called but no section "
|
||||
"defined!")
|
||||
if mode == self._modename:
|
||||
self.read_config()
|
||||
|
||||
def clear_keystring(self):
|
||||
"""Clear the currently entered key sequence."""
|
||||
self._debug_log("discarding keystring '{}'.".format(self._keystring))
|
||||
self._keystring = ''
|
||||
self.keystring_updated.emit(self._keystring)
|
||||
|
||||
@@ -55,7 +55,6 @@ class PassthroughKeyParser(CommandKeyParser):
|
||||
"""
|
||||
|
||||
do_log = False
|
||||
passthrough = True
|
||||
|
||||
def __init__(self, win_id, mode, parent=None, warn=True):
|
||||
"""Constructor.
|
||||
|
||||
@@ -17,13 +17,17 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Mode manager singleton which handles the current keyboard mode."""
|
||||
"""Mode manager singleton which handles the current keyboard mode.
|
||||
|
||||
Module attributes:
|
||||
manager: The ModeManager instance.
|
||||
"""
|
||||
|
||||
import functools
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, Qt, QObject, QEvent
|
||||
from PyQt5.QtGui import QWindow
|
||||
from PyQt5.QtCore import pyqtSignal, QObject, QEvent
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PyQt5.QtWebKitWidgets import QWebView
|
||||
|
||||
from qutebrowser.keyinput import modeparsers, keyparser
|
||||
from qutebrowser.config import config
|
||||
@@ -31,33 +35,6 @@ from qutebrowser.commands import cmdexc, cmdutils
|
||||
from qutebrowser.utils import usertypes, log, objreg, utils
|
||||
|
||||
|
||||
class KeyEvent:
|
||||
|
||||
"""A small wrapper over a QKeyEvent storing its data.
|
||||
|
||||
This is needed because Qt apparently mutates existing events with new data.
|
||||
It doesn't store the modifiers because they can be different for a key
|
||||
press/release.
|
||||
|
||||
Attributes:
|
||||
key: A Qt.Key member (QKeyEvent::key).
|
||||
text: A string (QKeyEvent::text).
|
||||
"""
|
||||
|
||||
def __init__(self, keyevent):
|
||||
self.key = keyevent.key()
|
||||
self.text = keyevent.text()
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, key=self.key, text=self.text)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.key == other.key and self.text == other.text
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.key, self.text))
|
||||
|
||||
|
||||
class NotInModeError(Exception):
|
||||
|
||||
"""Exception raised when we want to leave a mode we're not in."""
|
||||
@@ -78,51 +55,84 @@ def init(win_id, parent):
|
||||
KM.prompt: keyparser.PassthroughKeyParser(win_id, 'prompt', modeman,
|
||||
warn=False),
|
||||
KM.yesno: modeparsers.PromptKeyParser(win_id, modeman),
|
||||
KM.caret: modeparsers.CaretKeyParser(win_id, modeman),
|
||||
}
|
||||
objreg.register('keyparsers', keyparsers, scope='window', window=win_id)
|
||||
modeman.destroyed.connect(
|
||||
functools.partial(objreg.delete, 'keyparsers', scope='window',
|
||||
window=win_id))
|
||||
for mode, parser in keyparsers.items():
|
||||
modeman.register(mode, parser)
|
||||
modeman.register(KM.normal, keyparsers[KM.normal].handle)
|
||||
modeman.register(KM.hint, keyparsers[KM.hint].handle)
|
||||
modeman.register(KM.insert, keyparsers[KM.insert].handle, passthrough=True)
|
||||
modeman.register(KM.passthrough, keyparsers[KM.passthrough].handle,
|
||||
passthrough=True)
|
||||
modeman.register(KM.command, keyparsers[KM.command].handle,
|
||||
passthrough=True)
|
||||
modeman.register(KM.prompt, keyparsers[KM.prompt].handle, passthrough=True)
|
||||
modeman.register(KM.yesno, keyparsers[KM.yesno].handle)
|
||||
return modeman
|
||||
|
||||
|
||||
def instance(win_id):
|
||||
def _get_modeman(win_id):
|
||||
"""Get a modemanager object."""
|
||||
return objreg.get('mode-manager', scope='window', window=win_id)
|
||||
|
||||
|
||||
def enter(win_id, mode, reason=None, only_if_normal=False):
|
||||
"""Enter the mode 'mode'."""
|
||||
instance(win_id).enter(mode, reason, only_if_normal)
|
||||
_get_modeman(win_id).enter(mode, reason, only_if_normal)
|
||||
|
||||
|
||||
def leave(win_id, mode, reason=None):
|
||||
"""Leave the mode 'mode'."""
|
||||
instance(win_id).leave(mode, reason)
|
||||
_get_modeman(win_id).leave(mode, reason)
|
||||
|
||||
|
||||
def maybe_leave(win_id, mode, reason=None):
|
||||
"""Convenience method to leave 'mode' without exceptions."""
|
||||
try:
|
||||
instance(win_id).leave(mode, reason)
|
||||
_get_modeman(win_id).leave(mode, reason)
|
||||
except NotInModeError as e:
|
||||
# This is rather likely to happen, so we only log to debug log.
|
||||
log.modes.debug("{} (leave reason: {})".format(e, reason))
|
||||
|
||||
|
||||
class EventFilter(QObject):
|
||||
|
||||
"""Event filter which passes the event to the corrent ModeManager."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._activated = True
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
"""Forward events to the correct modeman."""
|
||||
if not self._activated:
|
||||
return False
|
||||
try:
|
||||
modeman = objreg.get('mode-manager', scope='window',
|
||||
window='current')
|
||||
return modeman.eventFilter(obj, event)
|
||||
except objreg.RegistryUnavailableError:
|
||||
# No window available yet, or not a MainWindow
|
||||
return False
|
||||
except:
|
||||
# If there is an exception in here and we leave the eventfilter
|
||||
# activated, we'll get an infinite loop and a stack overflow.
|
||||
self._activated = False
|
||||
raise
|
||||
|
||||
|
||||
class ModeManager(QObject):
|
||||
|
||||
"""Manager for keyboard modes.
|
||||
|
||||
Attributes:
|
||||
passthrough: A list of modes in which to pass through events.
|
||||
mode: The mode we're currently in.
|
||||
_win_id: The window ID of this ModeManager
|
||||
_parsers: A dictionary of modes and their keyparsers.
|
||||
_handlers: A dictionary of modes and their handlers.
|
||||
_forward_unbound_keys: If we should forward unbound keys.
|
||||
_releaseevents_to_pass: A set of KeyEvents where the keyPressEvent was
|
||||
_releaseevents_to_pass: A list of keys where the keyPressEvent was
|
||||
passed through, so the release event should as
|
||||
well.
|
||||
|
||||
@@ -142,15 +152,17 @@ class ModeManager(QObject):
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
self._win_id = win_id
|
||||
self._parsers = {}
|
||||
self._handlers = {}
|
||||
self.passthrough = []
|
||||
self.mode = usertypes.KeyMode.normal
|
||||
self._releaseevents_to_pass = set()
|
||||
self._releaseevents_to_pass = []
|
||||
self._forward_unbound_keys = config.get(
|
||||
'input', 'forward-unbound-keys')
|
||||
objreg.get('config').changed.connect(self.set_forward_unbound_keys)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, mode=self.mode)
|
||||
return utils.get_repr(self, mode=self.mode,
|
||||
passthrough=self.passthrough)
|
||||
|
||||
def _eventFilter_keypress(self, event):
|
||||
"""Handle filtering of KeyPress events.
|
||||
@@ -162,21 +174,17 @@ class ModeManager(QObject):
|
||||
True if event should be filtered, False otherwise.
|
||||
"""
|
||||
curmode = self.mode
|
||||
parser = self._parsers[curmode]
|
||||
handler = self._handlers[curmode]
|
||||
if curmode != usertypes.KeyMode.insert:
|
||||
log.modes.debug("got keypress in mode {} - delegating to "
|
||||
"{}".format(curmode, utils.qualname(parser)))
|
||||
handled = parser.handle(event)
|
||||
log.modes.debug("got keypress in mode {} - calling handler "
|
||||
"{}".format(curmode, utils.qualname(handler)))
|
||||
handled = handler(event) if handler is not None else False
|
||||
|
||||
is_non_alnum = bool(event.modifiers()) or not event.text().strip()
|
||||
focus_widget = QApplication.instance().focusWidget()
|
||||
is_tab = event.key() in (Qt.Key_Tab, Qt.Key_Backtab)
|
||||
|
||||
if handled:
|
||||
filter_this = True
|
||||
elif is_tab and not isinstance(focus_widget, QWebView):
|
||||
filter_this = True
|
||||
elif (parser.passthrough or
|
||||
elif (curmode in self.passthrough or
|
||||
self._forward_unbound_keys == 'all' or
|
||||
(self._forward_unbound_keys == 'auto' and is_non_alnum)):
|
||||
filter_this = False
|
||||
@@ -184,15 +192,16 @@ class ModeManager(QObject):
|
||||
filter_this = True
|
||||
|
||||
if not filter_this:
|
||||
self._releaseevents_to_pass.add(KeyEvent(event))
|
||||
self._releaseevents_to_pass.append(event)
|
||||
|
||||
if curmode != usertypes.KeyMode.insert:
|
||||
log.modes.debug("handled: {}, forward-unbound-keys: {}, "
|
||||
"passthrough: {}, is_non_alnum: {}, is_tab {} --> "
|
||||
"filter: {} (focused: {!r})".format(
|
||||
"passthrough: {}, is_non_alnum: {} --> filter: "
|
||||
"{} (focused: {!r})".format(
|
||||
handled, self._forward_unbound_keys,
|
||||
parser.passthrough, is_non_alnum, is_tab,
|
||||
filter_this, focus_widget))
|
||||
curmode in self.passthrough,
|
||||
is_non_alnum, filter_this,
|
||||
QApplication.instance().focusWidget()))
|
||||
return filter_this
|
||||
|
||||
def _eventFilter_keyrelease(self, event):
|
||||
@@ -205,9 +214,10 @@ class ModeManager(QObject):
|
||||
True if event should be filtered, False otherwise.
|
||||
"""
|
||||
# handle like matching KeyPress
|
||||
keyevent = KeyEvent(event)
|
||||
if keyevent in self._releaseevents_to_pass:
|
||||
self._releaseevents_to_pass.remove(keyevent)
|
||||
if event in self._releaseevents_to_pass:
|
||||
# remove all occurences
|
||||
self._releaseevents_to_pass = [
|
||||
e for e in self._releaseevents_to_pass if e != event]
|
||||
filter_this = False
|
||||
else:
|
||||
filter_this = True
|
||||
@@ -215,16 +225,20 @@ class ModeManager(QObject):
|
||||
log.modes.debug("filter: {}".format(filter_this))
|
||||
return filter_this
|
||||
|
||||
def register(self, mode, parser):
|
||||
def register(self, mode, handler, passthrough=False):
|
||||
"""Register a new mode.
|
||||
|
||||
Args:
|
||||
mode: The name of the mode.
|
||||
parser: The KeyParser which should be used.
|
||||
handler: Handler for keyPressEvents.
|
||||
passthrough: Whether to pass keybindings in this mode through to
|
||||
the widgets.
|
||||
"""
|
||||
assert isinstance(mode, usertypes.KeyMode)
|
||||
assert parser is not None
|
||||
self._parsers[mode] = parser
|
||||
if not isinstance(mode, usertypes.KeyMode):
|
||||
raise TypeError("Mode {} is no KeyMode member!".format(mode))
|
||||
self._handlers[mode] = handler
|
||||
if passthrough:
|
||||
self.passthrough.append(mode)
|
||||
|
||||
def enter(self, mode, reason=None, only_if_normal=False):
|
||||
"""Enter a new mode.
|
||||
@@ -238,8 +252,8 @@ class ModeManager(QObject):
|
||||
raise TypeError("Mode {} is no KeyMode member!".format(mode))
|
||||
log.modes.debug("Entering mode {}{}".format(
|
||||
mode, '' if reason is None else ' (reason: {})'.format(reason)))
|
||||
if mode not in self._parsers:
|
||||
raise ValueError("No keyparser for mode {}".format(mode))
|
||||
if mode not in self._handlers:
|
||||
raise ValueError("No handler for mode {}".format(mode))
|
||||
prompt_modes = (usertypes.KeyMode.prompt, usertypes.KeyMode.yesno)
|
||||
if self.mode == mode or (self.mode in prompt_modes and
|
||||
mode in prompt_modes):
|
||||
@@ -299,7 +313,7 @@ class ModeManager(QObject):
|
||||
self._forward_unbound_keys = config.get(
|
||||
'input', 'forward-unbound-keys')
|
||||
|
||||
def eventFilter(self, event):
|
||||
def eventFilter(self, obj, event):
|
||||
"""Filter all events based on the currently set mode.
|
||||
|
||||
Also calls the real keypress handler.
|
||||
@@ -313,12 +327,21 @@ 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:
|
||||
typ = event.type()
|
||||
if typ not in [QEvent.KeyPress, QEvent.KeyRelease]:
|
||||
# We're not interested in non-key-events so we pass them through.
|
||||
return False
|
||||
if not isinstance(obj, QWindow):
|
||||
# We already handled this same event at some point earlier, so
|
||||
# we're not interested in it anymore.
|
||||
return False
|
||||
if (QApplication.instance().activeWindow() not in
|
||||
objreg.window_registry.values()):
|
||||
# Some other window (print dialog, etc.) is focused so we pass
|
||||
# the event through.
|
||||
return False
|
||||
|
||||
if typ == QEvent.KeyPress:
|
||||
return self._eventFilter_keypress(event)
|
||||
else:
|
||||
return self._eventFilter_keyrelease(event)
|
||||
|
||||
@cmdutils.register(instance='mode-manager', scope='window', hide=True)
|
||||
def clear_keychain(self):
|
||||
"""Clear the currently entered key chain."""
|
||||
self._parsers[self.mode].clear_keystring()
|
||||
|
||||
@@ -37,18 +37,12 @@ LastPress = usertypes.enum('LastPress', ['none', 'filtertext', 'keystring'])
|
||||
|
||||
class NormalKeyParser(keyparser.CommandKeyParser):
|
||||
|
||||
"""KeyParser for normal mode with added STARTCHARS detection and more.
|
||||
|
||||
Attributes:
|
||||
_partial_timer: Timer to clear partial keypresses.
|
||||
"""
|
||||
"""KeyParser for normalmode with added STARTCHARS detection."""
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(win_id, parent, supports_count=True,
|
||||
supports_chains=True)
|
||||
self.read_config('normal')
|
||||
self._partial_timer = usertypes.Timer(self, 'partial-match')
|
||||
self._partial_timer.setSingleShot(True)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self)
|
||||
@@ -60,38 +54,13 @@ class NormalKeyParser(keyparser.CommandKeyParser):
|
||||
e: the KeyPressEvent from Qt.
|
||||
|
||||
Return:
|
||||
A self.Match member.
|
||||
True if event has been handled, False otherwise.
|
||||
"""
|
||||
txt = e.text().strip()
|
||||
if not self._keystring and any(txt == c for c in STARTCHARS):
|
||||
message.set_cmd_text(self._win_id, txt)
|
||||
return self.Match.definitive
|
||||
match = super()._handle_single_key(e)
|
||||
if match == self.Match.partial:
|
||||
timeout = config.get('input', 'partial-timeout')
|
||||
if timeout != 0:
|
||||
self._partial_timer.setInterval(timeout)
|
||||
self._partial_timer.timeout.connect(self._clear_partial_match)
|
||||
self._partial_timer.start()
|
||||
return match
|
||||
|
||||
@pyqtSlot()
|
||||
def _clear_partial_match(self):
|
||||
"""Clear a partial keystring after a timeout."""
|
||||
self._debug_log("Clearing partial keystring {}".format(
|
||||
self._keystring))
|
||||
self._keystring = ''
|
||||
self.keystring_updated.emit(self._keystring)
|
||||
|
||||
@pyqtSlot()
|
||||
def _stop_timers(self):
|
||||
super()._stop_timers()
|
||||
self._partial_timer.stop()
|
||||
try:
|
||||
self._partial_timer.timeout.disconnect(self._clear_partial_match)
|
||||
except TypeError:
|
||||
# no connections
|
||||
pass
|
||||
return True
|
||||
return super()._handle_single_key(e)
|
||||
|
||||
|
||||
class PromptKeyParser(keyparser.CommandKeyParser):
|
||||
@@ -171,25 +140,21 @@ class HintKeyParser(keyparser.CommandKeyParser):
|
||||
|
||||
Args:
|
||||
e: the KeyPressEvent from Qt
|
||||
|
||||
Returns:
|
||||
True if the match has been handled, False otherwise.
|
||||
"""
|
||||
match = self._handle_single_key(e)
|
||||
if match == self.Match.partial:
|
||||
handled = self._handle_single_key(e)
|
||||
if handled and self._keystring:
|
||||
# A key has been added to the keystring (Match.partial)
|
||||
self.keystring_updated.emit(self._keystring)
|
||||
self._last_press = LastPress.keystring
|
||||
return True
|
||||
elif match == self.Match.definitive:
|
||||
return handled
|
||||
elif handled:
|
||||
# We handled the key but the keystring is empty. This happens when
|
||||
# match is Match.definitive, so a keychain has been completed.
|
||||
self._last_press = LastPress.none
|
||||
return True
|
||||
elif match == self.Match.other:
|
||||
pass
|
||||
elif match == self.Match.none:
|
||||
return handled
|
||||
else:
|
||||
# We couldn't find a keychain so we check if it's a special key.
|
||||
return self._handle_special_key(e)
|
||||
else:
|
||||
raise ValueError("Got invalid match type {}!".format(match))
|
||||
|
||||
def execute(self, cmdstr, keytype, count=None):
|
||||
"""Handle a completed keychain."""
|
||||
@@ -218,15 +183,3 @@ class HintKeyParser(keyparser.CommandKeyParser):
|
||||
hintmanager = objreg.get('hintmanager', scope='tab',
|
||||
window=self._win_id, tab='current')
|
||||
hintmanager.handle_partial_key(keystr)
|
||||
|
||||
|
||||
class CaretKeyParser(keyparser.CommandKeyParser):
|
||||
|
||||
"""KeyParser for caret mode."""
|
||||
|
||||
passthrough = True
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(win_id, parent, supports_count=True,
|
||||
supports_chains=True)
|
||||
self.read_config('caret')
|
||||
|
||||
@@ -22,10 +22,9 @@
|
||||
import binascii
|
||||
import base64
|
||||
import itertools
|
||||
import functools
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, QRect, QPoint, QTimer, Qt
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout
|
||||
|
||||
from qutebrowser.commands import runners, cmdutils
|
||||
from qutebrowser.config import config
|
||||
@@ -34,138 +33,83 @@ from qutebrowser.mainwindow import tabbedbrowser
|
||||
from qutebrowser.mainwindow.statusbar import bar
|
||||
from qutebrowser.completion import completionwidget
|
||||
from qutebrowser.keyinput import modeman
|
||||
from qutebrowser.browser import hints, downloads, downloadview, commands
|
||||
from qutebrowser.browser import hints, downloads, downloadview
|
||||
|
||||
|
||||
win_id_gen = itertools.count(0)
|
||||
|
||||
|
||||
def get_window(via_ipc, force_window=False, force_tab=False):
|
||||
"""Helper function for app.py to get a window id.
|
||||
|
||||
Args:
|
||||
via_ipc: Whether the request was made via IPC.
|
||||
force_window: Whether to force opening in a window.
|
||||
force_tab: Whether to force opening in a tab.
|
||||
"""
|
||||
if force_window and force_tab:
|
||||
raise ValueError("force_window and force_tab are mutually exclusive!")
|
||||
if not via_ipc:
|
||||
# Initial main window
|
||||
return 0
|
||||
window_to_raise = None
|
||||
open_target = config.get('general', 'new-instance-open-target')
|
||||
if (open_target == 'window' or force_window) and not force_tab:
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
win_id = window.win_id
|
||||
window_to_raise = window
|
||||
else:
|
||||
try:
|
||||
window = objreg.last_window()
|
||||
except objreg.NoWindow:
|
||||
# There is no window left, so we open a new one
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
win_id = window.win_id
|
||||
window_to_raise = window
|
||||
win_id = window.win_id
|
||||
if open_target not in ('tab-silent', 'tab-bg-silent'):
|
||||
window_to_raise = window
|
||||
if window_to_raise is not None:
|
||||
window_to_raise.setWindowState(window.windowState() &
|
||||
~Qt.WindowMinimized | Qt.WindowActive)
|
||||
window_to_raise.raise_()
|
||||
window_to_raise.activateWindow()
|
||||
QApplication.instance().alert(window_to_raise)
|
||||
return win_id
|
||||
|
||||
|
||||
class MainWindow(QWidget):
|
||||
|
||||
"""The main window of qutebrowser.
|
||||
|
||||
Adds all needed components to a vbox, initializes sub-widgets and connects
|
||||
Adds all needed components to a vbox, initializes subwidgets and connects
|
||||
signals.
|
||||
|
||||
Attributes:
|
||||
status: The StatusBar widget.
|
||||
tabbed_browser: The TabbedBrowser widget.
|
||||
_downloadview: The DownloadView widget.
|
||||
_tabbed_browser: The TabbedBrowser widget.
|
||||
_vbox: The main QVBoxLayout.
|
||||
_commandrunner: The main CommandRunner instance.
|
||||
"""
|
||||
|
||||
def __init__(self, geometry=None, parent=None):
|
||||
"""Create a new main window.
|
||||
|
||||
Args:
|
||||
geometry: The geometry to load, as a bytes-object (or None).
|
||||
parent: The parent the window should get.
|
||||
"""
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setAttribute(Qt.WA_DeleteOnClose)
|
||||
self._commandrunner = None
|
||||
self.win_id = next(win_id_gen)
|
||||
self.win_id = win_id
|
||||
self.registry = objreg.ObjectRegistry()
|
||||
objreg.window_registry[self.win_id] = self
|
||||
objreg.register('main-window', self, scope='window',
|
||||
window=self.win_id)
|
||||
objreg.window_registry[win_id] = self
|
||||
objreg.register('main-window', self, scope='window', window=win_id)
|
||||
tab_registry = objreg.ObjectRegistry()
|
||||
objreg.register('tab-registry', tab_registry, scope='window',
|
||||
window=self.win_id)
|
||||
window=win_id)
|
||||
|
||||
message_bridge = message.MessageBridge(self)
|
||||
objreg.register('message-bridge', message_bridge, scope='window',
|
||||
window=self.win_id)
|
||||
window=win_id)
|
||||
|
||||
self.setWindowTitle('qutebrowser')
|
||||
if win_id == 0:
|
||||
self._load_geometry()
|
||||
else:
|
||||
self._set_default_geometry()
|
||||
log.init.debug("Initial mainwindow geometry: {}".format(
|
||||
self.geometry()))
|
||||
self._vbox = QVBoxLayout(self)
|
||||
self._vbox.setContentsMargins(0, 0, 0, 0)
|
||||
self._vbox.setSpacing(0)
|
||||
|
||||
log.init.debug("Initializing downloads...")
|
||||
download_manager = downloads.DownloadManager(self.win_id, self)
|
||||
download_manager = downloads.DownloadManager(win_id, self)
|
||||
objreg.register('download-manager', download_manager, scope='window',
|
||||
window=self.win_id)
|
||||
window=win_id)
|
||||
|
||||
self._downloadview = downloadview.DownloadView(self.win_id)
|
||||
|
||||
self.tabbed_browser = tabbedbrowser.TabbedBrowser(self.win_id)
|
||||
objreg.register('tabbed-browser', self.tabbed_browser, scope='window',
|
||||
window=self.win_id)
|
||||
dispatcher = commands.CommandDispatcher(self.win_id,
|
||||
self.tabbed_browser)
|
||||
objreg.register('command-dispatcher', dispatcher, scope='window',
|
||||
window=self.win_id)
|
||||
self.tabbed_browser.destroyed.connect(
|
||||
functools.partial(objreg.delete, 'command-dispatcher',
|
||||
scope='window', window=self.win_id))
|
||||
|
||||
# We need to set an explicit parent for StatusBar because it does some
|
||||
# show/hide magic immediately which would mean it'd show up as a
|
||||
# window.
|
||||
self.status = bar.StatusBar(self.win_id, parent=self)
|
||||
|
||||
self._add_widgets()
|
||||
self._downloadview = downloadview.DownloadView(win_id)
|
||||
self._vbox.addWidget(self._downloadview)
|
||||
self._downloadview.show()
|
||||
|
||||
self._completion = completionwidget.CompletionView(self.win_id, self)
|
||||
self._tabbed_browser = tabbedbrowser.TabbedBrowser(win_id)
|
||||
objreg.register('tabbed-browser', self._tabbed_browser, scope='window',
|
||||
window=win_id)
|
||||
self._vbox.addWidget(self._tabbed_browser)
|
||||
|
||||
self._commandrunner = runners.CommandRunner(self.win_id)
|
||||
self.status = bar.StatusBar(win_id)
|
||||
self._vbox.addWidget(self.status)
|
||||
|
||||
self._completion = completionwidget.CompletionView(win_id, self)
|
||||
|
||||
self._commandrunner = runners.CommandRunner(win_id)
|
||||
|
||||
log.init.debug("Initializing search...")
|
||||
search_runner = runners.SearchRunner(self)
|
||||
objreg.register('search-runner', search_runner, scope='window',
|
||||
window=win_id)
|
||||
|
||||
log.init.debug("Initializing modes...")
|
||||
modeman.init(self.win_id, self)
|
||||
|
||||
if geometry is not None:
|
||||
self._load_geometry(geometry)
|
||||
elif self.win_id == 0:
|
||||
self._load_state_geometry()
|
||||
else:
|
||||
self._set_default_geometry()
|
||||
log.init.debug("Initial main window geometry: {}".format(
|
||||
self.geometry()))
|
||||
|
||||
self._connect_signals()
|
||||
|
||||
# When we're here the statusbar might not even really exist yet, so
|
||||
@@ -173,10 +117,6 @@ class MainWindow(QWidget):
|
||||
# we defer this until everything else is initialized.
|
||||
QTimer.singleShot(0, self._connect_resize_completion)
|
||||
objreg.get('config').changed.connect(self.on_config_changed)
|
||||
|
||||
if config.get('ui', 'hide-mouse-cursor'):
|
||||
self.setCursor(Qt.BlankCursor)
|
||||
|
||||
#self.retranslateUi(MainWindow)
|
||||
#self.tabWidget.setCurrentIndex(0)
|
||||
#QtCore.QMetaObject.connectSlotsByName(MainWindow)
|
||||
@@ -189,30 +129,29 @@ class MainWindow(QWidget):
|
||||
"""Resize the completion if related config options changed."""
|
||||
if section == 'completion' and option in ('height', 'shrink'):
|
||||
self.resize_completion()
|
||||
elif section == 'ui' and option == 'downloads-position':
|
||||
self._add_widgets()
|
||||
|
||||
def _add_widgets(self):
|
||||
"""Add or readd all widgets to the VBox."""
|
||||
self._vbox.removeWidget(self.tabbed_browser)
|
||||
self._vbox.removeWidget(self._downloadview)
|
||||
self._vbox.removeWidget(self.status)
|
||||
position = config.get('ui', 'downloads-position')
|
||||
if position == 'north':
|
||||
self._vbox.addWidget(self._downloadview)
|
||||
self._vbox.addWidget(self.tabbed_browser)
|
||||
elif position == 'south':
|
||||
self._vbox.addWidget(self.tabbed_browser)
|
||||
self._vbox.addWidget(self._downloadview)
|
||||
else:
|
||||
raise ValueError("Invalid position {}!".format(position))
|
||||
self._vbox.addWidget(self.status)
|
||||
@classmethod
|
||||
def spawn(cls, show=True):
|
||||
"""Create a new main window.
|
||||
|
||||
def _load_state_geometry(self):
|
||||
Args:
|
||||
show: Show the window after creating.
|
||||
|
||||
Return:
|
||||
The new window id.
|
||||
"""
|
||||
win_id = next(win_id_gen)
|
||||
win = MainWindow(win_id)
|
||||
if show:
|
||||
win.show()
|
||||
return win_id
|
||||
|
||||
def _load_geometry(self):
|
||||
"""Load the geometry from the state file."""
|
||||
state_config = objreg.get('state-config')
|
||||
try:
|
||||
data = state_config['geometry']['mainwindow']
|
||||
log.init.debug("Restoring mainwindow from {}".format(data))
|
||||
geom = base64.b64decode(data, validate=True)
|
||||
except KeyError:
|
||||
# First start
|
||||
@@ -221,25 +160,14 @@ class MainWindow(QWidget):
|
||||
log.init.exception("Error while reading geometry")
|
||||
self._set_default_geometry()
|
||||
else:
|
||||
self._load_geometry(geom)
|
||||
|
||||
def _save_geometry(self):
|
||||
"""Save the window geometry to the state config."""
|
||||
state_config = objreg.get('state-config')
|
||||
data = bytes(self.saveGeometry())
|
||||
geom = base64.b64encode(data).decode('ASCII')
|
||||
state_config['geometry']['mainwindow'] = geom
|
||||
|
||||
def _load_geometry(self, geom):
|
||||
"""Load geometry from a bytes object.
|
||||
|
||||
If loading fails, loads default geometry.
|
||||
"""
|
||||
log.init.debug("Loading mainwindow from {}".format(geom))
|
||||
ok = self.restoreGeometry(geom)
|
||||
if not ok:
|
||||
log.init.warning("Error while loading geometry.")
|
||||
self._set_default_geometry()
|
||||
try:
|
||||
ok = self.restoreGeometry(geom)
|
||||
except KeyError:
|
||||
log.init.exception("Error while restoring geometry.")
|
||||
self._set_default_geometry()
|
||||
if not ok:
|
||||
log.init.warning("Error while restoring geometry.")
|
||||
self._set_default_geometry()
|
||||
|
||||
def _connect_resize_completion(self):
|
||||
"""Connect the resize_completion signal and resize it once."""
|
||||
@@ -256,7 +184,7 @@ class MainWindow(QWidget):
|
||||
|
||||
def _connect_signals(self):
|
||||
"""Connect all mainwindow signals."""
|
||||
# pylint: disable=too-many-statements
|
||||
# pylint: disable=too-many-locals,too-many-statements
|
||||
key_config = objreg.get('key-config')
|
||||
|
||||
status = self._get_object('statusbar')
|
||||
@@ -264,12 +192,14 @@ class MainWindow(QWidget):
|
||||
completion_obj = self._get_object('completion')
|
||||
tabs = self._get_object('tabbed-browser')
|
||||
cmd = self._get_object('status-command')
|
||||
completer = self._get_object('completer')
|
||||
search_runner = self._get_object('search-runner')
|
||||
message_bridge = self._get_object('message-bridge')
|
||||
mode_manager = self._get_object('mode-manager')
|
||||
prompter = self._get_object('prompter')
|
||||
|
||||
# misc
|
||||
self.tabbed_browser.close_window.connect(self.close)
|
||||
self._tabbed_browser.close_window.connect(self.close)
|
||||
mode_manager.entered.connect(hints.on_mode_entered)
|
||||
|
||||
# status bar
|
||||
@@ -282,7 +212,10 @@ class MainWindow(QWidget):
|
||||
keyparsers[usertypes.KeyMode.normal].keystring_updated.connect(
|
||||
status.keystring.setText)
|
||||
cmd.got_cmd.connect(self._commandrunner.run_safely)
|
||||
cmd.got_search.connect(search_runner.search)
|
||||
cmd.got_search_rev.connect(search_runner.search_rev)
|
||||
cmd.returnPressed.connect(tabs.on_cmd_return_pressed)
|
||||
search_runner.do_search.connect(tabs.search)
|
||||
tabs.got_cmd.connect(self._commandrunner.run_safely)
|
||||
|
||||
# config
|
||||
@@ -291,7 +224,6 @@ class MainWindow(QWidget):
|
||||
|
||||
# messages
|
||||
message_bridge.s_error.connect(status.disp_error)
|
||||
message_bridge.s_warning.connect(status.disp_warning)
|
||||
message_bridge.s_info.connect(status.disp_temp_text)
|
||||
message_bridge.s_set_text.connect(status.set_text)
|
||||
message_bridge.s_maybe_reset_text.connect(status.txt.maybe_reset_text)
|
||||
@@ -311,8 +243,6 @@ class MainWindow(QWidget):
|
||||
tabs.current_tab_changed.connect(status.percentage.on_tab_changed)
|
||||
tabs.cur_scroll_perc_changed.connect(status.percentage.set_perc)
|
||||
|
||||
tabs.tab_index_changed.connect(status.tabindex.on_tab_index_changed)
|
||||
|
||||
tabs.current_tab_changed.connect(status.txt.on_tab_changed)
|
||||
tabs.cur_statusbar_message.connect(status.txt.on_statusbar_message)
|
||||
tabs.cur_load_started.connect(status.txt.on_load_started)
|
||||
@@ -328,13 +258,13 @@ class MainWindow(QWidget):
|
||||
completion_obj.on_clear_completion_selection)
|
||||
cmd.hide_completion.connect(completion_obj.hide)
|
||||
|
||||
# quickmark completion
|
||||
quickmark_manager = objreg.get('quickmark-manager')
|
||||
quickmark_manager.changed.connect(completer.init_quickmark_completions)
|
||||
|
||||
@pyqtSlot()
|
||||
def resize_completion(self):
|
||||
"""Adjust completion according to config."""
|
||||
if not self._completion.isVisible():
|
||||
# It doesn't make sense to resize the completion as long as it's
|
||||
# not shown anyways.
|
||||
return
|
||||
# Get the configured height/percentage.
|
||||
confheight = str(config.get('completion', 'height'))
|
||||
if confheight.endswith('%'):
|
||||
@@ -358,7 +288,6 @@ class MainWindow(QWidget):
|
||||
topleft = QPoint(0, topleft_y)
|
||||
bottomright = self.status.geometry().topRight()
|
||||
rect = QRect(topleft, bottomright)
|
||||
log.misc.debug('completion rect: {}'.format(rect))
|
||||
if rect.isValid():
|
||||
self._completion.setGeometry(rect)
|
||||
|
||||
@@ -373,14 +302,6 @@ class MainWindow(QWidget):
|
||||
"""
|
||||
super().close()
|
||||
|
||||
@cmdutils.register(instance='main-window', scope='window')
|
||||
def fullscreen(self):
|
||||
"""Toggle fullscreen mode."""
|
||||
if self.isFullScreen():
|
||||
self.showNormal()
|
||||
else:
|
||||
self.showFullScreen()
|
||||
|
||||
def resizeEvent(self, e):
|
||||
"""Extend resizewindow's resizeEvent to adjust completion.
|
||||
|
||||
@@ -390,42 +311,27 @@ class MainWindow(QWidget):
|
||||
super().resizeEvent(e)
|
||||
self.resize_completion()
|
||||
self._downloadview.updateGeometry()
|
||||
self.tabbed_browser.tabBar().refresh()
|
||||
self._tabbed_browser.tabBar().refresh()
|
||||
|
||||
def closeEvent(self, e):
|
||||
"""Override closeEvent to display a confirmation if needed."""
|
||||
confirm_quit = config.get('ui', 'confirm-quit')
|
||||
tab_count = self.tabbed_browser.count()
|
||||
download_manager = objreg.get('download-manager', scope='window',
|
||||
window=self.win_id)
|
||||
download_count = download_manager.rowCount()
|
||||
quit_texts = []
|
||||
# Close if set to never ask for confirmation
|
||||
if 'never' in confirm_quit:
|
||||
count = self._tabbed_browser.count()
|
||||
if confirm_quit == 'never':
|
||||
pass
|
||||
# Ask if multiple-tabs are open
|
||||
if 'multiple-tabs' in confirm_quit and tab_count > 1:
|
||||
quit_texts.append("{} {} open.".format(
|
||||
tab_count, "tab is" if tab_count == 1 else "tabs are"))
|
||||
# Ask if multiple downloads running
|
||||
if 'downloads' in confirm_quit and download_count > 0:
|
||||
quit_texts.append("{} {} running.".format(
|
||||
tab_count,
|
||||
"download is" if tab_count == 1 else "downloads are"))
|
||||
# Process all quit messages that user must confirm
|
||||
if quit_texts or 'always' in confirm_quit:
|
||||
text = '\n'.join(['Really quit?'] + quit_texts)
|
||||
elif confirm_quit == 'multiple-tabs' and count <= 1:
|
||||
pass
|
||||
else:
|
||||
text = "Close {} {}?".format(
|
||||
count, "tab" if count == 1 else "tabs")
|
||||
confirmed = message.ask(self.win_id, text,
|
||||
usertypes.PromptMode.yesno,
|
||||
default=True)
|
||||
# Stop asking if the user cancels
|
||||
usertypes.PromptMode.yesno, default=True)
|
||||
if not confirmed:
|
||||
log.destroy.debug("Cancelling closing of window {}".format(
|
||||
log.destroy.debug("Cancelling losing of window {}".format(
|
||||
self.win_id))
|
||||
e.ignore()
|
||||
return
|
||||
e.accept()
|
||||
objreg.get('session-manager').save_last_window_session()
|
||||
self._save_geometry()
|
||||
objreg.get('app').geometry = bytes(self.saveGeometry())
|
||||
log.destroy.debug("Closing window {}".format(self.win_id))
|
||||
self.tabbed_browser.shutdown()
|
||||
self._tabbed_browser.shutdown()
|
||||
|
||||
@@ -21,22 +21,18 @@
|
||||
|
||||
import collections
|
||||
|
||||
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, pyqtProperty, Qt, QTime, QSize,
|
||||
QTimer)
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, pyqtProperty, Qt, QTime, QSize
|
||||
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QStackedLayout, QSizePolicy
|
||||
|
||||
from qutebrowser.config import config, style
|
||||
from qutebrowser.utils import usertypes, log, objreg, utils
|
||||
from qutebrowser.mainwindow.statusbar import (command, progress, keystring,
|
||||
percentage, url, prompt,
|
||||
tabindex)
|
||||
percentage, url, prompt)
|
||||
from qutebrowser.mainwindow.statusbar import text as textwidget
|
||||
|
||||
|
||||
PreviousWidget = usertypes.enum('PreviousWidget', ['none', 'prompt',
|
||||
'command'])
|
||||
Severity = usertypes.enum('Severity', ['normal', 'warning', 'error'])
|
||||
CaretMode = usertypes.enum('CaretMode', ['off', 'on', 'selection'])
|
||||
|
||||
|
||||
class StatusBar(QWidget):
|
||||
@@ -63,10 +59,11 @@ class StatusBar(QWidget):
|
||||
_win_id: The window ID the statusbar is associated with.
|
||||
|
||||
Class attributes:
|
||||
_severity: The severity of the current message, a Severity member.
|
||||
_error: If there currently is an error, accessed through the error
|
||||
property.
|
||||
|
||||
For some reason we need to have this as class attribute so
|
||||
pyqtProperty works correctly.
|
||||
For some reason we need to have this as class attribute so
|
||||
pyqtProperty works correctly.
|
||||
|
||||
_prompt_active: If we're currently in prompt-mode.
|
||||
|
||||
@@ -78,16 +75,6 @@ class StatusBar(QWidget):
|
||||
For some reason we need to have this as class attribute
|
||||
so pyqtProperty works correctly.
|
||||
|
||||
_command_active: If we're currently in command mode.
|
||||
|
||||
For some reason we need to have this as class
|
||||
attribute so pyqtProperty works correctly.
|
||||
|
||||
_caret_mode: The current caret mode (off/on/selection).
|
||||
|
||||
For some reason we need to have this as class attribute
|
||||
so pyqtProperty works correctly.
|
||||
|
||||
Signals:
|
||||
resized: Emitted when the statusbar has resized, so the completion
|
||||
widget can adjust its size to it.
|
||||
@@ -99,71 +86,31 @@ class StatusBar(QWidget):
|
||||
|
||||
resized = pyqtSignal('QRect')
|
||||
moved = pyqtSignal('QPoint')
|
||||
_severity = None
|
||||
_error = False
|
||||
_prompt_active = False
|
||||
_insert_active = False
|
||||
_command_active = False
|
||||
_caret_mode = CaretMode.off
|
||||
|
||||
STYLESHEET = """
|
||||
|
||||
QWidget#StatusBar,
|
||||
QWidget#StatusBar QLabel,
|
||||
QWidget#StatusBar QLineEdit {
|
||||
{{ font['statusbar'] }}
|
||||
QWidget#StatusBar {
|
||||
{{ color['statusbar.bg'] }}
|
||||
{{ color['statusbar.fg'] }}
|
||||
}
|
||||
|
||||
QWidget#StatusBar[caret_mode="on"],
|
||||
QWidget#StatusBar[caret_mode="on"] QLabel,
|
||||
QWidget#StatusBar[caret_mode="on"] QLineEdit {
|
||||
{{ color['statusbar.fg.caret'] }}
|
||||
{{ color['statusbar.bg.caret'] }}
|
||||
}
|
||||
|
||||
QWidget#StatusBar[caret_mode="selection"],
|
||||
QWidget#StatusBar[caret_mode="selection"] QLabel,
|
||||
QWidget#StatusBar[caret_mode="selection"] QLineEdit {
|
||||
{{ color['statusbar.fg.caret-selection'] }}
|
||||
{{ color['statusbar.bg.caret-selection'] }}
|
||||
}
|
||||
|
||||
QWidget#StatusBar[severity="error"],
|
||||
QWidget#StatusBar[severity="error"] QLabel,
|
||||
QWidget#StatusBar[severity="error"] QLineEdit {
|
||||
{{ color['statusbar.fg.error'] }}
|
||||
{{ color['statusbar.bg.error'] }}
|
||||
}
|
||||
|
||||
QWidget#StatusBar[severity="warning"],
|
||||
QWidget#StatusBar[severity="warning"] QLabel,
|
||||
QWidget#StatusBar[severity="warning"] QLineEdit {
|
||||
{{ color['statusbar.fg.warning'] }}
|
||||
{{ color['statusbar.bg.warning'] }}
|
||||
}
|
||||
|
||||
QWidget#StatusBar[prompt_active="true"],
|
||||
QWidget#StatusBar[prompt_active="true"] QLabel,
|
||||
QWidget#StatusBar[prompt_active="true"] QLineEdit {
|
||||
{{ color['statusbar.fg.prompt'] }}
|
||||
{{ color['statusbar.bg.prompt'] }}
|
||||
}
|
||||
|
||||
QWidget#StatusBar[insert_active="true"],
|
||||
QWidget#StatusBar[insert_active="true"] QLabel,
|
||||
QWidget#StatusBar[insert_active="true"] QLineEdit {
|
||||
{{ color['statusbar.fg.insert'] }}
|
||||
QWidget#StatusBar[insert_active="true"] {
|
||||
{{ color['statusbar.bg.insert'] }}
|
||||
}
|
||||
|
||||
QWidget#StatusBar[command_active="true"],
|
||||
QWidget#StatusBar[command_active="true"] QLabel,
|
||||
QWidget#StatusBar[command_active="true"] QLineEdit {
|
||||
{{ color['statusbar.fg.command'] }}
|
||||
{{ color['statusbar.bg.command'] }}
|
||||
QWidget#StatusBar[prompt_active="true"] {
|
||||
{{ color['statusbar.bg.prompt'] }}
|
||||
}
|
||||
|
||||
QWidget#StatusBar[error="true"] {
|
||||
{{ color['statusbar.bg.error'] }}
|
||||
}
|
||||
|
||||
QLabel, QLineEdit {
|
||||
{{ color['statusbar.fg'] }}
|
||||
{{ font['statusbar'] }}
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
@@ -222,60 +169,36 @@ class StatusBar(QWidget):
|
||||
self.percentage = percentage.Percentage()
|
||||
self._hbox.addWidget(self.percentage)
|
||||
|
||||
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.prog = progress.Progress(self)
|
||||
self._hbox.addWidget(self.prog)
|
||||
|
||||
objreg.get('config').changed.connect(self.maybe_hide)
|
||||
QTimer.singleShot(0, self.maybe_hide)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self)
|
||||
|
||||
@config.change_filter('ui', 'hide-statusbar')
|
||||
def maybe_hide(self):
|
||||
"""Hide the statusbar if it's configured to do so."""
|
||||
hide = config.get('ui', 'hide-statusbar')
|
||||
if hide:
|
||||
self.hide()
|
||||
else:
|
||||
self.show()
|
||||
@pyqtProperty(bool)
|
||||
def error(self):
|
||||
"""Getter for self.error, so it can be used as Qt property."""
|
||||
# pylint: disable=method-hidden
|
||||
return self._error
|
||||
|
||||
@pyqtProperty(str)
|
||||
def severity(self):
|
||||
"""Getter for self.severity, so it can be used as Qt property.
|
||||
|
||||
Return:
|
||||
The severity as a string (!)
|
||||
"""
|
||||
if self._severity is None:
|
||||
return ""
|
||||
else:
|
||||
return self._severity.name
|
||||
|
||||
def _set_severity(self, severity):
|
||||
"""Set the severity for the current message.
|
||||
def _set_error(self, val):
|
||||
"""Setter for self.error, so it can be used as Qt property.
|
||||
|
||||
Re-set the stylesheet after setting the value, so everything gets
|
||||
updated by Qt properly.
|
||||
|
||||
Args:
|
||||
severity: A Severity member.
|
||||
"""
|
||||
if self._severity == severity:
|
||||
if self._error == val:
|
||||
# This gets called a lot (e.g. if the completion selection was
|
||||
# changed), and setStyleSheet is relatively expensive, so we ignore
|
||||
# this if there's nothing to change.
|
||||
return
|
||||
log.statusbar.debug("Setting severity to {}".format(severity))
|
||||
self._severity = severity
|
||||
log.statusbar.debug("Setting error to {}".format(val))
|
||||
self._error = val
|
||||
self.setStyleSheet(style.get_stylesheet(self.STYLESHEET))
|
||||
if severity != Severity.normal:
|
||||
if val:
|
||||
# If we got an error while command/prompt was shown, raise the text
|
||||
# widget.
|
||||
self._stack.setCurrentWidget(self.txt)
|
||||
@@ -283,6 +206,7 @@ class StatusBar(QWidget):
|
||||
@pyqtProperty(bool)
|
||||
def prompt_active(self):
|
||||
"""Getter for self.prompt_active, so it can be used as Qt property."""
|
||||
# pylint: disable=method-hidden
|
||||
return self._prompt_active
|
||||
|
||||
def _set_prompt_active(self, val):
|
||||
@@ -295,47 +219,20 @@ class StatusBar(QWidget):
|
||||
self._prompt_active = val
|
||||
self.setStyleSheet(style.get_stylesheet(self.STYLESHEET))
|
||||
|
||||
@pyqtProperty(bool)
|
||||
def command_active(self):
|
||||
"""Getter for self.command_active, so it can be used as Qt property."""
|
||||
return self._command_active
|
||||
|
||||
@pyqtProperty(bool)
|
||||
def insert_active(self):
|
||||
"""Getter for self.insert_active, so it can be used as Qt property."""
|
||||
# pylint: disable=method-hidden
|
||||
return self._insert_active
|
||||
|
||||
@pyqtProperty(str)
|
||||
def caret_mode(self):
|
||||
"""Getter for self._caret_mode, so it can be used as Qt property."""
|
||||
return self._caret_mode.name
|
||||
|
||||
def set_mode_active(self, mode, val):
|
||||
"""Setter for self.{insert,command,caret}_active.
|
||||
def _set_insert_active(self, val):
|
||||
"""Setter for self.insert_active.
|
||||
|
||||
Re-set the stylesheet after setting the value, so everything gets
|
||||
updated by Qt properly.
|
||||
"""
|
||||
if mode == usertypes.KeyMode.insert:
|
||||
log.statusbar.debug("Setting insert_active to {}".format(val))
|
||||
self._insert_active = val
|
||||
if mode == usertypes.KeyMode.command:
|
||||
log.statusbar.debug("Setting command_active to {}".format(val))
|
||||
self._command_active = val
|
||||
elif mode == usertypes.KeyMode.caret:
|
||||
webview = objreg.get('tabbed-browser', scope='window',
|
||||
window=self._win_id).currentWidget()
|
||||
log.statusbar.debug("Setting caret_mode - val {}, selection "
|
||||
"{}".format(val, webview.selection_enabled))
|
||||
if val:
|
||||
if webview.selection_enabled:
|
||||
self._set_mode_text("{} selection".format(mode.name))
|
||||
self._caret_mode = CaretMode.selection
|
||||
else:
|
||||
self._set_mode_text(mode.name)
|
||||
self._caret_mode = CaretMode.on
|
||||
else:
|
||||
self._caret_mode = CaretMode.off
|
||||
log.statusbar.debug("Setting insert_active to {}".format(val))
|
||||
self._insert_active = val
|
||||
self.setStyleSheet(style.get_stylesheet(self.STYLESHEET))
|
||||
|
||||
def _set_mode_text(self, mode):
|
||||
@@ -346,37 +243,35 @@ class StatusBar(QWidget):
|
||||
def _pop_text(self):
|
||||
"""Display a text in the statusbar and pop it from _text_queue."""
|
||||
try:
|
||||
severity, text = self._text_queue.popleft()
|
||||
error, text = self._text_queue.popleft()
|
||||
except IndexError:
|
||||
self._set_severity(Severity.normal)
|
||||
self._set_error(False)
|
||||
self.txt.set_text(self.txt.Text.temp, '')
|
||||
self._text_pop_timer.stop()
|
||||
# If a previous widget was interrupted by an error, restore it.
|
||||
if self._previous_widget == PreviousWidget.prompt:
|
||||
self._stack.setCurrentWidget(self.prompt)
|
||||
elif self._previous_widget == PreviousWidget.command:
|
||||
self._stack.setCurrentWidget(self.cmd)
|
||||
self._stack.setCurrentWidget(self.command)
|
||||
elif self._previous_widget == PreviousWidget.none:
|
||||
self.maybe_hide()
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("Unknown _previous_widget!")
|
||||
return
|
||||
self.show()
|
||||
log.statusbar.debug("Displaying message: {} (severity {})".format(
|
||||
text, severity))
|
||||
log.statusbar.debug("Displaying {} message: {}".format(
|
||||
'error' if error else 'text', text))
|
||||
log.statusbar.debug("Remaining: {}".format(self._text_queue))
|
||||
self._set_severity(severity)
|
||||
self._set_error(error)
|
||||
self.txt.set_text(self.txt.Text.temp, text)
|
||||
|
||||
def _show_cmd_widget(self):
|
||||
"""Show command widget instead of temporary text."""
|
||||
self._set_severity(Severity.normal)
|
||||
self._previous_widget = PreviousWidget.command
|
||||
self._set_error(False)
|
||||
self._previous_widget = PreviousWidget.prompt
|
||||
if self._text_pop_timer.isActive():
|
||||
self._timer_was_active = True
|
||||
self._text_pop_timer.stop()
|
||||
self._stack.setCurrentWidget(self.cmd)
|
||||
self.show()
|
||||
|
||||
def _hide_cmd_widget(self):
|
||||
"""Show temporary text instead of command widget."""
|
||||
@@ -389,20 +284,18 @@ class StatusBar(QWidget):
|
||||
self._text_pop_timer.start()
|
||||
self._timer_was_active = False
|
||||
self._stack.setCurrentWidget(self.txt)
|
||||
self.maybe_hide()
|
||||
|
||||
def _show_prompt_widget(self):
|
||||
"""Show prompt widget instead of temporary text."""
|
||||
if self._stack.currentWidget() is self.prompt:
|
||||
return
|
||||
self._set_severity(Severity.normal)
|
||||
self._set_error(False)
|
||||
self._set_prompt_active(True)
|
||||
self._previous_widget = PreviousWidget.prompt
|
||||
if self._text_pop_timer.isActive():
|
||||
self._timer_was_active = True
|
||||
self._text_pop_timer.stop()
|
||||
self._stack.setCurrentWidget(self.prompt)
|
||||
self.show()
|
||||
|
||||
def _hide_prompt_widget(self):
|
||||
"""Show temporary text instead of prompt widget."""
|
||||
@@ -416,19 +309,18 @@ class StatusBar(QWidget):
|
||||
self._text_pop_timer.start()
|
||||
self._timer_was_active = False
|
||||
self._stack.setCurrentWidget(self.txt)
|
||||
self.maybe_hide()
|
||||
|
||||
def _disp_text(self, text, severity, immediately=False):
|
||||
def _disp_text(self, text, error, immediately=False):
|
||||
"""Inner logic for disp_error and disp_temp_text.
|
||||
|
||||
Args:
|
||||
text: The message to display.
|
||||
severity: The severity of the messages.
|
||||
error: Whether it's an error message (True) or normal text (False)
|
||||
immediately: If set, message gets displayed immediately instead of
|
||||
queued.
|
||||
"""
|
||||
log.statusbar.debug("Displaying text: {} (severity={})".format(
|
||||
text, severity))
|
||||
log.statusbar.debug("Displaying text: {} (error={})".format(
|
||||
text, error))
|
||||
mindelta = config.get('ui', 'message-timeout')
|
||||
if self._stopwatch.isNull():
|
||||
delta = None
|
||||
@@ -443,11 +335,10 @@ class StatusBar(QWidget):
|
||||
# immediately. We then start the pop_timer only to restore the
|
||||
# normal state in 2 seconds.
|
||||
log.statusbar.debug("Displaying immediately")
|
||||
self._set_severity(severity)
|
||||
self.show()
|
||||
self._set_error(error)
|
||||
self.txt.set_text(self.txt.Text.temp, text)
|
||||
self._text_pop_timer.start()
|
||||
elif self._text_queue and self._text_queue[-1] == (severity, text):
|
||||
elif self._text_queue and self._text_queue[-1] == (error, text):
|
||||
# If we get the same message multiple times in a row and we're
|
||||
# still displaying it *anyways* we ignore the new one
|
||||
log.statusbar.debug("ignoring")
|
||||
@@ -457,15 +348,14 @@ class StatusBar(QWidget):
|
||||
# We display this immediately and restart the timer.to clear it and
|
||||
# display the rest of the queue later.
|
||||
log.statusbar.debug("Moving to beginning of queue")
|
||||
self._set_severity(severity)
|
||||
self.show()
|
||||
self._set_error(error)
|
||||
self.txt.set_text(self.txt.Text.temp, text)
|
||||
self._text_pop_timer.start()
|
||||
else:
|
||||
# There are still some messages to be displayed, so we queue this
|
||||
# up.
|
||||
log.statusbar.debug("queueing")
|
||||
self._text_queue.append((severity, text))
|
||||
self._text_queue.append((error, text))
|
||||
self._text_pop_timer.start()
|
||||
|
||||
@pyqtSlot(str, bool)
|
||||
@@ -477,18 +367,7 @@ class StatusBar(QWidget):
|
||||
immediately: If set, message gets displayed immediately instead of
|
||||
queued.
|
||||
"""
|
||||
self._disp_text(text, Severity.error, immediately)
|
||||
|
||||
@pyqtSlot(str, bool)
|
||||
def disp_warning(self, text, immediately=False):
|
||||
"""Display a warning in the statusbar.
|
||||
|
||||
Args:
|
||||
text: The message to display.
|
||||
immediately: If set, message gets displayed immediately instead of
|
||||
queued.
|
||||
"""
|
||||
self._disp_text(text, Severity.warning, immediately)
|
||||
self._disp_text(text, True, immediately)
|
||||
|
||||
@pyqtSlot(str, bool)
|
||||
def disp_temp_text(self, text, immediately):
|
||||
@@ -499,7 +378,7 @@ class StatusBar(QWidget):
|
||||
immediately: If set, message gets displayed immediately instead of
|
||||
queued.
|
||||
"""
|
||||
self._disp_text(text, Severity.normal, immediately)
|
||||
self._disp_text(text, False, immediately)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def set_text(self, val):
|
||||
@@ -509,29 +388,25 @@ class StatusBar(QWidget):
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def on_mode_entered(self, mode):
|
||||
"""Mark certain modes in the commandline."""
|
||||
keyparsers = objreg.get('keyparsers', scope='window',
|
||||
window=self._win_id)
|
||||
if keyparsers[mode].passthrough:
|
||||
mode_manager = objreg.get('mode-manager', scope='window',
|
||||
window=self._win_id)
|
||||
if mode in mode_manager.passthrough:
|
||||
self._set_mode_text(mode.name)
|
||||
if mode in (usertypes.KeyMode.insert,
|
||||
usertypes.KeyMode.command,
|
||||
usertypes.KeyMode.caret):
|
||||
self.set_mode_active(mode, True)
|
||||
if mode == usertypes.KeyMode.insert:
|
||||
self._set_insert_active(True)
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode, usertypes.KeyMode)
|
||||
def on_mode_left(self, old_mode, new_mode):
|
||||
"""Clear marked mode."""
|
||||
keyparsers = objreg.get('keyparsers', scope='window',
|
||||
window=self._win_id)
|
||||
if keyparsers[old_mode].passthrough:
|
||||
if keyparsers[new_mode].passthrough:
|
||||
mode_manager = objreg.get('mode-manager', scope='window',
|
||||
window=self._win_id)
|
||||
if old_mode in mode_manager.passthrough:
|
||||
if new_mode in mode_manager.passthrough:
|
||||
self._set_mode_text(new_mode.name)
|
||||
else:
|
||||
self.txt.set_text(self.txt.Text.normal, '')
|
||||
if old_mode in (usertypes.KeyMode.insert,
|
||||
usertypes.KeyMode.command,
|
||||
usertypes.KeyMode.caret):
|
||||
self.set_mode_active(old_mode, False)
|
||||
if old_mode == usertypes.KeyMode.insert:
|
||||
self._set_insert_active(False)
|
||||
|
||||
@config.change_filter('ui', 'message-timeout')
|
||||
def set_pop_timer_interval(self):
|
||||
|
||||
@@ -26,7 +26,7 @@ from qutebrowser.keyinput import modeman, modeparsers
|
||||
from qutebrowser.commands import cmdexc, cmdutils
|
||||
from qutebrowser.misc import cmdhistory
|
||||
from qutebrowser.misc import miscwidgets as misc
|
||||
from qutebrowser.utils import usertypes, log, objreg, qtutils
|
||||
from qutebrowser.utils import usertypes, log, objreg
|
||||
|
||||
|
||||
class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
||||
@@ -39,6 +39,10 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
||||
Signals:
|
||||
got_cmd: Emitted when a command is triggered by the user.
|
||||
arg: The command string.
|
||||
got_search: Emitted when the user started a new search.
|
||||
arg: The search term.
|
||||
got_rev_search: Emitted when the user started a new reverse search.
|
||||
arg: The search term.
|
||||
clear_completion_selection: Emitted before the completion widget is
|
||||
hidden.
|
||||
hide_completion: Emitted when the completion widget should be hidden.
|
||||
@@ -48,6 +52,8 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
||||
"""
|
||||
|
||||
got_cmd = pyqtSignal(str)
|
||||
got_search = pyqtSignal(str)
|
||||
got_search_rev = pyqtSignal(str)
|
||||
clear_completion_selection = pyqtSignal()
|
||||
hide_completion = pyqtSignal()
|
||||
update_completion = pyqtSignal()
|
||||
@@ -58,10 +64,8 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
||||
misc.CommandLineEdit.__init__(self, parent)
|
||||
misc.MinimalLineEditMixin.__init__(self)
|
||||
self._win_id = win_id
|
||||
command_history = objreg.get('command-history')
|
||||
self.history.handle_private_mode = True
|
||||
self.history.history = command_history.data
|
||||
self.history.changed.connect(command_history.changed)
|
||||
self.history.history = objreg.get('command-history').data
|
||||
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Ignored)
|
||||
self.cursorPositionChanged.connect(self.update_completion)
|
||||
self.textChanged.connect(self.update_completion)
|
||||
@@ -92,7 +96,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
||||
|
||||
@cmdutils.register(instance='status-command', name='set-cmd-text',
|
||||
scope='window', maxsplit=0)
|
||||
def set_cmd_text_command(self, text, space=False):
|
||||
def set_cmd_text_command(self, text):
|
||||
"""Preset the statusbar to some text.
|
||||
|
||||
//
|
||||
@@ -102,29 +106,19 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
||||
|
||||
Args:
|
||||
text: The commandline to set.
|
||||
space: If given, a space is added to the end.
|
||||
"""
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=self._win_id)
|
||||
if '{url}' in text:
|
||||
try:
|
||||
url = tabbed_browser.current_url().toString(
|
||||
QUrl.FullyEncoded | QUrl.RemovePassword)
|
||||
except qtutils.QtValueError as e:
|
||||
msg = "Current URL is invalid"
|
||||
if e.reason:
|
||||
msg += " ({})".format(e.reason)
|
||||
msg += "!"
|
||||
raise cmdexc.CommandError(msg)
|
||||
url = tabbed_browser.current_url().toString(
|
||||
QUrl.FullyEncoded | QUrl.RemovePassword)
|
||||
# FIXME we currently replace the URL in any place in the arguments,
|
||||
# rather than just replacing it if it is a dedicated argument. We
|
||||
# could split the args, but then trailing spaces would be lost, so
|
||||
# I'm not sure what's the best thing to do here
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/123
|
||||
text = text.replace('{url}', url)
|
||||
if space:
|
||||
text += ' '
|
||||
if not text or text[0] not in modeparsers.STARTCHARS:
|
||||
if not text[0] in modeparsers.STARTCHARS:
|
||||
raise cmdexc.CommandError(
|
||||
"Invalid command text '{}'.".format(text))
|
||||
self.set_cmd_text(text)
|
||||
@@ -161,21 +155,22 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
||||
modes=[usertypes.KeyMode.command], scope='window')
|
||||
def command_accept(self):
|
||||
"""Execute the command currently in the commandline."""
|
||||
prefixes = {
|
||||
':': '',
|
||||
'/': 'search -- ',
|
||||
'?': 'search -r -- ',
|
||||
signals = {
|
||||
':': self.got_cmd,
|
||||
'/': self.got_search,
|
||||
'?': self.got_search_rev,
|
||||
}
|
||||
text = self.text()
|
||||
self.history.append(text)
|
||||
modeman.leave(self._win_id, usertypes.KeyMode.command, 'cmd accept')
|
||||
self.got_cmd.emit(prefixes[text[0]] + text[1:])
|
||||
if text[0] in signals:
|
||||
signals[text[0]].emit(text.lstrip(text[0]))
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def on_mode_left(self, mode):
|
||||
"""Clear up when command mode was left.
|
||||
|
||||
- Clear the statusbar text if it's explicitly unfocused.
|
||||
- Clear the statusbar text if it's explicitely unfocused.
|
||||
- Clear completion selection
|
||||
- Hide completion
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
"""The progress bar in the statusbar."""
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, QSize
|
||||
from PyQt5.QtCore import pyqtSlot
|
||||
from PyQt5.QtWidgets import QProgressBar, QSizePolicy
|
||||
|
||||
from qutebrowser.browser import webview
|
||||
@@ -50,7 +50,7 @@ class Progress(QProgressBar):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
style.set_register_stylesheet(self)
|
||||
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Ignored)
|
||||
self.setTextVisible(False)
|
||||
self.hide()
|
||||
|
||||
@@ -75,9 +75,3 @@ class Progress(QProgressBar):
|
||||
self.show()
|
||||
else:
|
||||
self.hide()
|
||||
|
||||
def sizeHint(self):
|
||||
"""Set the height to the text height plus some padding."""
|
||||
width = super().sizeHint().width()
|
||||
height = self.fontMetrics().height() + 3
|
||||
return QSize(width, height)
|
||||
|
||||
@@ -21,8 +21,7 @@
|
||||
|
||||
import functools
|
||||
|
||||
from PyQt5.QtCore import QSize
|
||||
from PyQt5.QtWidgets import QHBoxLayout, QWidget, QLineEdit, QSizePolicy
|
||||
from PyQt5.QtWidgets import QHBoxLayout, QWidget, QLineEdit
|
||||
|
||||
from qutebrowser.mainwindow.statusbar import textbase, prompter
|
||||
from qutebrowser.utils import objreg, utils
|
||||
@@ -36,16 +35,6 @@ class PromptLineEdit(misc.MinimalLineEditMixin, QLineEdit):
|
||||
def __init__(self, parent=None):
|
||||
QLineEdit.__init__(self, parent)
|
||||
misc.MinimalLineEditMixin.__init__(self)
|
||||
self.textChanged.connect(self.updateGeometry)
|
||||
|
||||
def sizeHint(self):
|
||||
"""Dynamically calculate the needed size."""
|
||||
height = super().sizeHint().height()
|
||||
text = self.text()
|
||||
if not text:
|
||||
text = 'x'
|
||||
width = self.fontMetrics().width(text)
|
||||
return QSize(width, height)
|
||||
|
||||
|
||||
class Prompt(QWidget):
|
||||
@@ -69,8 +58,6 @@ class Prompt(QWidget):
|
||||
self._hbox.addWidget(self.txt)
|
||||
|
||||
self.lineedit = PromptLineEdit()
|
||||
self.lineedit.setSizePolicy(QSizePolicy.MinimumExpanding,
|
||||
QSizePolicy.Fixed)
|
||||
self._hbox.addWidget(self.lineedit)
|
||||
|
||||
prompter_obj = prompter.Prompter(win_id)
|
||||
|
||||
@@ -33,7 +33,6 @@ from qutebrowser.utils import usertypes, log, qtutils, objreg, utils
|
||||
PromptContext = collections.namedtuple('PromptContext',
|
||||
['question', 'text', 'input_text',
|
||||
'echo_mode', 'input_visible'])
|
||||
AuthTuple = collections.namedtuple('AuthTuple', ['user', 'password'])
|
||||
|
||||
|
||||
class Prompter(QObject):
|
||||
@@ -188,7 +187,7 @@ class Prompter(QObject):
|
||||
def shutdown(self):
|
||||
"""Cancel all blocking questions.
|
||||
|
||||
Quits and removes all running event loops.
|
||||
Quits and removes all running eventloops.
|
||||
|
||||
Return:
|
||||
True if loops needed to be aborted,
|
||||
@@ -238,7 +237,7 @@ class Prompter(QObject):
|
||||
elif self._question.mode == usertypes.PromptMode.user_pwd:
|
||||
# User just entered a password
|
||||
password = prompt.lineedit.text()
|
||||
self._question.answer = AuthTuple(self._question.user, password)
|
||||
self._question.answer = (self._question.user, password)
|
||||
modeman.maybe_leave(self._win_id, usertypes.KeyMode.prompt,
|
||||
'prompt accept')
|
||||
self._question.done()
|
||||
|
||||
@@ -71,10 +71,10 @@ class Text(textbase.TextBase):
|
||||
def maybe_reset_text(self, text):
|
||||
"""Clear a normal text if it still matches an expected text."""
|
||||
if self._normaltext == text:
|
||||
log.statusbar.debug("Resetting: '{}'".format(text))
|
||||
log.misc.debug("Resetting: '{}'".format(text))
|
||||
self.set_text(self.Text.normal, '')
|
||||
else:
|
||||
log.statusbar.debug("Ignoring reset: '{}'".format(text))
|
||||
log.misc.debug("Ignoring reset: '{}'".format(text))
|
||||
|
||||
@config.change_filter('ui', 'display-statusbar-messages')
|
||||
def update_text(self):
|
||||
|
||||
@@ -32,7 +32,7 @@ class TextBase(QLabel):
|
||||
|
||||
Unlike QLabel, the text will get elided.
|
||||
|
||||
Eliding is loosely based on
|
||||
Eliding is loosly based on
|
||||
http://gedgedev.blogspot.ch/2010/12/elided-labels-in-qt.html
|
||||
|
||||
Attributes:
|
||||
@@ -64,13 +64,13 @@ class TextBase(QLabel):
|
||||
|
||||
This update the elided text after setting the text, and also works
|
||||
around a weird QLabel redrawing bug where it doesn't redraw correctly
|
||||
when the text is empty -- we explicitly need to call repaint() to
|
||||
when the text is empty -- we explicitely need to call repaint() to
|
||||
resolve this.
|
||||
|
||||
More info:
|
||||
|
||||
http://stackoverflow.com/q/21890462/2085149
|
||||
https://bugreports.qt.io/browse/QTBUG-36945
|
||||
https://bugreports.qt-project.org/browse/QTBUG-36945
|
||||
https://codereview.qt-project.org/#/c/79181/
|
||||
|
||||
Args:
|
||||
|
||||
@@ -40,7 +40,7 @@ class UrlText(textbase.TextBase):
|
||||
_normal_url: The normal URL to be displayed as a UrlType instance.
|
||||
_normal_url_type: The type of the normal URL as a UrlType instance.
|
||||
_hover_url: The URL we're currently hovering over.
|
||||
_ssl_errors: Whether SSL errors occurred while loading.
|
||||
_ssl_errors: Whether SSL errors occured while loading.
|
||||
|
||||
Class attributes:
|
||||
_urltype: The URL type to show currently (normal/ok/error/warn/hover).
|
||||
@@ -90,6 +90,7 @@ class UrlText(textbase.TextBase):
|
||||
Return:
|
||||
The urltype as a string (!)
|
||||
"""
|
||||
# pylint: disable=method-hidden
|
||||
if self._urltype is None:
|
||||
return ""
|
||||
else:
|
||||
|
||||
@@ -25,33 +25,31 @@ import collections
|
||||
from PyQt5.QtWidgets import QSizePolicy
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QSize, QTimer, QUrl
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWebKitWidgets import QWebPage
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.commands import cmdexc
|
||||
from qutebrowser.keyinput import modeman
|
||||
from qutebrowser.mainwindow import tabwidget
|
||||
from qutebrowser.browser import signalfilter, webview
|
||||
from qutebrowser.utils import log, usertypes, utils, qtutils, objreg, urlutils
|
||||
from qutebrowser.browser import signalfilter, commands, webview
|
||||
from qutebrowser.utils import (log, message, usertypes, utils, qtutils, objreg,
|
||||
urlutils)
|
||||
|
||||
|
||||
UndoEntry = collections.namedtuple('UndoEntry', ['url', 'history'])
|
||||
|
||||
|
||||
class TabDeletedError(Exception):
|
||||
|
||||
"""Exception raised when _tab_index is called for a deleted tab."""
|
||||
|
||||
|
||||
class TabbedBrowser(tabwidget.TabWidget):
|
||||
|
||||
"""A TabWidget with QWebViews inside.
|
||||
|
||||
Provides methods to manage tabs, convenience methods to interact with the
|
||||
current tab (cur_*) and filters signals to re-emit them when they occurred
|
||||
current tab (cur_*) and filters signals to re-emit them when they occured
|
||||
in the currently visible tab.
|
||||
|
||||
For all tab-specific signals (cur_*) emitted by a tab, this happens:
|
||||
- the signal gets filtered with _filter_signals and self.cur_* gets
|
||||
emitted if the signal occurred in the current tab.
|
||||
emitted if the signal occured in the current tab.
|
||||
|
||||
Attributes:
|
||||
_win_id: The window ID this tabbedbrowser is associated with.
|
||||
@@ -61,7 +59,6 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
tabbar -> new-tab-position set to 'left'.
|
||||
_tab_insert_idx_right: Same as above, for 'right'.
|
||||
_undo_stack: List of UndoEntry namedtuples of closed tabs.
|
||||
_shutting_down: Whether we're currently shutting down.
|
||||
|
||||
Signals:
|
||||
cur_progress: Progress of the current tab changed (loadProgress).
|
||||
@@ -100,39 +97,27 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
self._win_id = win_id
|
||||
self._tab_insert_idx_left = 0
|
||||
self._tab_insert_idx_right = -1
|
||||
self._shutting_down = False
|
||||
self.tabCloseRequested.connect(self.on_tab_close_requested)
|
||||
self.currentChanged.connect(self.on_current_changed)
|
||||
self.cur_load_started.connect(self.on_cur_load_started)
|
||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
self._undo_stack = []
|
||||
self._filter = signalfilter.SignalFilter(win_id, self)
|
||||
dispatcher = commands.CommandDispatcher(win_id)
|
||||
objreg.register('command-dispatcher', dispatcher, scope='window',
|
||||
window=win_id)
|
||||
self.destroyed.connect(
|
||||
functools.partial(objreg.delete, 'command-dispatcher',
|
||||
scope='window', window=win_id))
|
||||
self._now_focused = None
|
||||
# FIXME adjust this to font size
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/119
|
||||
self.setIconSize(QSize(12, 12))
|
||||
objreg.get('config').changed.connect(self.update_favicons)
|
||||
objreg.get('config').changed.connect(self.update_window_title)
|
||||
objreg.get('config').changed.connect(self.update_tab_titles)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, count=self.count())
|
||||
|
||||
def _tab_index(self, tab):
|
||||
"""Get the index of a given tab.
|
||||
|
||||
Raises TabDeletedError if the tab doesn't exist anymore.
|
||||
"""
|
||||
try:
|
||||
idx = self.indexOf(tab)
|
||||
except RuntimeError as e:
|
||||
log.webview.debug("Got invalid tab ({})!".format(e))
|
||||
raise TabDeletedError(e)
|
||||
if idx == -1:
|
||||
log.webview.debug("Got invalid tab (index is -1)!")
|
||||
raise TabDeletedError("index is -1!")
|
||||
return idx
|
||||
|
||||
def widgets(self):
|
||||
"""Get a list of open tab widgets.
|
||||
|
||||
@@ -144,28 +129,13 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
w.append(self.widget(i))
|
||||
return w
|
||||
|
||||
@config.change_filter('ui', 'window-title-format')
|
||||
def update_window_title(self):
|
||||
"""Change the window title to match the current tab."""
|
||||
idx = self.currentIndex()
|
||||
if idx == -1:
|
||||
# (e.g. last tab removed)
|
||||
log.webview.debug("Not updating window title because index is -1")
|
||||
return
|
||||
tabtitle = self.page_title(idx)
|
||||
widget = self.widget(idx)
|
||||
|
||||
fields = {}
|
||||
if widget.load_status == webview.LoadStatus.loading:
|
||||
fields['perc'] = '[{}%] '.format(widget.progress)
|
||||
def _change_app_title(self, text):
|
||||
"""Change the window title based on the tab text."""
|
||||
if not text:
|
||||
title = 'qutebrowser'
|
||||
else:
|
||||
fields['perc'] = ''
|
||||
fields['perc_raw'] = widget.progress
|
||||
fields['title'] = tabtitle
|
||||
fields['title_sep'] = ' - ' if tabtitle else ''
|
||||
fields['id'] = self._win_id
|
||||
fmt = config.get('ui', 'window-title-format')
|
||||
self.window().setWindowTitle(fmt.format(**fields))
|
||||
title = '{} - qutebrowser'.format(text)
|
||||
self.window().setWindowTitle(title)
|
||||
|
||||
def _connect_tab_signals(self, tab):
|
||||
"""Set up the needed signals for tab."""
|
||||
@@ -217,13 +187,22 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
url = QUrl()
|
||||
else:
|
||||
url = widget.cur_url
|
||||
# It's possible for url to be invalid, but the caller will handle that.
|
||||
qtutils.ensure_valid(url)
|
||||
try:
|
||||
qtutils.ensure_valid(url)
|
||||
except qtutils.QtValueError as e:
|
||||
msg = "Current URL is invalid"
|
||||
if e.reason:
|
||||
msg += " ({})".format(e.reason)
|
||||
msg += "!"
|
||||
raise cmdexc.CommandError(msg)
|
||||
return url
|
||||
|
||||
def shutdown(self):
|
||||
"""Try to shut down all tabs cleanly."""
|
||||
self._shutting_down = True
|
||||
try:
|
||||
self.currentChanged.disconnect()
|
||||
except TypeError:
|
||||
log.destroy.exception("Error while shutting down tabs")
|
||||
for tab in self.widgets():
|
||||
self._remove_tab(tab)
|
||||
|
||||
@@ -241,10 +220,6 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
self.close_window.emit()
|
||||
elif last_close == 'blank':
|
||||
tab.openurl(QUrl('about:blank'))
|
||||
elif last_close == 'startpage':
|
||||
tab.openurl(QUrl(config.get('general', 'startpage')[0]))
|
||||
elif last_close == 'default-page':
|
||||
tab.openurl(config.get('general', 'default-page'))
|
||||
|
||||
def _remove_tab(self, tab):
|
||||
"""Remove a tab from the tab list and delete it properly.
|
||||
@@ -296,7 +271,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
newtab: True to open URL in a new tab, False otherwise.
|
||||
"""
|
||||
qtutils.ensure_valid(url)
|
||||
if newtab or self.currentWidget() is None:
|
||||
if newtab:
|
||||
self.tabopen(url, background=False)
|
||||
else:
|
||||
self.currentWidget().openurl(url)
|
||||
@@ -327,12 +302,12 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
url: The URL to open as QUrl or None for an empty tab.
|
||||
background: Whether to open the tab in the background.
|
||||
if None, the background-tabs setting decides.
|
||||
explicit: Whether the tab was opened explicitly.
|
||||
explicit: Whether the tab was opened explicitely.
|
||||
If this is set, the new position might be different. With
|
||||
the default settings we handle it like Chromium does:
|
||||
- Tabs from clicked links etc. are to the right of
|
||||
the current.
|
||||
- Explicitly opened tabs are at the very right.
|
||||
- Explicitely opened tabs are at the very right.
|
||||
|
||||
Return:
|
||||
The opened WebView instance.
|
||||
@@ -342,10 +317,9 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
log.webview.debug("Creating new tab with URL {}".format(url))
|
||||
if config.get('tabs', 'tabs-are-windows') and self.count() > 0:
|
||||
from qutebrowser.mainwindow import mainwindow
|
||||
window = mainwindow.MainWindow()
|
||||
window.show()
|
||||
window = mainwindow.MainWindow.spawn()
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=window.win_id)
|
||||
window=window)
|
||||
return tabbed_browser.tabopen(url, background, explicit)
|
||||
tab = webview.WebView(self._win_id, self)
|
||||
self._connect_tab_signals(tab)
|
||||
@@ -364,7 +338,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
"""Get the index of a tab to insert.
|
||||
|
||||
Args:
|
||||
explicit: Whether the tab was opened explicitly.
|
||||
explicit: Whether the tab was opened explicitely.
|
||||
|
||||
Return:
|
||||
The index of the new tab.
|
||||
@@ -395,6 +369,36 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
self._tab_insert_idx_right))
|
||||
return idx
|
||||
|
||||
@pyqtSlot(str, int)
|
||||
def search(self, text, flags):
|
||||
"""Search for text in the current page.
|
||||
|
||||
Args:
|
||||
text: The text to search for.
|
||||
flags: The QWebPage::FindFlags.
|
||||
"""
|
||||
log.webview.debug("Searching with text '{}' and flags "
|
||||
"0x{:04x}.".format(text, int(flags)))
|
||||
widget = self.currentWidget()
|
||||
old_scroll_pos = widget.scroll_pos
|
||||
found = widget.findText(text, flags)
|
||||
if not found and not flags & QWebPage.HighlightAllOccurrences and text:
|
||||
message.error(self._win_id, "Text '{}' not found on "
|
||||
"page!".format(text), immediately=True)
|
||||
else:
|
||||
backward = int(flags) & QWebPage.FindBackward
|
||||
|
||||
def check_scroll_pos():
|
||||
"""Check if the scroll position got smaller and show info."""
|
||||
if not backward and widget.scroll_pos < old_scroll_pos:
|
||||
message.info(self._win_id, "Search hit BOTTOM, continuing "
|
||||
"at TOP", immediately=True)
|
||||
elif backward and widget.scroll_pos > old_scroll_pos:
|
||||
message.info(self._win_id, "Search hit TOP, continuing at "
|
||||
"BOTTOM", immediately=True)
|
||||
# We first want QWebPage to refresh.
|
||||
QTimer.singleShot(0, check_scroll_pos)
|
||||
|
||||
@config.change_filter('tabs', 'show-favicons')
|
||||
def update_favicons(self):
|
||||
"""Update favicons when config was changed."""
|
||||
@@ -407,23 +411,21 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
|
||||
@pyqtSlot()
|
||||
def on_load_started(self, tab):
|
||||
"""Clear icon and update title when a tab started loading.
|
||||
"""Clear icon when a tab started loading.
|
||||
|
||||
Args:
|
||||
tab: The tab where the signal belongs to.
|
||||
"""
|
||||
try:
|
||||
idx = self._tab_index(tab)
|
||||
except TabDeletedError:
|
||||
idx = self.indexOf(tab)
|
||||
except RuntimeError:
|
||||
# We can get signals for tabs we already deleted...
|
||||
return
|
||||
self.update_tab_title(idx)
|
||||
if tab.keep_icon:
|
||||
tab.keep_icon = False
|
||||
else:
|
||||
self.setTabIcon(idx, QIcon())
|
||||
if idx == self.currentIndex():
|
||||
self.update_window_title()
|
||||
if idx == -1:
|
||||
# We can get signals for tabs we already deleted...
|
||||
log.webview.debug("Got invalid tab {}!".format(tab))
|
||||
return
|
||||
self.setTabIcon(idx, QIcon())
|
||||
|
||||
@pyqtSlot()
|
||||
def on_cur_load_started(self):
|
||||
@@ -447,15 +449,19 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
log.webview.debug("Ignoring title change to '{}'.".format(text))
|
||||
return
|
||||
try:
|
||||
idx = self._tab_index(tab)
|
||||
except TabDeletedError:
|
||||
idx = self.indexOf(tab)
|
||||
except RuntimeError:
|
||||
# We can get signals for tabs we already deleted...
|
||||
return
|
||||
log.webview.debug("Changing title for idx {} to '{}'".format(
|
||||
idx, text))
|
||||
self.set_page_title(idx, text)
|
||||
if idx == -1:
|
||||
# We can get signals for tabs we already deleted...
|
||||
log.webview.debug("Got invalid tab {}!".format(tab))
|
||||
return
|
||||
self.setTabText(idx, text.replace('&', '&&'))
|
||||
if idx == self.currentIndex():
|
||||
self.update_window_title()
|
||||
self._change_app_title(text)
|
||||
|
||||
@pyqtSlot(webview.WebView, str)
|
||||
def on_url_text_changed(self, tab, url):
|
||||
@@ -466,12 +472,16 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
url: The new URL.
|
||||
"""
|
||||
try:
|
||||
idx = self._tab_index(tab)
|
||||
except TabDeletedError:
|
||||
idx = self.indexOf(tab)
|
||||
except RuntimeError:
|
||||
# We can get signals for tabs we already deleted...
|
||||
return
|
||||
if not self.page_title(idx):
|
||||
self.set_page_title(idx, url)
|
||||
if idx == -1:
|
||||
# We can get signals for tabs we already deleted...
|
||||
log.webview.debug("Got invalid tab {}!".format(tab))
|
||||
return
|
||||
if not self.tabText(idx):
|
||||
self.setTabText(idx, url)
|
||||
|
||||
@pyqtSlot(webview.WebView)
|
||||
def on_icon_changed(self, tab):
|
||||
@@ -485,10 +495,14 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
if not config.get('tabs', 'show-favicons'):
|
||||
return
|
||||
try:
|
||||
idx = self._tab_index(tab)
|
||||
except TabDeletedError:
|
||||
idx = self.indexOf(tab)
|
||||
except RuntimeError:
|
||||
# We can get signals for tabs we already deleted...
|
||||
return
|
||||
if idx == -1:
|
||||
# We can get *_changed signals for tabs we already deleted...
|
||||
log.webview.debug("Got invalid tab {}!".format(tab))
|
||||
return
|
||||
self.setTabIcon(idx, tab.icon())
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
@@ -506,21 +520,20 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
@pyqtSlot(int)
|
||||
def on_current_changed(self, idx):
|
||||
"""Set last-focused-tab and leave hinting mode when focus changed."""
|
||||
if idx == -1 or self._shutting_down:
|
||||
# closing the last tab (before quitting) or shutting down
|
||||
if idx == -1:
|
||||
# closing the last tab (before quitting)
|
||||
return
|
||||
tab = self.widget(idx)
|
||||
log.modes.debug("Current tab changed, focusing {!r}".format(tab))
|
||||
tab.setFocus()
|
||||
for mode in (usertypes.KeyMode.hint, usertypes.KeyMode.insert,
|
||||
usertypes.KeyMode.caret):
|
||||
for mode in (usertypes.KeyMode.hint, usertypes.KeyMode.insert):
|
||||
modeman.maybe_leave(self._win_id, mode, 'tab changed')
|
||||
if self._now_focused is not None:
|
||||
objreg.register('last-focused-tab', self._now_focused, update=True,
|
||||
scope='window', window=self._win_id)
|
||||
self._now_focused = tab
|
||||
self.current_tab_changed.emit(tab)
|
||||
QTimer.singleShot(0, self.update_window_title)
|
||||
self._change_app_title(self.tabText(idx))
|
||||
self._tab_insert_idx_left = self.currentIndex()
|
||||
self._tab_insert_idx_right = self.currentIndex() + 1
|
||||
|
||||
@@ -532,18 +545,15 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
def on_load_progress(self, tab, perc):
|
||||
"""Adjust tab indicator on load progress."""
|
||||
try:
|
||||
idx = self._tab_index(tab)
|
||||
except TabDeletedError:
|
||||
idx = self.indexOf(tab)
|
||||
except RuntimeError:
|
||||
# We can get signals for tabs we already deleted...
|
||||
return
|
||||
start = config.get('colors', 'tabs.indicator.start')
|
||||
stop = config.get('colors', 'tabs.indicator.stop')
|
||||
system = config.get('colors', 'tabs.indicator.system')
|
||||
color = utils.interpolate_color(start, stop, perc, system)
|
||||
self.set_tab_indicator_color(idx, color)
|
||||
self.update_tab_title(idx)
|
||||
if idx == self.currentIndex():
|
||||
self.update_window_title()
|
||||
self.tabBar().set_tab_indicator_color(idx, color)
|
||||
|
||||
def on_load_finished(self, tab):
|
||||
"""Adjust tab indicator when loading finished.
|
||||
@@ -553,21 +563,18 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
See https://github.com/The-Compiler/qutebrowser/issues/84
|
||||
"""
|
||||
try:
|
||||
idx = self._tab_index(tab)
|
||||
except TabDeletedError:
|
||||
idx = self.indexOf(tab)
|
||||
except RuntimeError:
|
||||
# We can get signals for tabs we already deleted...
|
||||
return
|
||||
if tab.page().error_occurred:
|
||||
if tab.page().error_occured:
|
||||
color = config.get('colors', 'tabs.indicator.error')
|
||||
else:
|
||||
start = config.get('colors', 'tabs.indicator.start')
|
||||
stop = config.get('colors', 'tabs.indicator.stop')
|
||||
system = config.get('colors', 'tabs.indicator.system')
|
||||
color = utils.interpolate_color(start, stop, 100, system)
|
||||
self.set_tab_indicator_color(idx, color)
|
||||
self.update_tab_title(idx)
|
||||
if idx == self.currentIndex():
|
||||
self.update_window_title()
|
||||
self.tabBar().set_tab_indicator_color(idx, color)
|
||||
|
||||
def resizeEvent(self, e):
|
||||
"""Extend resizeEvent of QWidget to emit a resized signal afterwards.
|
||||
@@ -577,14 +584,3 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
"""
|
||||
super().resizeEvent(e)
|
||||
self.resized.emit(self.geometry())
|
||||
|
||||
def wheelEvent(self, e):
|
||||
"""Override wheelEvent of QWidget to forward it to the focused tab.
|
||||
|
||||
Args:
|
||||
e: The QWheelEvent
|
||||
"""
|
||||
if self._now_focused is not None:
|
||||
self._now_focused.wheelEvent(e)
|
||||
else:
|
||||
e.ignore()
|
||||
|
||||
@@ -26,14 +26,13 @@ Module attributes:
|
||||
|
||||
import functools
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QSize, QRect, QPoint, QTimer
|
||||
from PyQt5.QtCore import pyqtSlot, Qt, QSize, QRect, QPoint, QTimer
|
||||
from PyQt5.QtWidgets import (QTabWidget, QTabBar, QSizePolicy, QCommonStyle,
|
||||
QStyle, QStylePainter, QStyleOptionTab)
|
||||
from PyQt5.QtGui import QIcon, QPalette, QColor
|
||||
|
||||
from qutebrowser.utils import qtutils, objreg, utils
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.browser import webview
|
||||
|
||||
|
||||
PM_TabBarPadding = QStyle.PM_CustomBase
|
||||
@@ -41,24 +40,13 @@ PM_TabBarPadding = QStyle.PM_CustomBase
|
||||
|
||||
class TabWidget(QTabWidget):
|
||||
|
||||
"""The tab widget used for TabbedBrowser.
|
||||
|
||||
Signals:
|
||||
tab_index_changed: Emitted when the current tab was changed.
|
||||
arg 0: The index of the tab which is now focused.
|
||||
arg 1: The total count of tabs.
|
||||
"""
|
||||
|
||||
tab_index_changed = pyqtSignal(int, int)
|
||||
"""The tabwidget used for TabbedBrowser."""
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
bar = TabBar(win_id)
|
||||
self.setTabBar(bar)
|
||||
bar.tabCloseRequested.connect(self.tabCloseRequested)
|
||||
bar.tabMoved.connect(functools.partial(
|
||||
QTimer.singleShot, 0, self.update_tab_titles))
|
||||
bar.currentChanged.connect(self.emit_tab_index_changed)
|
||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
self.setDocumentMode(True)
|
||||
self.setElideMode(Qt.ElideRight)
|
||||
@@ -74,134 +62,16 @@ class TabWidget(QTabWidget):
|
||||
self.setMovable(config.get('tabs', 'movable'))
|
||||
self.setTabsClosable(False)
|
||||
position = config.get('tabs', 'position')
|
||||
selection_behavior = config.get('tabs', 'select-on-remove')
|
||||
selection_behaviour = config.get('tabs', 'select-on-remove')
|
||||
self.setTabPosition(position)
|
||||
tabbar.vertical = position in (QTabWidget.West, QTabWidget.East)
|
||||
tabbar.setSelectionBehaviorOnRemove(selection_behavior)
|
||||
tabbar.setSelectionBehaviorOnRemove(selection_behaviour)
|
||||
tabbar.refresh()
|
||||
|
||||
def set_tab_indicator_color(self, idx, color):
|
||||
"""Set the tab indicator color.
|
||||
|
||||
Args:
|
||||
idx: The tab index.
|
||||
color: A QColor.
|
||||
"""
|
||||
bar = self.tabBar()
|
||||
bar.set_tab_data(idx, 'indicator-color', color)
|
||||
bar.update(bar.tabRect(idx))
|
||||
|
||||
def set_page_title(self, idx, title):
|
||||
"""Set the tab title user data."""
|
||||
self.tabBar().set_tab_data(idx, 'page-title', title)
|
||||
self.update_tab_title(idx)
|
||||
|
||||
def page_title(self, idx):
|
||||
"""Get the tab title user data."""
|
||||
return self.tabBar().page_title(idx)
|
||||
|
||||
def update_tab_title(self, idx):
|
||||
"""Update the tab text for the given tab."""
|
||||
widget = self.widget(idx)
|
||||
page_title = self.page_title(idx).replace('&', '&&')
|
||||
|
||||
fields = {}
|
||||
if widget.load_status == webview.LoadStatus.loading:
|
||||
fields['perc'] = '[{}%] '.format(widget.progress)
|
||||
else:
|
||||
fields['perc'] = ''
|
||||
fields['perc_raw'] = widget.progress
|
||||
fields['title'] = page_title
|
||||
fields['index'] = idx + 1
|
||||
fields['id'] = widget.tab_id
|
||||
fields['title_sep'] = ' - ' if page_title else ''
|
||||
|
||||
fmt = config.get('tabs', 'title-format')
|
||||
self.tabBar().setTabText(idx, fmt.format(**fields))
|
||||
|
||||
@config.change_filter('tabs', 'title-format')
|
||||
def update_tab_titles(self):
|
||||
"""Update all texts."""
|
||||
for idx in range(self.count()):
|
||||
self.update_tab_title(idx)
|
||||
|
||||
def tabInserted(self, idx):
|
||||
"""Update titles when a tab was inserted."""
|
||||
super().tabInserted(idx)
|
||||
self.update_tab_titles()
|
||||
|
||||
def tabRemoved(self, idx):
|
||||
"""Update titles when a tab was removed."""
|
||||
super().tabRemoved(idx)
|
||||
self.update_tab_titles()
|
||||
|
||||
def addTab(self, page, icon_or_text, text_or_empty=None):
|
||||
"""Override addTab to use our own text setting logic.
|
||||
|
||||
Unfortunately QTabWidget::addTab has these two overloads:
|
||||
- QWidget * page, const QIcon & icon, const QString & label
|
||||
- QWidget * page, const QString & label
|
||||
|
||||
This means we'll get different arguments based on the chosen overload.
|
||||
|
||||
Args:
|
||||
page: The QWidget to add.
|
||||
icon_or_text: Either the QIcon to add or the label.
|
||||
text_or_empty: Either the label or None.
|
||||
|
||||
Return:
|
||||
The index of the newly added tab.
|
||||
"""
|
||||
if text_or_empty is None:
|
||||
icon = None
|
||||
text = icon_or_text
|
||||
new_idx = super().addTab(page, '')
|
||||
else:
|
||||
icon = icon_or_text
|
||||
text = text_or_empty
|
||||
new_idx = super().addTab(page, icon, '')
|
||||
self.set_page_title(new_idx, text)
|
||||
return new_idx
|
||||
|
||||
def insertTab(self, idx, page, icon_or_text, text_or_empty=None):
|
||||
"""Override insertTab to use our own text setting logic.
|
||||
|
||||
Unfortunately QTabWidget::insertTab has these two overloads:
|
||||
- int index, QWidget * page, const QIcon & icon,
|
||||
const QString & label
|
||||
- int index, QWidget * page, const QString & label
|
||||
|
||||
This means we'll get different arguments based on the chosen overload.
|
||||
|
||||
Args:
|
||||
idx: Where to insert the widget.
|
||||
page: The QWidget to add.
|
||||
icon_or_text: Either the QIcon to add or the label.
|
||||
text_or_empty: Either the label or None.
|
||||
|
||||
Return:
|
||||
The index of the newly added tab.
|
||||
"""
|
||||
if text_or_empty is None:
|
||||
icon = None
|
||||
text = icon_or_text
|
||||
new_idx = super().insertTab(idx, page, '')
|
||||
else:
|
||||
icon = icon_or_text
|
||||
text = text_or_empty
|
||||
new_idx = super().insertTab(idx, page, icon, '')
|
||||
self.set_page_title(new_idx, text)
|
||||
return new_idx
|
||||
|
||||
@pyqtSlot(int)
|
||||
def emit_tab_index_changed(self, index):
|
||||
"""Emit the tab_index_changed signal if the current tab changed."""
|
||||
self.tab_index_changed.emit(index, self.count())
|
||||
|
||||
|
||||
class TabBar(QTabBar):
|
||||
|
||||
"""Custom tab bar with our own style.
|
||||
"""Custom tabbar with our own style.
|
||||
|
||||
FIXME: Dragging tabs doesn't look as nice as it does in QTabBar. However,
|
||||
fixing this would be a lot of effort, so we'll postpone it until we're
|
||||
@@ -225,80 +95,46 @@ class TabBar(QTabBar):
|
||||
self.setAutoFillBackground(True)
|
||||
self.set_colors()
|
||||
config_obj.changed.connect(self.set_colors)
|
||||
QTimer.singleShot(0, self._tabhide)
|
||||
QTimer.singleShot(0, self.autohide)
|
||||
config_obj.changed.connect(self.autohide)
|
||||
config_obj.changed.connect(self.alwayshide)
|
||||
config_obj.changed.connect(self.on_tab_colors_changed)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, count=self.count())
|
||||
|
||||
@config.change_filter('tabs', 'hide-auto')
|
||||
@config.change_filter('tabs', 'auto-hide')
|
||||
def autohide(self):
|
||||
"""Hide tab bar if needed when tabs->hide-auto got changed."""
|
||||
self._tabhide()
|
||||
|
||||
@config.change_filter('tabs', 'hide-always')
|
||||
def alwayshide(self):
|
||||
"""Hide tab bar if needed when tabs->hide-always got changed."""
|
||||
self._tabhide()
|
||||
|
||||
def _tabhide(self):
|
||||
"""Hide the tab bar if needed."""
|
||||
hide_auto = config.get('tabs', 'hide-auto')
|
||||
hide_always = config.get('tabs', 'hide-always')
|
||||
if hide_always or (hide_auto and self.count() == 1):
|
||||
"""Auto-hide the tabbar if needed."""
|
||||
auto_hide = config.get('tabs', 'auto-hide')
|
||||
if auto_hide and self.count() == 1:
|
||||
self.hide()
|
||||
else:
|
||||
self.show()
|
||||
|
||||
def set_tab_data(self, idx, key, value):
|
||||
"""Set tab data as a dictionary."""
|
||||
if not 0 <= idx < self.count():
|
||||
raise IndexError("Tab index ({}) out of range ({})!".format(
|
||||
idx, self.count()))
|
||||
data = self.tabData(idx)
|
||||
if data is None:
|
||||
data = {}
|
||||
data[key] = value
|
||||
self.setTabData(idx, data)
|
||||
|
||||
def tab_data(self, idx, key):
|
||||
"""Get tab data for a given key."""
|
||||
if not 0 <= idx < self.count():
|
||||
raise IndexError("Tab index ({}) out of range ({})!".format(
|
||||
idx, self.count()))
|
||||
data = self.tabData(idx)
|
||||
if data is None:
|
||||
data = {}
|
||||
return data[key]
|
||||
|
||||
def page_title(self, idx):
|
||||
"""Get the tab title user data.
|
||||
|
||||
Args:
|
||||
idx: The tab index to get the title for.
|
||||
handle_unset: Whether to return an empty string on KeyError.
|
||||
"""
|
||||
try:
|
||||
return self.tab_data(idx, 'page-title')
|
||||
except KeyError:
|
||||
return ''
|
||||
|
||||
def refresh(self):
|
||||
"""Properly repaint the tab bar and relayout tabs."""
|
||||
# This is a horrible hack, but we need to do this so the underlaying Qt
|
||||
# code sets layoutDirty so it actually relayouts the tabs.
|
||||
self.setIconSize(self.iconSize())
|
||||
|
||||
def set_tab_indicator_color(self, idx, color):
|
||||
"""Set the tab indicator color.
|
||||
|
||||
Args:
|
||||
idx: The tab index.
|
||||
color: A QColor.
|
||||
"""
|
||||
self.setTabData(idx, color)
|
||||
self.update(self.tabRect(idx))
|
||||
|
||||
@config.change_filter('fonts', 'tabbar')
|
||||
def set_font(self):
|
||||
"""Set the tab bar font."""
|
||||
"""Set the tabbar font."""
|
||||
self.setFont(config.get('fonts', 'tabbar'))
|
||||
|
||||
@config.change_filter('colors', 'tabs.bg.bar')
|
||||
def set_colors(self):
|
||||
"""Set the tab bar colors."""
|
||||
"""Set the tabbar colors."""
|
||||
p = self.palette()
|
||||
p.setColor(QPalette.Window, config.get('colors', 'tabs.bg.bar'))
|
||||
self.setPalette(p)
|
||||
@@ -325,7 +161,7 @@ class TabBar(QTabBar):
|
||||
"""Set the minimum tab size to indicator/icon/... text.
|
||||
|
||||
Args:
|
||||
index: The index of the tab to get a size hint for.
|
||||
index: The index of the tab to get a sizehint for.
|
||||
|
||||
Return:
|
||||
A QSize.
|
||||
@@ -391,9 +227,9 @@ class TabBar(QTabBar):
|
||||
def paintEvent(self, _e):
|
||||
"""Override paintEvent to draw the tabs like we want to."""
|
||||
p = QStylePainter(self)
|
||||
tab = QStyleOptionTab()
|
||||
selected = self.currentIndex()
|
||||
for idx in range(self.count()):
|
||||
tab = QStyleOptionTab()
|
||||
self.initStyleOption(tab, idx)
|
||||
if idx == selected:
|
||||
bg_color = config.get('colors', 'tabs.bg.selected')
|
||||
@@ -406,9 +242,8 @@ class TabBar(QTabBar):
|
||||
fg_color = config.get('colors', 'tabs.fg.even')
|
||||
tab.palette.setColor(QPalette.Window, bg_color)
|
||||
tab.palette.setColor(QPalette.WindowText, fg_color)
|
||||
try:
|
||||
indicator_color = self.tab_data(idx, 'indicator-color')
|
||||
except KeyError:
|
||||
indicator_color = self.tabData(idx)
|
||||
if indicator_color is None:
|
||||
indicator_color = QColor()
|
||||
tab.palette.setColor(QPalette.Base, indicator_color)
|
||||
if tab.rect.right() < 0 or tab.rect.left() > self.width():
|
||||
@@ -418,80 +253,14 @@ class TabBar(QTabBar):
|
||||
p.drawControl(QStyle.CE_TabBarTab, tab)
|
||||
|
||||
def tabInserted(self, idx):
|
||||
"""Update visibility when a tab was inserted."""
|
||||
"""Show the tabbar if configured to hide and >1 tab is open."""
|
||||
self.autohide()
|
||||
super().tabInserted(idx)
|
||||
self._tabhide()
|
||||
|
||||
def tabRemoved(self, idx):
|
||||
"""Update visibility when a tab was removed."""
|
||||
"""Hide the tabbar if configured when only one tab is open."""
|
||||
self.autohide()
|
||||
super().tabRemoved(idx)
|
||||
self._tabhide()
|
||||
|
||||
def addTab(self, icon_or_text, text_or_empty=None):
|
||||
"""Override addTab to use our own text setting logic.
|
||||
|
||||
Unfortunately QTabBar::addTab has these two overloads:
|
||||
- const QIcon & icon, const QString & label
|
||||
- const QString & label
|
||||
|
||||
This means we'll get different arguments based on the chosen overload.
|
||||
|
||||
Args:
|
||||
icon_or_text: Either the QIcon to add or the label.
|
||||
text_or_empty: Either the label or None.
|
||||
|
||||
Return:
|
||||
The index of the newly added tab.
|
||||
"""
|
||||
if text_or_empty is None:
|
||||
icon = None
|
||||
text = icon_or_text
|
||||
new_idx = super().addTab('')
|
||||
else:
|
||||
icon = icon_or_text
|
||||
text = text_or_empty
|
||||
new_idx = super().addTab(icon, '')
|
||||
self.set_page_title(new_idx, text)
|
||||
|
||||
def insertTab(self, idx, icon_or_text, text_or_empty=None):
|
||||
"""Override insertTab to use our own text setting logic.
|
||||
|
||||
Unfortunately QTabBar::insertTab has these two overloads:
|
||||
- int index, const QIcon & icon, const QString & label
|
||||
- int index, const QString & label
|
||||
|
||||
This means we'll get different arguments based on the chosen overload.
|
||||
|
||||
Args:
|
||||
idx: Where to insert the widget.
|
||||
icon_or_text: Either the QIcon to add or the label.
|
||||
text_or_empty: Either the label or None.
|
||||
|
||||
Return:
|
||||
The index of the newly added tab.
|
||||
"""
|
||||
if text_or_empty is None:
|
||||
icon = None
|
||||
text = icon_or_text
|
||||
new_idx = super().InsertTab(idx, '')
|
||||
else:
|
||||
icon = icon_or_text
|
||||
text = text_or_empty
|
||||
new_idx = super().insertTab(idx, icon, '')
|
||||
self.set_page_title(new_idx, text)
|
||||
|
||||
def wheelEvent(self, e):
|
||||
"""Override wheelEvent to make the action configurable.
|
||||
|
||||
Args:
|
||||
e: The QWheelEvent
|
||||
"""
|
||||
if config.get('tabs', 'mousewheel-tab-switching'):
|
||||
super().wheelEvent(e)
|
||||
else:
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=self._win_id)
|
||||
tabbed_browser.wheelEvent(e)
|
||||
|
||||
|
||||
class TabBarStyle(QCommonStyle):
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Classes related to auto-updating and getting the latest version."""
|
||||
|
||||
import json
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl
|
||||
|
||||
from qutebrowser.misc import httpclient
|
||||
|
||||
|
||||
class PyPIVersionClient(QObject):
|
||||
|
||||
"""A client for the PyPI API using HTTPClient.
|
||||
|
||||
It gets the latest version of qutebrowser from PyPI.
|
||||
|
||||
Attributes:
|
||||
_client: The HTTPClient used.
|
||||
|
||||
Class attributes:
|
||||
API_URL: The base API URL.
|
||||
|
||||
Signals:
|
||||
success: Emitted when getting the version info succeeded.
|
||||
arg: The newest version.
|
||||
error: Emitted when getting the version info failed.
|
||||
arg: The error message, as string.
|
||||
"""
|
||||
|
||||
API_URL = 'https://pypi.python.org/pypi/{}/json'
|
||||
success = pyqtSignal(str)
|
||||
error = pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._client = httpclient.HTTPClient(self)
|
||||
self._client.error.connect(self.error)
|
||||
self._client.success.connect(self.on_client_success)
|
||||
|
||||
def get_version(self, package='qutebrowser'):
|
||||
"""Get the newest version of a given package.
|
||||
|
||||
Emits success/error when done.
|
||||
|
||||
Args:
|
||||
package: The name of the package to check.
|
||||
"""
|
||||
url = QUrl(self.API_URL.format(package))
|
||||
self._client.get(url)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def on_client_success(self, data):
|
||||
"""Process the data and finish when the client finished.
|
||||
|
||||
Args:
|
||||
data: A string with the received data.
|
||||
"""
|
||||
try:
|
||||
json_data = json.loads(data)
|
||||
except ValueError as e:
|
||||
self.error.emit("Invalid JSON received in reply: {}!".format(e))
|
||||
return
|
||||
try:
|
||||
self.success.emit(json_data['info']['version'])
|
||||
except KeyError as e:
|
||||
self.error.emit("Malformed data recieved in reply "
|
||||
"({!r} not found)!".format(e))
|
||||
return
|
||||
@@ -47,7 +47,7 @@ def check_python_version():
|
||||
version_str = '.'.join(map(str, sys.version_info[:3]))
|
||||
text = ("At least Python 3.4 is required to run qutebrowser, but " +
|
||||
version_str + " is installed!\n")
|
||||
if Tk and '--no-err-windows' not in sys.argv:
|
||||
if Tk:
|
||||
root = Tk()
|
||||
root.withdraw()
|
||||
messagebox.showerror("qutebrowser: Fatal error!", text)
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
"""Command history for the status bar."""
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject
|
||||
from PyQt5.QtCore import pyqtSlot
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import usertypes, log
|
||||
@@ -39,7 +39,7 @@ class HistoryEndReachedError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class History(QObject):
|
||||
class History:
|
||||
|
||||
"""Command history.
|
||||
|
||||
@@ -47,20 +47,14 @@ class History(QObject):
|
||||
handle_private_mode: Whether to ignore history in private mode.
|
||||
history: A list of executed commands, with newer commands at the end.
|
||||
_tmphist: Temporary history for history browsing (as NeighborList)
|
||||
|
||||
Signals:
|
||||
changed: Emitted when an entry was added to the history.
|
||||
"""
|
||||
|
||||
changed = pyqtSignal()
|
||||
|
||||
def __init__(self, history=None, parent=None):
|
||||
def __init__(self, history=None):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
history: The initial history to set.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.handle_private_mode = False
|
||||
self._tmphist = None
|
||||
if history is None:
|
||||
@@ -134,4 +128,3 @@ class History(QObject):
|
||||
return
|
||||
if not self.history or text != self.history[-1]:
|
||||
self.history.append(text)
|
||||
self.changed.emit()
|
||||
|
||||
@@ -21,14 +21,16 @@
|
||||
|
||||
import sys
|
||||
import code
|
||||
import rlcompleter
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt
|
||||
from PyQt5.QtWidgets import QTextEdit, QWidget, QVBoxLayout, QApplication
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QStringListModel
|
||||
from PyQt5.QtWidgets import (QTextEdit, QWidget, QVBoxLayout, QApplication,
|
||||
QCompleter)
|
||||
from PyQt5.QtGui import QTextCursor
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.misc import cmdhistory, miscwidgets
|
||||
from qutebrowser.utils import utils, objreg
|
||||
from qutebrowser.utils import utils, log, objreg
|
||||
|
||||
|
||||
class ConsoleLineEdit(miscwidgets.CommandLineEdit):
|
||||
@@ -37,6 +39,8 @@ class ConsoleLineEdit(miscwidgets.CommandLineEdit):
|
||||
|
||||
Attributes:
|
||||
_history: The command history of executed commands.
|
||||
_rlcompleter: The rlcompleter.Completer instance.
|
||||
_qcompleter: The QCompleter instance.
|
||||
|
||||
Signals:
|
||||
execute: Emitted when a commandline should be executed.
|
||||
@@ -44,18 +48,46 @@ class ConsoleLineEdit(miscwidgets.CommandLineEdit):
|
||||
|
||||
execute = pyqtSignal(str)
|
||||
|
||||
def __init__(self, _namespace, parent):
|
||||
def __init__(self, namespace, parent):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
_namespace: The local namespace of the interpreter.
|
||||
namespace: The local namespace of the interpreter.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.update_font()
|
||||
objreg.get('config').changed.connect(self.update_font)
|
||||
self._history = cmdhistory.History(parent=self)
|
||||
self.textChanged.connect(self.on_text_changed)
|
||||
|
||||
self._rlcompleter = rlcompleter.Completer(namespace)
|
||||
qcompleter = QCompleter(self)
|
||||
self._model = QStringListModel(qcompleter)
|
||||
qcompleter.setModel(self._model)
|
||||
qcompleter.setCompletionMode(
|
||||
QCompleter.UnfilteredPopupCompletion)
|
||||
qcompleter.setModelSorting(
|
||||
QCompleter.CaseSensitivelySortedModel)
|
||||
self.setCompleter(qcompleter)
|
||||
|
||||
self._history = cmdhistory.History()
|
||||
self.returnPressed.connect(self.on_return_pressed)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def on_text_changed(self, text):
|
||||
"""Update completion when text changed."""
|
||||
strings = set()
|
||||
i = 0
|
||||
while True:
|
||||
s = self._rlcompleter.complete(text, i)
|
||||
if s is None:
|
||||
break
|
||||
else:
|
||||
strings.add(s)
|
||||
i += 1
|
||||
strings = sorted(list(strings))
|
||||
self._model.setStringList(strings)
|
||||
log.misc.vdebug('completions: {!r}'.format(strings))
|
||||
|
||||
@pyqtSlot(str)
|
||||
def on_return_pressed(self):
|
||||
"""Execute the line of code which was entered."""
|
||||
@@ -149,7 +181,7 @@ class ConsoleWidget(QWidget):
|
||||
_output: The output widget in the console.
|
||||
_vbox: The layout which contains everything.
|
||||
_more: A flag which is set when more input is expected.
|
||||
_buffer: The buffer for multi-line commands.
|
||||
_buffer: The buffer for multiline commands.
|
||||
_interpreter: The InteractiveInterpreter to execute code with.
|
||||
"""
|
||||
|
||||
@@ -195,13 +227,13 @@ class ConsoleWidget(QWidget):
|
||||
self._buffer.append(line)
|
||||
source = '\n'.join(self._buffer)
|
||||
self.write(line + '\n')
|
||||
# We do two special things with the context managers here:
|
||||
# We do two special things with the contextmanagers here:
|
||||
# - We replace stdout/stderr to capture output. Even if we could
|
||||
# override InteractiveInterpreter's write method, most things are
|
||||
# printed elsewhere (e.g. by exec). Other Python GUI shells do the
|
||||
# same.
|
||||
# - We disable our exception hook, so exceptions from the console get
|
||||
# printed and don't open a crashdialog.
|
||||
# printed and don't ooen a crashdialog.
|
||||
with utils.fake_io(self.write), utils.disabled_excepthook():
|
||||
self._more = self._interpreter.runsource(source, '<console>')
|
||||
self.write(self._curprompt())
|
||||
|
||||
@@ -17,97 +17,48 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# pylint: disable=broad-except
|
||||
|
||||
"""The dialog which gets shown when qutebrowser crashes."""
|
||||
|
||||
import re
|
||||
import sys
|
||||
import html
|
||||
import getpass
|
||||
import traceback
|
||||
import functools
|
||||
|
||||
import pkg_resources
|
||||
from PyQt5.QtCore import pyqtSlot, Qt, QSize, qVersion
|
||||
from PyQt5.QtCore import pyqtSlot, Qt, QSize
|
||||
from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton,
|
||||
QVBoxLayout, QHBoxLayout, QCheckBox,
|
||||
QDialogButtonBox, QMessageBox)
|
||||
QVBoxLayout, QHBoxLayout, QCheckBox)
|
||||
|
||||
import qutebrowser
|
||||
from qutebrowser.utils import version, log, utils, objreg, qtutils
|
||||
from qutebrowser.misc import miscwidgets, autoupdate, msgbox
|
||||
from qutebrowser.utils import version, log, utils, objreg
|
||||
from qutebrowser.misc import miscwidgets
|
||||
from qutebrowser.browser.network import pastebin
|
||||
from qutebrowser.config import config
|
||||
|
||||
|
||||
def parse_fatal_stacktrace(text):
|
||||
"""Get useful information from a fatal faulthandler stacktrace.
|
||||
|
||||
Args:
|
||||
text: The text to parse.
|
||||
|
||||
Return:
|
||||
A tuple with the first element being the error type, and the second
|
||||
element being the first stacktrace frame.
|
||||
"""
|
||||
lines = [
|
||||
r'Fatal Python error: (.*)',
|
||||
r' *',
|
||||
r'(Current )?[Tt]hread [^ ]* \(most recent call first\): *',
|
||||
r' File ".*", line \d+ in (.*)',
|
||||
]
|
||||
m = re.match('\n'.join(lines), text)
|
||||
if m is None:
|
||||
# We got some invalid text.
|
||||
return ('', '')
|
||||
else:
|
||||
return (m.group(1), m.group(3))
|
||||
|
||||
|
||||
def get_fatal_crash_dialog(debug, data):
|
||||
"""Get a fatal crash dialog based on a crash log.
|
||||
|
||||
If the crash is a segfault in qt_mainloop and we're on an old Qt version
|
||||
this is a simple error dialog which lets the user know they should upgrade
|
||||
if possible.
|
||||
|
||||
If it's anything else, it's a normal FatalCrashDialog with the possibility
|
||||
to report the crash.
|
||||
|
||||
Args:
|
||||
debug: Whether the debug flag (--debug) was given.
|
||||
data: The crash log data.
|
||||
"""
|
||||
errtype, frame = parse_fatal_stacktrace(data)
|
||||
if (qtutils.version_check('5.4') or errtype != 'Segmentation fault' or
|
||||
frame != 'qt_mainloop'):
|
||||
return FatalCrashDialog(debug, data)
|
||||
else:
|
||||
title = "qutebrowser was restarted after a fatal crash!"
|
||||
text = ("<b>qutebrowser was restarted after a fatal crash!</b><br/>"
|
||||
"Unfortunately, this crash occurred in Qt (the library "
|
||||
"qutebrowser uses), and your version ({}) is outdated - "
|
||||
"Qt 5.4 or later is recommended. Unfortuntately Debian and "
|
||||
"Ubuntu don't ship a newer version (yet?)...".format(
|
||||
qVersion()))
|
||||
return QMessageBox(QMessageBox.Critical, title, text, QMessageBox.Ok)
|
||||
|
||||
|
||||
class _CrashDialog(QDialog):
|
||||
|
||||
"""Dialog which gets shown after there was a crash.
|
||||
|
||||
Class attributes:
|
||||
NAME: The kind of condition we report.
|
||||
|
||||
Attributes:
|
||||
These are just here to have a static reference to avoid GCing.
|
||||
_vbox: The main QVBoxLayout
|
||||
_lbl: The QLabel with the static text
|
||||
_debug_log: The QTextEdit with the crash information
|
||||
_btn_box: The QDialogButtonBox containing the buttons.
|
||||
_hbox: The QHboxLayout containing the buttons
|
||||
_url: Pastebin URL QLabel.
|
||||
_crash_info: A list of tuples with title and crash information.
|
||||
_paste_client: A PastebinClient instance to use.
|
||||
_pypi_client: A PyPIVersionClient instance to use.
|
||||
_paste_text: The text to pastebin.
|
||||
_resolution: Whether the dialog should be accepted on close.
|
||||
"""
|
||||
|
||||
NAME = None
|
||||
|
||||
def __init__(self, debug, parent=None):
|
||||
"""Constructor for CrashDialog.
|
||||
|
||||
@@ -117,36 +68,19 @@ class _CrashDialog(QDialog):
|
||||
super().__init__(parent)
|
||||
# We don't set WA_DeleteOnClose here as on an exception, we'll get
|
||||
# closed anyways, and it only could have unintended side-effects.
|
||||
self._buttons = []
|
||||
self._crash_info = []
|
||||
self._btn_box = None
|
||||
self._btn_report = None
|
||||
self._btn_cancel = None
|
||||
self._hbox = None
|
||||
self._lbl = None
|
||||
self._chk_report = None
|
||||
self._resolution = None
|
||||
self._paste_text = None
|
||||
self.setWindowTitle("Whoops!")
|
||||
self.resize(QSize(640, 600))
|
||||
self._vbox = QVBoxLayout(self)
|
||||
self._paste_client = pastebin.PastebinClient(self)
|
||||
self._pypi_client = autoupdate.PyPIVersionClient(self)
|
||||
self._init_text()
|
||||
|
||||
contact = QLabel("I'd like to be able to follow up with you, to keep "
|
||||
"you posted on the status of this crash and get more "
|
||||
"information if I need it - how can I contact you?",
|
||||
wordWrap=True)
|
||||
self._vbox.addWidget(contact)
|
||||
self._contact = QTextEdit(tabChangesFocus=True, acceptRichText=False)
|
||||
try:
|
||||
state = objreg.get('state-config')
|
||||
try:
|
||||
self._contact.setPlainText(state['general']['contact-info'])
|
||||
except KeyError:
|
||||
self._contact.setPlaceholderText("Mail or IRC nickname")
|
||||
except Exception:
|
||||
log.misc.exception("Failed to get contact information!")
|
||||
self._contact.setPlaceholderText("Mail or IRC nickname")
|
||||
self._vbox.addWidget(self._contact, 2)
|
||||
|
||||
info = QLabel("What were you doing when this crash/bug happened?")
|
||||
self._vbox.addWidget(info)
|
||||
self._info = QTextEdit(tabChangesFocus=True, acceptRichText=False)
|
||||
@@ -154,6 +88,11 @@ class _CrashDialog(QDialog):
|
||||
"- Switched tabs\n"
|
||||
"- etc...")
|
||||
self._vbox.addWidget(self._info, 5)
|
||||
contact = QLabel("How can I contact you if I need more info?")
|
||||
self._vbox.addWidget(contact)
|
||||
self._contact = QTextEdit(tabChangesFocus=True, acceptRichText=False)
|
||||
self._contact.setPlaceholderText("Github username, mail or IRC")
|
||||
self._vbox.addWidget(self._contact, 2)
|
||||
|
||||
self._vbox.addSpacing(15)
|
||||
self._debug_log = QTextEdit(tabChangesFocus=True, acceptRichText=False,
|
||||
@@ -172,8 +111,7 @@ class _CrashDialog(QDialog):
|
||||
self._vbox.addWidget(self._debug_log, 10)
|
||||
self._vbox.addSpacing(15)
|
||||
|
||||
self._init_checkboxes()
|
||||
self._init_info_text()
|
||||
self._init_checkboxes(debug)
|
||||
self._init_buttons()
|
||||
|
||||
def __repr__(self):
|
||||
@@ -182,34 +120,33 @@ class _CrashDialog(QDialog):
|
||||
def _init_text(self):
|
||||
"""Initialize the main text to be displayed on an exception.
|
||||
|
||||
Should be extended by subclasses to set the actual text."""
|
||||
Should be extended by superclass to set the actual text."""
|
||||
self._lbl = QLabel(wordWrap=True, openExternalLinks=True,
|
||||
textInteractionFlags=Qt.LinksAccessibleByMouse)
|
||||
self._vbox.addWidget(self._lbl)
|
||||
|
||||
def _init_checkboxes(self):
|
||||
"""Initialize the checkboxes."""
|
||||
pass
|
||||
def _init_checkboxes(self, debug):
|
||||
"""Initialize the checkboxes.
|
||||
|
||||
Args:
|
||||
debug: Whether a --debug arg was given.
|
||||
"""
|
||||
self._chk_report = QCheckBox("Send a report")
|
||||
if not debug:
|
||||
self._chk_report.setChecked(True)
|
||||
self._vbox.addWidget(self._chk_report)
|
||||
info_label = QLabel("<i>Note that without your help, I can't fix the "
|
||||
"bug you encountered.</i>", wordWrap=True)
|
||||
self._vbox.addWidget(info_label)
|
||||
|
||||
def _init_buttons(self):
|
||||
"""Initialize the buttons."""
|
||||
self._btn_box = QDialogButtonBox()
|
||||
self._vbox.addWidget(self._btn_box)
|
||||
"""Initialize the buttons.
|
||||
|
||||
self._btn_report = QPushButton("Report", default=True)
|
||||
self._btn_report.clicked.connect(self.on_report_clicked)
|
||||
self._btn_box.addButton(self._btn_report, QDialogButtonBox.AcceptRole)
|
||||
|
||||
self._btn_cancel = QPushButton("Don't report", autoDefault=False)
|
||||
self._btn_cancel.clicked.connect(self.finish)
|
||||
self._btn_box.addButton(self._btn_cancel, QDialogButtonBox.RejectRole)
|
||||
|
||||
def _init_info_text(self):
|
||||
"""Add an info text encouraging the user to report crashes."""
|
||||
info_label = QLabel("<br/><b>Note that without your help, I can't fix "
|
||||
"the bug you encountered.<br/>I read and respond "
|
||||
"to all crash reports!</b>", wordWrap=True)
|
||||
self._vbox.addWidget(info_label)
|
||||
Should be extended by subclasses to provide the actual buttons.
|
||||
"""
|
||||
self._hbox = QHBoxLayout()
|
||||
self._vbox.addLayout(self._hbox)
|
||||
self._hbox.addStretch()
|
||||
|
||||
def _gather_crash_info(self):
|
||||
"""Gather crash information to display.
|
||||
@@ -241,31 +178,6 @@ class _CrashDialog(QDialog):
|
||||
text = '\n\n'.join(chunks)
|
||||
self._debug_log.setText(text)
|
||||
|
||||
def _get_error_type(self):
|
||||
"""Get the type of the error we're reporting."""
|
||||
raise NotImplementedError
|
||||
|
||||
def _get_paste_title_desc(self):
|
||||
"""Get a short description of the paste."""
|
||||
return ''
|
||||
|
||||
def _get_paste_title(self):
|
||||
"""Get a title for the paste."""
|
||||
desc = self._get_paste_title_desc()
|
||||
title = "qute {} {}".format(
|
||||
qutebrowser.__version__, self._get_error_type())
|
||||
if desc:
|
||||
title += ' {}'.format(desc)
|
||||
return title
|
||||
|
||||
def _save_contact_info(self):
|
||||
"""Save the contact info to disk."""
|
||||
try:
|
||||
state = objreg.get('state-config')
|
||||
state['general']['contact-info'] = self._contact.toPlainText()
|
||||
except Exception:
|
||||
log.misc.exception("Failed to save contact information!")
|
||||
|
||||
def report(self):
|
||||
"""Paste the crash info into the pastebin."""
|
||||
lines = []
|
||||
@@ -283,7 +195,7 @@ class _CrashDialog(QDialog):
|
||||
user = 'unknown'
|
||||
try:
|
||||
# parent: http://p.cmpl.cc/90286958
|
||||
self._paste_client.paste(user, self._get_paste_title(),
|
||||
self._paste_client.paste(user, "qutebrowser {}".format(self.NAME),
|
||||
self._paste_text, parent='90286958')
|
||||
except Exception as e:
|
||||
log.misc.exception("Error while paste-binning")
|
||||
@@ -291,21 +203,17 @@ class _CrashDialog(QDialog):
|
||||
self.show_error(exc_text)
|
||||
|
||||
@pyqtSlot()
|
||||
def on_report_clicked(self):
|
||||
"""Report and close dialog if report button was clicked."""
|
||||
self._btn_report.setEnabled(False)
|
||||
self._btn_cancel.setEnabled(False)
|
||||
self._btn_report.setText("Reporting...")
|
||||
self._paste_client.success.connect(self.on_paste_success)
|
||||
def on_button_clicked(self, button, accept):
|
||||
"""Report and close dialog if button was clicked."""
|
||||
button.setText("Reporting...")
|
||||
for btn in self._buttons:
|
||||
btn.setEnabled(False)
|
||||
self._resolution = accept
|
||||
self._paste_client.success.connect(self.finish)
|
||||
self._paste_client.error.connect(self.show_error)
|
||||
self.report()
|
||||
|
||||
@pyqtSlot()
|
||||
def on_paste_success(self):
|
||||
"""Get the newest version from PyPI when the paste is done."""
|
||||
self._pypi_client.success.connect(self.on_version_success)
|
||||
self._pypi_client.error.connect(self.on_version_error)
|
||||
self._pypi_client.get_version()
|
||||
reported = self.maybe_report()
|
||||
if not reported:
|
||||
self.finish()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def show_error(self, text):
|
||||
@@ -318,49 +226,26 @@ class _CrashDialog(QDialog):
|
||||
error_dlg.finished.connect(self.finish)
|
||||
error_dlg.show()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def on_version_success(self, newest):
|
||||
"""Called when the version was obtained from self._pypi_client.
|
||||
|
||||
Args:
|
||||
newest: The newest version as a string.
|
||||
"""
|
||||
# pylint: disable=no-member
|
||||
# https://bitbucket.org/logilab/pylint/issue/73/
|
||||
new_version = pkg_resources.parse_version(newest)
|
||||
cur_version = pkg_resources.parse_version(qutebrowser.__version__)
|
||||
lines = ['The report has been sent successfully. Thanks!']
|
||||
if new_version > cur_version:
|
||||
lines.append("<b>Note:</b> The newest available version is v{}, "
|
||||
"but you're currently running v{} - please "
|
||||
"update!".format(newest, qutebrowser.__version__))
|
||||
text = '<br/><br/>'.join(lines)
|
||||
self.hide()
|
||||
msgbox.information(self, "Report successfully sent!", text,
|
||||
on_finished=self.finish, plain_text=False)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def on_version_error(self, msg):
|
||||
"""Called when the version was not obtained from self._pypi_client.
|
||||
|
||||
Args:
|
||||
msg: The error message to show.
|
||||
"""
|
||||
lines = ['The report has been sent successfully. Thanks!']
|
||||
lines.append("There was an error while getting the newest version: "
|
||||
"{}. Please check for a new version on "
|
||||
"<a href=http://www.qutebrowser.org/>qutebrowser.org</a> "
|
||||
"by yourself.".format(msg))
|
||||
text = '<br/><br/>'.join(lines)
|
||||
self.hide()
|
||||
msgbox.information(self, "Report successfully sent!", text,
|
||||
on_finished=self.finish, plain_text=False)
|
||||
|
||||
@pyqtSlot()
|
||||
def finish(self):
|
||||
"""Save contact info and close the dialog."""
|
||||
self._save_contact_info()
|
||||
self.accept()
|
||||
"""Accept/reject the dialog when reporting is done."""
|
||||
if self._resolution:
|
||||
self.accept()
|
||||
else:
|
||||
self.reject()
|
||||
|
||||
@pyqtSlot()
|
||||
def maybe_report(self):
|
||||
"""Report the bug if the user allowed us to.
|
||||
|
||||
Return:
|
||||
True if a report was done, False otherwise.
|
||||
"""
|
||||
if self._chk_report.isChecked():
|
||||
self.report()
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
class ExceptionCrashDialog(_CrashDialog):
|
||||
@@ -368,15 +253,17 @@ class ExceptionCrashDialog(_CrashDialog):
|
||||
"""Dialog which gets shown on an exception.
|
||||
|
||||
Attributes:
|
||||
_buttons: A list of buttons.
|
||||
_pages: A list of lists of the open pages (URLs as strings)
|
||||
_cmdhist: A list with the command history (as strings)
|
||||
_exc: An exception tuple (type, value, traceback)
|
||||
_objects: A list of all QObjects as string.
|
||||
"""
|
||||
|
||||
NAME = 'exception'
|
||||
|
||||
def __init__(self, debug, pages, cmdhist, exc, objects, parent=None):
|
||||
self._chk_log = None
|
||||
self._chk_restore = None
|
||||
super().__init__(debug, parent)
|
||||
self._pages = pages
|
||||
self._cmdhist = cmdhist
|
||||
@@ -392,15 +279,26 @@ class ExceptionCrashDialog(_CrashDialog):
|
||||
|
||||
def _init_buttons(self):
|
||||
super()._init_buttons()
|
||||
btn_quit = QPushButton("Quit")
|
||||
btn_quit.clicked.connect(
|
||||
functools.partial(self.on_button_clicked, btn_quit, False))
|
||||
self._hbox.addWidget(btn_quit)
|
||||
|
||||
def _init_checkboxes(self):
|
||||
"""Add checkboxes to the dialog."""
|
||||
super()._init_checkboxes()
|
||||
self._chk_restore = QCheckBox("Restore open pages")
|
||||
self._chk_restore.setChecked(True)
|
||||
self._vbox.addWidget(self._chk_restore)
|
||||
self._chk_log = QCheckBox("Include a debug log in the report",
|
||||
checked=True)
|
||||
btn_restart = QPushButton("Restart", default=True)
|
||||
btn_restart.clicked.connect(
|
||||
functools.partial(self.on_button_clicked, btn_restart, True))
|
||||
self._hbox.addWidget(btn_restart)
|
||||
|
||||
self._buttons = [btn_quit, btn_restart]
|
||||
|
||||
def _init_checkboxes(self, debug):
|
||||
"""Add checkboxes to send crash report."""
|
||||
super()._init_checkboxes(debug)
|
||||
self._chk_log = QCheckBox("Include a debug log and a list of open "
|
||||
"pages", checked=True)
|
||||
if debug:
|
||||
self._chk_log.setChecked(False)
|
||||
self._chk_log.setEnabled(False)
|
||||
try:
|
||||
if config.get('general', 'private-browsing'):
|
||||
self._chk_log.setChecked(False)
|
||||
@@ -413,19 +311,12 @@ class ExceptionCrashDialog(_CrashDialog):
|
||||
"information such as which pages you visited "
|
||||
"or keyboard input.</i>", wordWrap=True)
|
||||
self._vbox.addWidget(info_label)
|
||||
|
||||
def _get_error_type(self):
|
||||
return 'exc'
|
||||
|
||||
def _get_paste_title_desc(self):
|
||||
desc = traceback.format_exception_only(self._exc[0], self._exc[1])
|
||||
return desc[0].rstrip()
|
||||
self._chk_report.toggled.connect(self.on_chk_report_toggled)
|
||||
|
||||
def _gather_crash_info(self):
|
||||
self._crash_info += [
|
||||
("Exception", ''.join(traceback.format_exception(*self._exc))),
|
||||
]
|
||||
super()._gather_crash_info()
|
||||
if self._chk_log.isChecked():
|
||||
self._crash_info += [
|
||||
("Commandline args", ' '.join(sys.argv[1:])),
|
||||
@@ -433,6 +324,8 @@ class ExceptionCrashDialog(_CrashDialog):
|
||||
("Command history", '\n'.join(self._cmdhist)),
|
||||
("Objects", self._objects),
|
||||
]
|
||||
super()._gather_crash_info()
|
||||
if self._chk_log.isChecked():
|
||||
try:
|
||||
self._crash_info.append(
|
||||
("Debug log", log.ram_handler.dump_log()))
|
||||
@@ -441,42 +334,28 @@ class ExceptionCrashDialog(_CrashDialog):
|
||||
("Debug log", traceback.format_exc()))
|
||||
|
||||
@pyqtSlot()
|
||||
def finish(self):
|
||||
self._save_contact_info()
|
||||
if self._chk_restore.isChecked():
|
||||
self.accept()
|
||||
else:
|
||||
self.reject()
|
||||
def on_chk_report_toggled(self):
|
||||
"""Disable log checkbox if report is disabled."""
|
||||
is_checked = self._chk_report.isChecked()
|
||||
self._chk_log.setEnabled(is_checked)
|
||||
self._chk_log.setChecked(is_checked)
|
||||
|
||||
|
||||
class FatalCrashDialog(_CrashDialog):
|
||||
|
||||
"""Dialog which gets shown when a fatal error occurred.
|
||||
"""Dialog which gets shown when a fatal error occured.
|
||||
|
||||
Attributes:
|
||||
_log: The log text to display.
|
||||
_type: The type of error which occurred.
|
||||
_func: The function (top of the stack) in which the error occurred.
|
||||
_chk_history: A checkbox for the user to decide if page history should
|
||||
be sent.
|
||||
"""
|
||||
|
||||
NAME = 'segfault'
|
||||
|
||||
def __init__(self, debug, text, parent=None):
|
||||
self._chk_history = None
|
||||
super().__init__(debug, parent)
|
||||
self._log = text
|
||||
self.setAttribute(Qt.WA_DeleteOnClose)
|
||||
self._set_crash_info()
|
||||
self._type, self._func = parse_fatal_stacktrace(self._log)
|
||||
|
||||
def _get_error_type(self):
|
||||
if self._type == 'Segmentation fault':
|
||||
return 'segv'
|
||||
else:
|
||||
return self._type
|
||||
|
||||
def _get_paste_title_desc(self):
|
||||
return self._func
|
||||
|
||||
def _init_text(self):
|
||||
super()._init_text()
|
||||
@@ -488,29 +367,19 @@ class FatalCrashDialog(_CrashDialog):
|
||||
"stacktrace.asciidoc</a> to submit a stacktrace.<br/>")
|
||||
self._lbl.setText(text)
|
||||
|
||||
def _init_checkboxes(self):
|
||||
"""Add checkboxes to the dialog."""
|
||||
super()._init_checkboxes()
|
||||
self._chk_history = QCheckBox("Include a history of the last "
|
||||
"accessed pages in the report.",
|
||||
checked=True)
|
||||
try:
|
||||
if config.get('general', 'private-browsing'):
|
||||
self._chk_history.setChecked(False)
|
||||
except Exception:
|
||||
log.misc.exception("Error while checking private browsing mode")
|
||||
self._chk_history.toggled.connect(self._set_crash_info)
|
||||
self._vbox.addWidget(self._chk_history)
|
||||
def _init_buttons(self):
|
||||
super()._init_buttons()
|
||||
btn_ok = QPushButton(text="OK", default=True)
|
||||
btn_ok.clicked.connect(
|
||||
functools.partial(self.on_button_clicked, btn_ok, True))
|
||||
self._hbox.addWidget(btn_ok)
|
||||
self._buttons = [btn_ok]
|
||||
|
||||
def _gather_crash_info(self):
|
||||
self._crash_info.append(("Fault log", self._log))
|
||||
self._crash_info += [
|
||||
("Fault log", self._log),
|
||||
]
|
||||
super()._gather_crash_info()
|
||||
if self._chk_history.isChecked():
|
||||
try:
|
||||
history = objreg.get('web-history').get_recent()
|
||||
self._crash_info.append(("History", ''.join(history)))
|
||||
except Exception:
|
||||
self._crash_info.append(("History", traceback.format_exc()))
|
||||
|
||||
|
||||
class ReportDialog(_CrashDialog):
|
||||
@@ -523,9 +392,12 @@ class ReportDialog(_CrashDialog):
|
||||
_objects: A list of all QObjects as string.
|
||||
"""
|
||||
|
||||
NAME = 'report'
|
||||
|
||||
def __init__(self, pages, cmdhist, objects, parent=None):
|
||||
super().__init__(False, parent)
|
||||
self.setAttribute(Qt.WA_DeleteOnClose)
|
||||
self._btn_report = None
|
||||
self._pages = pages
|
||||
self._cmdhist = cmdhist
|
||||
self._objects = objects
|
||||
@@ -536,12 +408,17 @@ class ReportDialog(_CrashDialog):
|
||||
text = "Please describe the bug you encountered below."
|
||||
self._lbl.setText(text)
|
||||
|
||||
def _init_info_text(self):
|
||||
"""We don't want an info text as the user wanted to report."""
|
||||
pass
|
||||
def _init_buttons(self):
|
||||
super()._init_buttons()
|
||||
self._btn_report = QPushButton("Report", default=True)
|
||||
self._btn_report.clicked.connect(
|
||||
functools.partial(self.on_button_clicked, self._btn_report, True))
|
||||
self._hbox.addWidget(self._btn_report)
|
||||
self._buttons = [self._btn_report]
|
||||
|
||||
def _get_error_type(self):
|
||||
return 'report'
|
||||
def _init_checkboxes(self, _debug):
|
||||
"""We don't want any checkboxes as the user wanted to report."""
|
||||
pass
|
||||
|
||||
def _gather_crash_info(self):
|
||||
super()._gather_crash_info()
|
||||
@@ -556,6 +433,16 @@ class ReportDialog(_CrashDialog):
|
||||
except Exception:
|
||||
self._crash_info.append(("Debug log", traceback.format_exc()))
|
||||
|
||||
@pyqtSlot()
|
||||
def maybe_report(self):
|
||||
"""Report the crash.
|
||||
|
||||
We don't have a "Send a report" checkbox here because it was a manual
|
||||
report, which would be pretty useless without this info.
|
||||
"""
|
||||
self.report()
|
||||
return True
|
||||
|
||||
|
||||
class ReportErrorDialog(QDialog):
|
||||
|
||||
@@ -583,38 +470,3 @@ class ReportErrorDialog(QDialog):
|
||||
btn.clicked.connect(self.close)
|
||||
hbox.addWidget(btn)
|
||||
vbox.addLayout(hbox)
|
||||
|
||||
|
||||
def dump_exception_info(exc, pages, cmdhist, objects):
|
||||
"""Dump exception info to stderr.
|
||||
|
||||
Args:
|
||||
exc: An exception tuple (type, value, traceback)
|
||||
pages: A list of lists of the open pages (URLs as strings)
|
||||
cmdhist: A list with the command history (as strings)
|
||||
objects: A list of all QObjects as string.
|
||||
"""
|
||||
print(file=sys.stderr)
|
||||
print("\n\n===== Handling exception with --no-err-windows... =====\n\n",
|
||||
file=sys.stderr)
|
||||
print("\n---- Exceptions ----", file=sys.stderr)
|
||||
print(''.join(traceback.format_exception(*exc)), file=sys.stderr)
|
||||
print("\n---- Version info ----", file=sys.stderr)
|
||||
try:
|
||||
print(version.version(), file=sys.stderr)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
print("\n---- Config ----", file=sys.stderr)
|
||||
try:
|
||||
conf = objreg.get('config')
|
||||
print(conf.dump_userconfig(), file=sys.stderr)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
print("\n---- Commandline args ----", file=sys.stderr)
|
||||
print(' '.join(sys.argv[1:]), file=sys.stderr)
|
||||
print("\n---- Open pages ----", file=sys.stderr)
|
||||
print('\n\n'.join('\n'.join(e) for e in pages), file=sys.stderr)
|
||||
print("\n---- Command history ----", file=sys.stderr)
|
||||
print('\n'.join(cmdhist), file=sys.stderr)
|
||||
print("\n---- Objects ----", file=sys.stderr)
|
||||
print(objects, file=sys.stderr)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user