Merge branch 'new-config'

This commit is contained in:
Florian Bruhin 2017-09-16 16:12:21 +02:00
commit 624c3a4c27
225 changed files with 13744 additions and 14630 deletions

View File

@ -43,6 +43,8 @@ putty-ignore =
tests/helpers/fixtures.py : +N806 tests/helpers/fixtures.py : +N806
tests/unit/browser/webkit/http/test_content_disposition.py : +D400 tests/unit/browser/webkit/http/test_content_disposition.py : +D400
scripts/dev/ci/appveyor_install.py : +FI53 scripts/dev/ci/appveyor_install.py : +FI53
# FIXME:conf
tests/unit/completion/test_models.py : +F821
copyright-check = True copyright-check = True
copyright-regexp = # Copyright [\d-]+ .* copyright-regexp = # Copyright [\d-]+ .*
copyright-min-file-size = 110 copyright-min-file-size = 110

View File

@ -30,6 +30,7 @@ disable=no-self-use,
broad-except, broad-except,
bare-except, bare-except,
eval-used, eval-used,
exec-used,
ungrouped-imports, ungrouped-imports,
suppressed-message, suppressed-message,
too-many-return-statements, too-many-return-statements,

View File

@ -193,7 +193,7 @@ Configuration not saved after modifying config.::
Unable to view flash content.:: Unable to view flash content.::
If you have flash installed for on your system, it's necessary to enable plugins If you have flash installed for on your system, it's necessary to enable plugins
to use the flash plugin. Using the command `:set content allow-plugins true` to use the flash plugin. Using the command `:set content.plugins true`
in qutebrowser will enable plugins. Packages for flash should in qutebrowser will enable plugins. Packages for flash should
be provided for your platform or it can be obtained from be provided for your platform or it can be obtained from
http://get.adobe.com/flashplayer/[Adobe]. http://get.adobe.com/flashplayer/[Adobe].

View File

@ -17,6 +17,7 @@ include requirements.txt
include tox.ini include tox.ini
include qutebrowser.py include qutebrowser.py
include misc/cheatsheet.svg include misc/cheatsheet.svg
include qutebrowser/config/configdata.yml
prune www prune www
prune scripts/dev prune scripts/dev

View File

@ -50,6 +50,7 @@ image:https://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding
* link:doc/quickstart.asciidoc[Quick start guide] * link:doc/quickstart.asciidoc[Quick start guide]
* A https://www.shortcutfoo.com/app/dojos/qutebrowser[free training course] to remember those key bindings. * A https://www.shortcutfoo.com/app/dojos/qutebrowser[free training course] to remember those key bindings.
* link:FAQ.asciidoc[Frequently asked questions] * link:FAQ.asciidoc[Frequently asked questions]
* link:doc/help/configuring.html[Configuring qutebrowser]
* link:CONTRIBUTING.asciidoc[Contributing to qutebrowser] * link:CONTRIBUTING.asciidoc[Contributing to qutebrowser]
* link:INSTALL.asciidoc[INSTALL] * link:INSTALL.asciidoc[INSTALL]
* link:CHANGELOG.asciidoc[Change Log] * link:CHANGELOG.asciidoc[Change Log]

View File

@ -88,7 +88,6 @@ It is possible to run or bind multiple commands by separating them with `;;`.
|<<undo,undo>>|Re-open a closed tab. |<<undo,undo>>|Re-open a closed tab.
|<<view-source,view-source>>|Show the source of the current page in a new tab. |<<view-source,view-source>>|Show the source of the current page in a new tab.
|<<window-only,window-only>>|Close all windows except for the current one. |<<window-only,window-only>>|Close all windows except for the current one.
|<<wq,wq>>|Save open pages and quit.
|<<yank,yank>>|Yank something to the clipboard or primary selection. |<<yank,yank>>|Yank something to the clipboard or primary selection.
|<<zoom,zoom>>|Set the zoom level for the current tab. |<<zoom,zoom>>|Set the zoom level for the current tab.
|<<zoom-in,zoom-in>>|Increase the zoom level for the current tab. |<<zoom-in,zoom-in>>|Increase the zoom level for the current tab.
@ -126,7 +125,8 @@ Bind a key to a command.
==== optional arguments ==== optional arguments
* +*-m*+, +*--mode*+: A comma-separated list of modes to bind the key in (default: `normal`). * +*-m*+, +*--mode*+: A comma-separated list of modes to bind the key in (default: `normal`). See `:help bindings.commands` for the
available modes.
* +*-f*+, +*--force*+: Rebind the key if it is already bound. * +*-f*+, +*--force*+: Rebind the key if it is already bound.
@ -281,7 +281,7 @@ Syntax: +:edit-url [*--bg*] [*--tab*] [*--window*] ['url']+
Navigate to a url formed in an external editor. Navigate to a url formed in an external editor.
The editor which should be launched can be configured via the `general -> editor` config option. The editor which should be launched can be configured via the `editor.command` config option.
==== positional arguments ==== positional arguments
* +'url'+: URL to edit; defaults to the current page url. * +'url'+: URL to edit; defaults to the current page url.
@ -338,7 +338,7 @@ Show help about a command or setting.
* +'topic'+: The topic to show help for. * +'topic'+: The topic to show help for.
- :__command__ for commands. - :__command__ for commands.
- __section__\->__option__ for settings. - __section__.__option__ for settings.
==== optional arguments ==== optional arguments
@ -368,7 +368,7 @@ Start hinting.
- `normal`: Open the link. - `normal`: Open the link.
- `current`: Open the link in the current tab. - `current`: Open the link in the current tab.
- `tab`: Open the link in a new tab (honoring the - `tab`: Open the link in a new tab (honoring the
background-tabs setting). `tabs.background_tabs` setting).
- `tab-fg`: Open the link in a new foreground tab. - `tab-fg`: Open the link in a new foreground tab.
- `tab-bg`: Open the link in a new background tab. - `tab-bg`: Open the link in a new background tab.
- `window`: Open the link in a new window. - `window`: Open the link in a new window.
@ -402,13 +402,13 @@ Start hinting.
==== optional arguments ==== optional arguments
* +*-r*+, +*--rapid*+: Whether to do rapid hinting. This is only possible with targets `tab` (with background-tabs=true), `tab-bg`, * +*-r*+, +*--rapid*+: Whether to do rapid hinting. This is only possible with targets `tab` (with `tabs.background_tabs=true`), `tab-bg`,
`window`, `run`, `hover`, `userscript` and `spawn`. `window`, `run`, `hover`, `userscript` and `spawn`.
* +*-m*+, +*--mode*+: The hinting mode to use. * +*-m*+, +*--mode*+: The hinting mode to use.
- `number`: Use numeric hints. - `number`: Use numeric hints.
- `letter`: Use the chars in the hints->chars settings. - `letter`: Use the chars in the hints.chars setting.
- `word`: Use hint words based on the html elements and the - `word`: Use hint words based on the html elements and the
extra words. extra words.
@ -545,7 +545,7 @@ For `increment` and `decrement`, the number to change the URL by. For `up`, the
[[open]] [[open]]
=== open === open
Syntax: +:open [*--implicit*] [*--bg*] [*--tab*] [*--window*] [*--secure*] [*--private*] Syntax: +:open [*--related*] [*--bg*] [*--tab*] [*--window*] [*--secure*] [*--private*]
['url']+ ['url']+
Open a URL in the current/[count]th tab. Open a URL in the current/[count]th tab.
@ -556,7 +556,7 @@ If the URL contains newlines, each line gets opened in its own tab.
* +'url'+: The URL to open. * +'url'+: The URL to open.
==== optional arguments ==== optional arguments
* +*-i*+, +*--implicit*+: If opening a new tab, treat the tab as implicit (like clicking on a link). * +*-r*+, +*--related*+: If opening a new tab, position the tab as related to the current one (like clicking on a link).
* +*-b*+, +*--bg*+: Open in a new background tab. * +*-b*+, +*--bg*+: Open in a new background tab.
* +*-t*+, +*--tab*+: Open in a new tab. * +*-t*+, +*--tab*+: Open in a new tab.
@ -632,8 +632,17 @@ Save the current page as a quickmark.
[[quit]] [[quit]]
=== quit === quit
Syntax: +:quit [*--save*] ['session']+
Quit qutebrowser. Quit qutebrowser.
==== positional arguments
* +'session'+: The name of the session to save.
==== optional arguments
* +*-s*+, +*--save*+: When given, save the open windows even if auto_save.session is turned off.
[[record-macro]] [[record-macro]]
=== record-macro === record-macro
Syntax: +:record-macro ['register']+ Syntax: +:record-macro ['register']+
@ -752,7 +761,7 @@ Syntax: +:session-save [*--current*] [*--quiet*] [*--force*] [*--only-active-win
Save a session. Save a session.
==== positional arguments ==== positional arguments
* +'name'+: The name of the session. If not given, the session configured in general -> session-default-name is saved. * +'name'+: The name of the session. If not given, the session configured in session_default_name is saved.
==== optional arguments ==== optional arguments
@ -764,19 +773,18 @@ Save a session.
[[set]] [[set]]
=== set === set
Syntax: +:set [*--temp*] [*--print*] ['section'] ['option'] ['values' ['values' ...]]+ Syntax: +:set [*--temp*] [*--print*] ['option'] ['values' ['values' ...]]+
Set an option. 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. If the option name ends with '!' and it is a boolean value, toggle it.
==== positional arguments ==== positional arguments
* +'section'+: The section where the option is in.
* +'option'+: The name of the option. * +'option'+: The name of the option.
* +'values'+: The value to set, or the values to cycle through. * +'values'+: The value to set, or the values to cycle through.
==== optional arguments ==== optional arguments
* +*-t*+, +*--temp*+: Set value temporarily. * +*-t*+, +*--temp*+: Set value temporarily until qutebrowser is closed.
* +*-p*+, +*--print*+: Print the value after setting. * +*-p*+, +*--print*+: Print the value after setting.
[[set-cmd-text]] [[set-cmd-text]]
@ -843,7 +851,7 @@ Close the current/[count]th tab.
==== optional arguments ==== optional arguments
* +*-p*+, +*--prev*+: Force selecting the tab before the current tab. * +*-p*+, +*--prev*+: Force selecting the tab before the current tab.
* +*-n*+, +*--next*+: Force selecting the tab after the current tab. * +*-n*+, +*--next*+: Force selecting the tab after 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 opposite direction of what's configured in 'tabs.select_on_remove'.
* +*-f*+, +*--force*+: Avoid confirmation for pinned tabs. * +*-f*+, +*--force*+: Avoid confirmation for pinned tabs.
@ -911,7 +919,7 @@ Close all tabs except for the current one.
=== tab-pin === tab-pin
Pin/Unpin the current/[count]th tab. Pin/Unpin the current/[count]th tab.
Pinning a tab shrinks it to tabs->pinned-width size. Attempting to close a pinned tab will cause a confirmation, unless --force is passed. Pinning a tab shrinks it to `tabs.width.pinned` size. Attempting to close a pinned tab will cause a confirmation, unless --force is passed.
==== count ==== count
The tab index to pin or unpin The tab index to pin or unpin
@ -925,13 +933,15 @@ How many tabs to switch back.
[[unbind]] [[unbind]]
=== unbind === unbind
Syntax: +:unbind 'key' ['mode']+ Syntax: +:unbind [*--mode* 'mode'] 'key'+
Unbind a keychain. Unbind a keychain.
==== positional arguments ==== positional arguments
* +'key'+: The keychain or special key (inside <...>) to unbind. * +'key'+: The keychain or special key (inside <...>) to unbind.
* +'mode'+: A comma-separated list of modes to unbind the key in (default: `normal`).
==== optional arguments
* +*-m*+, +*--mode*+: A mode to unbind the key in (default: `normal`). See `:help bindings.commands` for the available modes.
[[undo]] [[undo]]
@ -946,15 +956,6 @@ Show the source of the current page in a new tab.
=== window-only === window-only
Close all windows except for the current one. Close all windows except for the current one.
[[wq]]
=== wq
Syntax: +:wq ['name']+
Save open pages and quit.
==== positional arguments
* +'name'+: The name of the session.
[[yank]] [[yank]]
=== yank === yank
Syntax: +:yank [*--sel*] [*--keep*] ['what']+ Syntax: +:yank [*--sel*] [*--keep*] ['what']+
@ -1043,6 +1044,7 @@ How many steps to zoom out.
|<<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-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-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. |<<move-to-start-of-prev-block,move-to-start-of-prev-block>>|Move the cursor or selection to the start of previous block.
|<<nop,nop>>|Do nothing.
|<<open-editor,open-editor>>|Open an external editor with the currently selected form field. |<<open-editor,open-editor>>|Open an external editor with the currently selected form field.
|<<prompt-accept,prompt-accept>>|Accept the current prompt. |<<prompt-accept,prompt-accept>>|Accept the current prompt.
|<<prompt-item-focus,prompt-item-focus>>|Shift the focus of the prompt file completion menu to another item. |<<prompt-item-focus,prompt-item-focus>>|Shift the focus of the prompt file completion menu to another item.
@ -1290,11 +1292,15 @@ Move the cursor or selection to the start of previous block.
==== count ==== count
How many blocks to move. How many blocks to move.
[[nop]]
=== nop
Do nothing.
[[open-editor]] [[open-editor]]
=== open-editor === open-editor
Open an external editor with the currently selected form field. Open an external editor with the currently selected form field.
The editor which should be launched can be configured via the `general -> editor` config option. The editor which should be launched can be configured via the `editor.command` config option.
[[prompt-accept]] [[prompt-accept]]
=== prompt-accept === prompt-accept

View File

@ -0,0 +1,180 @@
Configuring qutebrowser
=======================
IMPORTANT: qutebrowser's configuration system was completely rewritten in
September 2017. This information is not applicable to older releases, and older
information elsewhere might be outdated. **If you had an old configuration
around and upgraded, this page will automatically open once**. To view it at a
later time, use the `:help` command.
Migrating older configurations
------------------------------
qutebrowser does no automatic migration for the new configuration. However,
there's a special link:qute://configdiff[config diff page] which will show you
the changes you did in your old configuration, compared to the old defaults.
Configuring qutebrowser via the user interface
----------------------------------------------
The easy (but less flexible) way to configure qutebrowser is using its user
interface or command line. Changes you make this way are immediately active
(with the exception of a few settings, where this is pointed out in the
documentation) and are persisted in an `autoconfig.yml` file.
Using the link:commands.html#set[`:set`] command and command completion, you
can quickly set settings interactively, for example `:set tabs.position left`.
To get more help about a setting, use e.g. `:help tabs.position`.
If you want to customize many settings, you can open the link:qute://settings[]
page by running `:set` without any arguments, where all settings are listed and
customizable.
To bind and unbind keys, you can use the link:commands.html#bind[`:bind`] and
link:commands.html#unbind[`:unbind`] commands:
- Binding a key: `:bind ,v spawn mpv {url}`
- Unbinding: `:unbind ,v`
- Changing an existing binding: `bind --force ,v message-info foo`
Key chains starting with a comma are ideal for custom bindings, as the comma key
will never be used in a default keybinding.
The `autoconfig.yml` file is located in the "config" folder listed on the
link:qute://version[] page. On macOS, the "auto config" folder is used, which is
different from where hand-written config files are kept.
**Do not** edit `autoconfig.yml` by hand. Instead, see the next section.
Configuring qutebrowser via config.py
-------------------------------------
For more powerful configuration possibilities, you can create a `config.py`
file. Since it's a Python file, you have much more flexibility for
configuration. Note that qutebrowser will never touch this file - this means
you'll be responsible for updating it when upgrading to a newer qutebrowser
version.
The file should be located in the "config" location listed on
link:qute://version[], which is typically `~/.config/qutebrowser/config.py` on
Linux, `~/.qutebrowser/config.py` on macOS, and
`%APPDATA%/qutebrowser/config.py` on Windows.
Two global objects are pre-defined when executing the file: `c` and `config`.
Changing settings
~~~~~~~~~~~~~~~~~
`c` is a shorthand object to easily set settings like this:
[source,python]
----
c.tabs.position = "left"
c.completion.shrink = True
----
See the link:settings.html[settings help page] for all available settings. The
accepted values depend on the type of the option. Commonly used are:
- Strings: `c.tabs.position = "left"`
- Booleans: `c.completion.shrink = True`
- Integers: `c.messages.timeout = 5000`
- Dictionaries:
* `c.headers.custom = {'X-Hello': 'World'}` to override any other values in the
dictionary.
* `c.aliases['foo'] = ':message-info foo'` to add a single value.
- Lists:
* `c.url.start_pages = ["https://www.qutebrowser.org/"]` to override any
previous elements.
* `c.url.start_pages.append("https://www.python.org/")` to add a new value.
Any other config types (e.g. a color) are specified as a string, with the
exception of the `Regex` type which can take either a string (with an `r` prefix
to preserve backslashes) or a Python regex object:
- `c.hints.next_regexes.append(r'\bvor\b')`
- `c.hints.prev_regexes.append(re.compile(r'\bzurück\b'))`
If you want to read a setting, you can use the `c` object to do so as well:
`c.colors.tabs.even.bg = c.colors.tabs.odd.bg`.
Using strings for setting names
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you want to set settings based on their name as a string, use the
`config.set` method:
[source,python]
----
config.set('content.javascript.enabled', False)
----
To read a setting, use the `config.get` method:
[source,python]
----
color = config.get('colors.completion.fg')
----
Binding keys
~~~~~~~~~~~~
While it's possible to change the `bindings.commands` setting to bind keys, it's
preferred to use the `config.bind` command. Doing so ensures the commands are
valid and normalizes different expressions which map to the same key.
For details on how to specify keys and the available modes, see the
link:settings.html#bindings.commands[documentation] for the `bindings.commands`
setting.
To bind a key:
[source,python]
----
config.bind(',v', 'spawn mpv {url}', mode='normal')
----
If the key is already bound, `force=True` needs to be given to rebind it:
[source,python]
----
config.bind(',v', 'message-info foo', mode='normal', force=True)
----
To unbind a key (either a key which has been bound before, or a default binding):
[source,python]
----
config.unbind(',v', mode='normal')
----
Key chains starting with a comma are ideal for custom bindings, as the comma key
will never be used in a default keybinding.
To suppress loading of any default keybindings, you can set `c.bindings.defaults
= {}`.
Prevent loading `autoconfig.yml`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you want all customization done via `:set`, `:bind` and `:unbind` to be
temporary, you can suppress loading `autoconfig.yml` in your `config.py` by
doing:
[source,python]
----
config.load_autoconfig = False
----
Handling errors
~~~~~~~~~~~~~~~
If there are errors in your `config.py`, qutebrowser will try to apply as much
of it as possible, and show an error dialog before starting.
qutebrowser tries to display errors which are easy to understand even for people
who are not used to writing Python. If you see a config error which you find
confusing or you think qutebrowser could handle better, please
https://github.com/qutebrowser/qutebrowser/issues[open an issue]!

View File

@ -10,6 +10,7 @@ The following help pages are currently available:
* link:../../FAQ.html[Frequently asked questions] * link:../../FAQ.html[Frequently asked questions]
* link:../../CHANGELOG.html[Change Log] * link:../../CHANGELOG.html[Change Log]
* link:commands.html[Documentation of commands] * link:commands.html[Documentation of commands]
* link:configuring.html[Configuring qutebrowser]
* link:settings.html[Documentation of settings] * link:settings.html[Documentation of settings]
* link:../userscripts.html[How to write userscripts] * link:../userscripts.html[How to write userscripts]
* link:../../CONTRIBUTING.html[Contributing to qutebrowser] * link:../../CONTRIBUTING.html[Contributing to qutebrowser]

File diff suppressed because it is too large Load Diff

View File

@ -44,7 +44,7 @@ show it.
*-V*, *--version*:: *-V*, *--version*::
Show version and quit. Show version and quit.
*-s* 'SECTION' 'OPTION' 'VALUE', *--set* 'SECTION' 'OPTION' 'VALUE':: *-s* 'OPTION' 'VALUE', *--set* 'OPTION' 'VALUE'::
Set a temporary setting for this session. Set a temporary setting for this session.
*-r* 'SESSION', *--restore* 'SESSION':: *-r* 'SESSION', *--restore* 'SESSION'::

View File

@ -15,7 +15,8 @@ def get_data_files():
('../qutebrowser/img', 'img'), ('../qutebrowser/img', 'img'),
('../qutebrowser/javascript', 'javascript'), ('../qutebrowser/javascript', 'javascript'),
('../qutebrowser/html/doc', 'html/doc'), ('../qutebrowser/html/doc', 'html/doc'),
('../qutebrowser/git-commit-id', '') ('../qutebrowser/git-commit-id', ''),
('../qutebrowser/config/configdata.yml', 'config'),
] ]
# if os.path.exists(os.path.join('qutebrowser', '3rdparty', 'pdfjs')): # if os.path.exists(os.path.join('qutebrowser', '3rdparty', 'pdfjs')):

View File

@ -23,7 +23,7 @@
# If run from qutebrowser as a userscript, it runs :open on the URL # If run from qutebrowser as a userscript, it runs :open on the URL
# If not, it opens a new qutebrowser window at 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: # Ideal for use with tabs_are_windows. Set a hotkey to launch this script, then:
# :bind o spawn --userscript dmenu_qutebrowser # :bind o spawn --userscript dmenu_qutebrowser
# #
# Use the hotkey to open in new tab/window, press 'o' to open URL in current tab/window # Use the hotkey to open in new tab/window, press 'o' to open URL in current tab/window

View File

@ -12,7 +12,7 @@
# - rofi (in a recent version) # - rofi (in a recent version)
# - xdg-open and xdg-mime # - xdg-open and xdg-mime
# - You should configure qutebrowser to download files to a single directory # - You should configure qutebrowser to download files to a single directory
# - It comes in handy if you enable remove-finished-downloads. If you want to # - It comes in handy if you enable downloads.remove_finished. If you want to
# see the recent downloads, just press "sd". # see the recent downloads, just press "sd".
# #
# Thorsten Wißmann, 2015 (thorsten` on freenode) # Thorsten Wißmann, 2015 (thorsten` on freenode)

View File

@ -21,7 +21,7 @@
# Opens all links to feeds defined in the head of a site # Opens all links to feeds defined in the head of a site
# #
# Ideal for use with tabs-are-windows. Set a hotkey to launch this script, then: # Ideal for use with tabs_are_windows. Set a hotkey to launch this script, then:
# :bind gF spawn --userscript openfeeds # :bind gF spawn --userscript openfeeds
# #
# Use the hotkey to open the feeds in new tab/window, press 'gF' to open # Use the hotkey to open the feeds in new tab/window, press 'gF' to open

View File

@ -15,6 +15,7 @@ markers =
end2end: End to end tests which run qutebrowser as subprocess end2end: End to end tests which run qutebrowser as subprocess
xfail_norun: xfail the test with out running it xfail_norun: xfail the test with out running it
ci: Tests which should only run on CI. ci: Tests which should only run on CI.
no_ci: Tests which should not run on CI.
qtwebengine_todo: Features still missing with QtWebEngine qtwebengine_todo: Features still missing with QtWebEngine
qtwebengine_skip: Tests not applicable with QtWebEngine qtwebengine_skip: Tests not applicable with QtWebEngine
qtwebkit_skip: Tests not applicable with QtWebKit qtwebkit_skip: Tests not applicable with QtWebKit
@ -26,6 +27,7 @@ markers =
this: Used to mark tests during development this: Used to mark tests during development
no_invalid_lines: Don't fail on unparseable lines in end2end tests no_invalid_lines: Don't fail on unparseable lines in end2end tests
issue2478: Tests which are broken on Windows with QtWebEngine, https://github.com/qutebrowser/qutebrowser/issues/2478 issue2478: Tests which are broken on Windows with QtWebEngine, https://github.com/qutebrowser/qutebrowser/issues/2478
qt55: Tests only running on Qt 5.5 or later
qt_log_level_fail = WARNING qt_log_level_fail = WARNING
qt_log_ignore = qt_log_ignore =
^SpellCheck: .* ^SpellCheck: .*
@ -50,7 +52,8 @@ qt_log_ignore =
^Error when parsing the netrc file ^Error when parsing the netrc file
^Image of format '' blocked because it is not considered safe. If you are sure it is safe to do so, you can white-list the format by setting the environment variable QTWEBKIT_IMAGEFORMAT_WHITELIST= ^Image of format '' blocked because it is not considered safe. If you are sure it is safe to do so, you can white-list the format by setting the environment variable QTWEBKIT_IMAGEFORMAT_WHITELIST=
^QPainter::end: Painter ended with \d+ saved states ^QPainter::end: Painter ended with \d+ saved states
^QSslSocket: cannot resolve SSLv[23]_(client|server)_method ^QSslSocket: cannot resolve *
^Incompatible version of OpenSSL
^QQuickWidget::invalidateRenderControl could not make context current ^QQuickWidget::invalidateRenderControl could not make context current
^libpng warning: iCCP: known incorrect sRGB profile ^libpng warning: iCCP: known incorrect sRGB profile
xfail_strict = true xfail_strict = true

View File

@ -22,7 +22,6 @@
import os import os
import sys import sys
import subprocess import subprocess
import configparser
import functools import functools
import json import json
import shutil import shutil
@ -44,8 +43,7 @@ import qutebrowser
import qutebrowser.resources import qutebrowser.resources
from qutebrowser.completion.models import miscmodels from qutebrowser.completion.models import miscmodels
from qutebrowser.commands import cmdutils, runners, cmdexc from qutebrowser.commands import cmdutils, runners, cmdexc
from qutebrowser.config import style, config, websettings, configexc from qutebrowser.config import config, websettings, configexc
from qutebrowser.config.parsers import keyconf
from qutebrowser.browser import (urlmarks, adblock, history, browsertab, from qutebrowser.browser import (urlmarks, adblock, history, browsertab,
downloads) downloads)
from qutebrowser.browser.network import proxy from qutebrowser.browser.network import proxy
@ -54,7 +52,7 @@ from qutebrowser.browser.webkit.network import networkmanager
from qutebrowser.keyinput import macros from qutebrowser.keyinput import macros
from qutebrowser.mainwindow import mainwindow, prompt from qutebrowser.mainwindow import mainwindow, prompt
from qutebrowser.misc import (readline, ipc, savemanager, sessions, from qutebrowser.misc import (readline, ipc, savemanager, sessions,
crashsignal, earlyinit, objects, sql) crashsignal, earlyinit, objects, sql, cmdhistory)
from qutebrowser.misc import utilcmds # pylint: disable=unused-import from qutebrowser.misc import utilcmds # pylint: disable=unused-import
from qutebrowser.utils import (log, version, message, utils, qtutils, urlutils, from qutebrowser.utils import (log, version, message, utils, qtutils, urlutils,
objreg, usertypes, standarddir, error) objreg, usertypes, standarddir, error)
@ -148,8 +146,6 @@ def init(args, crash_handler):
objreg.register('event-filter', event_filter) objreg.register('event-filter', event_filter)
log.init.debug("Connecting signals...") log.init.debug("Connecting signals...")
config_obj = objreg.get('config')
config_obj.style_changed.connect(style.get_stylesheet.cache_clear)
qApp.focusChanged.connect(on_focus_changed) qApp.focusChanged.connect(on_focus_changed)
_process_args(args) _process_args(args)
@ -184,11 +180,10 @@ def _init_icon():
def _process_args(args): def _process_args(args):
"""Open startpage etc. and process commandline args.""" """Open startpage etc. and process commandline args."""
config_obj = objreg.get('config') for opt, val in args.temp_settings:
for sect, opt, val in args.temp_settings:
try: try:
config_obj.set('temp', sect, opt, val) config.instance.set_str(opt, val)
except (configexc.Error, configparser.Error) as e: except configexc.Error as e:
message.error("set: {} - {}".format(e.__class__.__name__, e)) message.error("set: {} - {}".format(e.__class__.__name__, e))
if not args.override_restore: if not args.override_restore:
@ -274,7 +269,7 @@ def process_pos_args(args, via_ipc=False, cwd=None, target_arg=None):
if via_ipc and target_arg and target_arg != 'auto': if via_ipc and target_arg and target_arg != 'auto':
open_target = target_arg open_target = target_arg
else: else:
open_target = config.get('general', 'new-instance-open-target') open_target = config.val.new_instance_open_target
win_id = mainwindow.get_window(via_ipc, force_target=open_target) win_id = mainwindow.get_window(via_ipc, force_target=open_target)
tabbed_browser = objreg.get('tabbed-browser', scope='window', tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id) window=win_id)
@ -289,7 +284,7 @@ def process_pos_args(args, via_ipc=False, cwd=None, target_arg=None):
else: else:
background = open_target in ['tab-bg', 'tab-bg-silent'] background = open_target in ['tab-bg', 'tab-bg-silent']
tabbed_browser.tabopen(url, background=background, tabbed_browser.tabopen(url, background=background,
explicit=True) related=False)
def _open_startpage(win_id=None): def _open_startpage(win_id=None):
@ -309,15 +304,9 @@ def _open_startpage(win_id=None):
tabbed_browser = objreg.get('tabbed-browser', scope='window', tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=cur_win_id) window=cur_win_id)
if tabbed_browser.count() == 0: if tabbed_browser.count() == 0:
log.init.debug("Opening startpage") log.init.debug("Opening start pages")
for urlstr in config.get('general', 'startpage'): for url in config.val.url.start_pages:
try: tabbed_browser.tabopen(url)
url = urlutils.fuzzy_url(urlstr, do_search=False)
except urlutils.InvalidUrlError as e:
message.error("Error when opening startpage: {}".format(e))
tabbed_browser.tabopen(QUrl('about:blank'))
else:
tabbed_browser.tabopen(url)
def _open_special_pages(args): def _open_special_pages(args):
@ -335,6 +324,7 @@ def _open_special_pages(args):
return return
state_config = objreg.get('state-config') state_config = objreg.get('state-config')
general_sect = state_config['general']
tabbed_browser = objreg.get('tabbed-browser', scope='window', tabbed_browser = objreg.get('tabbed-browser', scope='window',
window='last-focused') window='last-focused')
@ -342,21 +332,32 @@ def _open_special_pages(args):
needs_warning = (objects.backend == usertypes.Backend.QtWebKit and needs_warning = (objects.backend == usertypes.Backend.QtWebKit and
not qtutils.is_qtwebkit_ng()) not qtutils.is_qtwebkit_ng())
warning_shown = state_config['general'].get('backend-warning-shown') == '1' warning_shown = general_sect.get('backend-warning-shown') == '1'
if not warning_shown and needs_warning: if not warning_shown and needs_warning:
tabbed_browser.tabopen(QUrl('qute://backend-warning'), tabbed_browser.tabopen(QUrl('qute://backend-warning'),
background=False) background=False)
state_config['general']['backend-warning-shown'] = '1' general_sect['backend-warning-shown'] = '1'
# Quickstart page # Quickstart page
quickstart_done = state_config['general'].get('quickstart-done') == '1' quickstart_done = general_sect.get('quickstart-done') == '1'
if not quickstart_done: if not quickstart_done:
tabbed_browser.tabopen( tabbed_browser.tabopen(
QUrl('https://www.qutebrowser.org/quickstart.html')) QUrl('https://www.qutebrowser.org/quickstart.html'))
state_config['general']['quickstart-done'] = '1' general_sect['quickstart-done'] = '1'
# Setting migration page
needs_migration = os.path.exists(
os.path.join(standarddir.config(), 'qutebrowser.conf'))
migration_shown = general_sect.get('config-migration-shown') == '1'
if needs_migration and not migration_shown:
tabbed_browser.tabopen(QUrl('qute://help/configuring.html'),
background=False)
general_sect['config-migration-shown'] = '1'
def _save_version(): def _save_version():
@ -422,9 +423,6 @@ def _init_modules(args, crash_handler):
config.init(qApp) config.init(qApp)
save_manager.init_autosave() save_manager.init_autosave()
log.init.debug("Initializing keys...")
keyconf.init(qApp)
log.init.debug("Initializing sql...") log.init.debug("Initializing sql...")
try: try:
sql.init(os.path.join(standarddir.data(), 'history.sqlite')) sql.init(os.path.join(standarddir.data(), 'history.sqlite'))
@ -433,6 +431,9 @@ def _init_modules(args, crash_handler):
pre_text='Error initializing SQL') pre_text='Error initializing SQL')
sys.exit(usertypes.Exit.err_init) sys.exit(usertypes.Exit.err_init)
log.init.debug("Initializing command history...")
cmdhistory.init()
log.init.debug("Initializing web history...") log.init.debug("Initializing web history...")
history.init(qApp) history.init(qApp)
@ -470,7 +471,7 @@ def _init_modules(args, crash_handler):
objreg.register('cache', diskcache) objreg.register('cache', diskcache)
log.init.debug("Misc initialization...") log.init.debug("Misc initialization...")
if config.get('ui', 'hide-wayland-decoration'): if config.val.window.hide_wayland_decoration:
os.environ['QT_WAYLAND_DISABLE_WINDOWDECORATION'] = '1' os.environ['QT_WAYLAND_DISABLE_WINDOWDECORATION'] = '1'
else: else:
os.environ.pop('QT_WAYLAND_DISABLE_WINDOWDECORATION', None) os.environ.pop('QT_WAYLAND_DISABLE_WINDOWDECORATION', None)
@ -640,8 +641,25 @@ class Quitter:
else: else:
return True return True
@cmdutils.register(instance='quitter', name=['quit', 'q'], @cmdutils.register(instance='quitter', name='quit')
ignore_args=True) @cmdutils.argument('session', completion=miscmodels.session)
def quit(self, save=False, session=None):
"""Quit qutebrowser.
Args:
save: When given, save the open windows even if auto_save.session
is turned off.
session: The name of the session to save.
"""
if session is not None and not save:
raise cmdexc.CommandError("Session name given without --save!")
if save:
if session is None:
session = sessions.default
self.shutdown(session=session)
else:
self.shutdown()
def shutdown(self, status=0, session=None, last_window=False, def shutdown(self, status=0, session=None, last_window=False,
restart=False): restart=False):
"""Quit qutebrowser. """Quit qutebrowser.
@ -663,7 +681,7 @@ class Quitter:
if session is not None: if session is not None:
session_manager.save(session, last_window=last_window, session_manager.save(session, last_window=last_window,
load_next_time=True) load_next_time=True)
elif config.get('general', 'save-session'): elif config.val.auto_save.session:
session_manager.save(sessions.default, last_window=last_window, session_manager.save(sessions.default, last_window=last_window,
load_next_time=True) load_next_time=True)
@ -742,16 +760,6 @@ class Quitter:
# segfaults. # segfaults.
QTimer.singleShot(0, functools.partial(qApp.exit, status)) QTimer.singleShot(0, functools.partial(qApp.exit, status))
@cmdutils.register(instance='quitter', name='wq')
@cmdutils.argument('name', completion=miscmodels.session)
def save_and_quit(self, name=sessions.default):
"""Save open pages and quit.
Args:
name: The name of the session.
"""
self.shutdown(session=name)
class Application(QApplication): class Application(QApplication):

View File

@ -67,11 +67,7 @@ def is_whitelisted_host(host):
Args: Args:
host: The host of the request as string. host: The host of the request as string.
""" """
whitelist = config.get('content', 'host-blocking-whitelist') for pattern in config.val.content.host_blocking.whitelist:
if whitelist is None:
return False
for pattern in whitelist:
if fnmatch.fnmatch(host, pattern.lower()): if fnmatch.fnmatch(host, pattern.lower()):
return True return True
return False return False
@ -114,16 +110,16 @@ class HostBlocker:
data_dir = standarddir.data() data_dir = standarddir.data()
self._local_hosts_file = os.path.join(data_dir, 'blocked-hosts') self._local_hosts_file = os.path.join(data_dir, 'blocked-hosts')
self.on_config_changed() self._update_files()
config_dir = standarddir.config() config_dir = standarddir.config()
self._config_hosts_file = os.path.join(config_dir, 'blocked-hosts') self._config_hosts_file = os.path.join(config_dir, 'blocked-hosts')
objreg.get('config').changed.connect(self.on_config_changed) config.instance.changed.connect(self._update_files)
def is_blocked(self, url): def is_blocked(self, url):
"""Check if the given URL (as QUrl) is blocked.""" """Check if the given URL (as QUrl) is blocked."""
if not config.get('content', 'host-blocking-enabled'): if not config.val.content.host_blocking.enabled:
return False return False
host = url.host() host = url.host()
return ((host in self._blocked_hosts or return ((host in self._blocked_hosts or
@ -164,9 +160,9 @@ class HostBlocker:
if not found: if not found:
args = objreg.get('args') args = objreg.get('args')
if (config.get('content', 'host-block-lists') is not None and if (config.val.content.host_blocking.lists and
args.basedir is None and args.basedir is None and
config.get('content', 'host-blocking-enabled')): config.val.content.host_blocking.enabled):
message.info("Run :adblock-update to get adblock lists.") message.info("Run :adblock-update to get adblock lists.")
@cmdutils.register(instance='host-blocker') @cmdutils.register(instance='host-blocker')
@ -180,18 +176,16 @@ class HostBlocker:
self._config_blocked_hosts) self._config_blocked_hosts)
self._blocked_hosts = set() self._blocked_hosts = set()
self._done_count = 0 self._done_count = 0
urls = config.get('content', 'host-block-lists')
download_manager = objreg.get('qtnetwork-download-manager', download_manager = objreg.get('qtnetwork-download-manager',
scope='window', window='last-focused') scope='window', window='last-focused')
if urls is None: for url in config.val.content.host_blocking.lists:
return
for url in urls:
if url.scheme() == 'file': if url.scheme() == 'file':
filename = url.toLocalFile()
try: try:
fileobj = open(url.path(), 'rb') fileobj = open(filename, 'rb')
except OSError as e: except OSError as e:
message.error("adblock: Error while reading {}: {}".format( message.error("adblock: Error while reading {}: {}".format(
url.path(), e.strerror)) filename, e.strerror))
continue continue
download = FakeDownload(fileobj) download = FakeDownload(fileobj)
self._in_progress.append(download) self._in_progress.append(download)
@ -292,11 +286,10 @@ class HostBlocker:
message.info("adblock: Read {} hosts from {} sources.".format( message.info("adblock: Read {} hosts from {} sources.".format(
len(self._blocked_hosts), self._done_count)) len(self._blocked_hosts), self._done_count))
@config.change_filter('content', 'host-block-lists') @config.change_filter('content.host_blocking.lists')
def on_config_changed(self): def _update_files(self):
"""Update files when the config changed.""" """Update files when the config changed."""
urls = config.get('content', 'host-block-lists') if not config.val.content.host_blocking.lists:
if urls is None:
try: try:
os.remove(self._local_hosts_file) os.remove(self._local_hosts_file)
except FileNotFoundError: except FileNotFoundError:

View File

@ -61,9 +61,6 @@ def init():
if objects.backend == usertypes.Backend.QtWebEngine: if objects.backend == usertypes.Backend.QtWebEngine:
from qutebrowser.browser.webengine import webenginetab from qutebrowser.browser.webengine import webenginetab
webenginetab.init() webenginetab.init()
else:
from qutebrowser.browser.webkit import webkittab
webkittab.init()
class WebTabError(Exception): class WebTabError(Exception):
@ -188,13 +185,28 @@ class AbstractSearch(QObject):
self.text = None self.text = None
self.search_displayed = False self.search_displayed = False
def search(self, text, *, ignore_case=False, reverse=False, def _is_case_sensitive(self, ignore_case):
"""Check if case-sensitivity should be used.
This assumes self.text is already set properly.
Arguments:
ignore_case: The ignore_case value from the config.
"""
mapping = {
'smart': not self.text.islower(),
'never': True,
'always': False,
}
return mapping[ignore_case]
def search(self, text, *, ignore_case='never', reverse=False,
result_cb=None): result_cb=None):
"""Find the given text on the page. """Find the given text on the page.
Args: Args:
text: The text to search for. text: The text to search for.
ignore_case: Search case-insensitively. (True/False/'smart') ignore_case: Search case-insensitively. ('always'/'never/'smart')
reverse: Reverse search direction. reverse: Reverse search direction.
result_cb: Called with a bool indicating whether a match was found. result_cb: Called with a bool indicating whether a match was found.
""" """
@ -236,7 +248,7 @@ class AbstractZoom(QObject):
self._win_id = win_id self._win_id = win_id
self._default_zoom_changed = False self._default_zoom_changed = False
self._init_neighborlist() self._init_neighborlist()
objreg.get('config').changed.connect(self._on_config_changed) config.instance.changed.connect(self._on_config_changed)
# # FIXME:qtwebengine is this needed? # # FIXME:qtwebengine is this needed?
# # For some reason, this signal doesn't get disconnected automatically # # For some reason, this signal doesn't get disconnected automatically
@ -245,21 +257,21 @@ class AbstractZoom(QObject):
# self.destroyed.connect(functools.partial( # self.destroyed.connect(functools.partial(
# cfg.changed.disconnect, self.init_neighborlist)) # cfg.changed.disconnect, self.init_neighborlist))
@pyqtSlot(str, str) @pyqtSlot(str)
def _on_config_changed(self, section, option): def _on_config_changed(self, option):
if section == 'ui' and option in ['zoom-levels', 'default-zoom']: if option in ['zoom.levels', 'zoom.default']:
if not self._default_zoom_changed: if not self._default_zoom_changed:
factor = float(config.get('ui', 'default-zoom')) / 100 factor = float(config.val.zoom.default) / 100
self._set_factor_internal(factor) self._set_factor_internal(factor)
self._default_zoom_changed = False self._default_zoom_changed = False
self._init_neighborlist() self._init_neighborlist()
def _init_neighborlist(self): def _init_neighborlist(self):
"""Initialize self._neighborlist.""" """Initialize self._neighborlist."""
levels = config.get('ui', 'zoom-levels') levels = config.val.zoom.levels
self._neighborlist = usertypes.NeighborList( self._neighborlist = usertypes.NeighborList(
levels, mode=usertypes.NeighborList.Modes.edge) levels, mode=usertypes.NeighborList.Modes.edge)
self._neighborlist.fuzzyval = config.get('ui', 'default-zoom') self._neighborlist.fuzzyval = config.val.zoom.default
def offset(self, offset): def offset(self, offset):
"""Increase/Decrease the zoom level by the given offset. """Increase/Decrease the zoom level by the given offset.
@ -295,8 +307,7 @@ class AbstractZoom(QObject):
raise NotImplementedError raise NotImplementedError
def set_default(self): def set_default(self):
default_zoom = config.get('ui', 'default-zoom') self._set_factor_internal(float(config.val.zoom.default) / 100)
self._set_factor_internal(float(default_zoom) / 100)
class AbstractCaret(QObject): class AbstractCaret(QObject):
@ -705,8 +716,8 @@ class AbstractTab(QWidget):
self.load_started.emit() self.load_started.emit()
def _handle_auto_insert_mode(self, ok): def _handle_auto_insert_mode(self, ok):
"""Handle auto-insert-mode after loading finished.""" """Handle `input.insert_mode.auto_load` after loading finished."""
if not config.get('input', 'auto-insert-mode') or not ok: if not config.val.input.insert_mode.auto_load or not ok:
return return
cur_mode = self._mode_manager.mode cur_mode = self._mode_manager.mode

View File

@ -34,7 +34,7 @@ import pygments.lexers
import pygments.formatters import pygments.formatters
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
from qutebrowser.config import config, configexc from qutebrowser.config import config, configdata
from qutebrowser.browser import (urlmarks, browsertab, inspector, navigate, from qutebrowser.browser import (urlmarks, browsertab, inspector, navigate,
webelem, downloads) webelem, downloads)
from qutebrowser.keyinput import modeman from qutebrowser.keyinput import modeman
@ -111,7 +111,7 @@ class CommandDispatcher:
return widget return widget
def _open(self, url, tab=False, background=False, window=False, def _open(self, url, tab=False, background=False, window=False,
explicit=True, private=None): related=False, private=None):
"""Helper function to open a page. """Helper function to open a page.
Args: Args:
@ -132,9 +132,9 @@ class CommandDispatcher:
tabbed_browser = self._new_tabbed_browser(private) tabbed_browser = self._new_tabbed_browser(private)
tabbed_browser.tabopen(url) tabbed_browser.tabopen(url)
elif tab: elif tab:
tabbed_browser.tabopen(url, background=False, explicit=explicit) tabbed_browser.tabopen(url, background=False, related=related)
elif background: elif background:
tabbed_browser.tabopen(url, background=True, explicit=explicit) tabbed_browser.tabopen(url, background=True, related=related)
else: else:
widget = self._current_widget() widget = self._current_widget()
widget.openurl(url) widget.openurl(url)
@ -179,7 +179,7 @@ class CommandDispatcher:
prev: Force selecting the tab before the current tab. prev: Force selecting the tab before the current tab.
next_: Force selecting the tab after the current tab. next_: Force selecting the tab after the current tab.
opposite: Force selecting the tab in the opposite direction of opposite: Force selecting the tab in the opposite direction of
what's configured in 'tabs->select-on-remove'. what's configured in 'tabs.select_on_remove'.
Return: Return:
QTabBar.SelectLeftTab, QTabBar.SelectRightTab, or None if no change QTabBar.SelectLeftTab, QTabBar.SelectRightTab, or None if no change
@ -191,17 +191,17 @@ class CommandDispatcher:
elif next_: elif next_:
return QTabBar.SelectRightTab return QTabBar.SelectRightTab
elif opposite: elif opposite:
conf_selection = config.get('tabs', 'select-on-remove') conf_selection = config.val.tabs.select_on_remove
if conf_selection == QTabBar.SelectLeftTab: if conf_selection == QTabBar.SelectLeftTab:
return QTabBar.SelectRightTab return QTabBar.SelectRightTab
elif conf_selection == QTabBar.SelectRightTab: elif conf_selection == QTabBar.SelectRightTab:
return QTabBar.SelectLeftTab return QTabBar.SelectLeftTab
elif conf_selection == QTabBar.SelectPreviousTab: elif conf_selection == QTabBar.SelectPreviousTab:
raise cmdexc.CommandError( raise cmdexc.CommandError(
"-o is not supported with 'tabs->select-on-remove' set to " "-o is not supported with 'tabs.select_on_remove' set to "
"'last-used'!") "'last-used'!")
else: # pragma: no cover else: # pragma: no cover
raise ValueError("Invalid select-on-remove value " raise ValueError("Invalid select_on_remove value "
"{!r}!".format(conf_selection)) "{!r}!".format(conf_selection))
return None return None
@ -213,7 +213,7 @@ class CommandDispatcher:
prev: Force selecting the tab before the current tab. prev: Force selecting the tab before the current tab.
next_: Force selecting the tab after the current tab. next_: Force selecting the tab after the current tab.
opposite: Force selecting the tab in the opposite direction of opposite: Force selecting the tab in the opposite direction of
what's configured in 'tabs->select-on-remove'. what's configured in 'tabs.select_on_remove'.
count: The tab index to close, or None count: The tab index to close, or None
""" """
tabbar = self._tabbed_browser.tabBar() tabbar = self._tabbed_browser.tabBar()
@ -238,7 +238,7 @@ class CommandDispatcher:
prev: Force selecting the tab before the current tab. prev: Force selecting the tab before the current tab.
next_: Force selecting the tab after the current tab. next_: Force selecting the tab after the current tab.
opposite: Force selecting the tab in the opposite direction of opposite: Force selecting the tab in the opposite direction of
what's configured in 'tabs->select-on-remove'. what's configured in 'tabs.select_on_remove'.
force: Avoid confirmation for pinned tabs. force: Avoid confirmation for pinned tabs.
count: The tab index to close, or None count: The tab index to close, or None
""" """
@ -256,7 +256,7 @@ class CommandDispatcher:
def tab_pin(self, count=None): def tab_pin(self, count=None):
"""Pin/Unpin the current/[count]th tab. """Pin/Unpin the current/[count]th tab.
Pinning a tab shrinks it to tabs->pinned-width size. Pinning a tab shrinks it to `tabs.width.pinned` size.
Attempting to close a pinned tab will cause a confirmation, Attempting to close a pinned tab will cause a confirmation,
unless --force is passed. unless --force is passed.
@ -274,7 +274,7 @@ class CommandDispatcher:
maxsplit=0, scope='window') maxsplit=0, scope='window')
@cmdutils.argument('url', completion=urlmodel.url) @cmdutils.argument('url', completion=urlmodel.url)
@cmdutils.argument('count', count=True) @cmdutils.argument('count', count=True)
def openurl(self, url=None, implicit=False, def openurl(self, url=None, related=False,
bg=False, tab=False, window=False, count=None, secure=False, bg=False, tab=False, window=False, count=None, secure=False,
private=False): private=False):
"""Open a URL in the current/[count]th tab. """Open a URL in the current/[count]th tab.
@ -286,14 +286,14 @@ class CommandDispatcher:
bg: Open in a new background tab. bg: Open in a new background tab.
tab: Open in a new tab. tab: Open in a new tab.
window: Open in a new window. window: Open in a new window.
implicit: If opening a new tab, treat the tab as implicit (like related: If opening a new tab, position the tab as related to the
clicking on a link). current one (like clicking on a link).
count: The tab index to open the URL in, or None. count: The tab index to open the URL in, or None.
secure: Force HTTPS. secure: Force HTTPS.
private: Open a new window in private browsing mode. private: Open a new window in private browsing mode.
""" """
if url is None: if url is None:
urls = [config.get('general', 'default-page')] urls = [config.val.url.default_page]
else: else:
urls = self._parse_url_input(url) urls = self._parse_url_input(url)
@ -305,7 +305,7 @@ class CommandDispatcher:
bg = True bg = True
if tab or bg or window or private: if tab or bg or window or private:
self._open(cur_url, tab, bg, window, explicit=not implicit, self._open(cur_url, tab, bg, window, related=related,
private=private) private=private)
else: else:
curtab = self._cntwidget(count) curtab = self._cntwidget(count)
@ -490,7 +490,7 @@ class CommandDispatcher:
raise cmdexc.CommandError(e) raise cmdexc.CommandError(e)
# The new tab could be in a new tabbed_browser (e.g. because of # The new tab could be in a new tabbed_browser (e.g. because of
# tabs-are-windows being set) # tabs.tabs_are_windows being set)
if window: if window:
new_tabbed_browser = self._new_tabbed_browser( new_tabbed_browser = self._new_tabbed_browser(
private=self._tabbed_browser.private) private=self._tabbed_browser.private)
@ -502,9 +502,9 @@ class CommandDispatcher:
idx = new_tabbed_browser.indexOf(newtab) idx = new_tabbed_browser.indexOf(newtab)
new_tabbed_browser.set_page_title(idx, cur_title) new_tabbed_browser.set_page_title(idx, cur_title)
if config.get('tabs', 'show-favicons'): if config.val.tabs.favicons.show:
new_tabbed_browser.setTabIcon(idx, curtab.icon()) new_tabbed_browser.setTabIcon(idx, curtab.icon())
if config.get('tabs', 'tabs-are-windows'): if config.val.tabs.tabs_are_windows:
new_tabbed_browser.window().setWindowIcon(curtab.icon()) new_tabbed_browser.window().setWindowIcon(curtab.icon())
newtab.data.keep_icon = True newtab.data.keep_icon = True
@ -622,7 +622,7 @@ class CommandDispatcher:
tab=tab, background=bg, window=window) tab=tab, background=bg, window=window)
elif where in ['up', 'increment', 'decrement']: elif where in ['up', 'increment', 'decrement']:
new_url = handlers[where](url, count) new_url = handlers[where](url, count)
self._open(new_url, tab, bg, window, explicit=False) self._open(new_url, tab, bg, window, related=True)
else: # pragma: no cover else: # pragma: no cover
raise ValueError("Got called with invalid value {} for " raise ValueError("Got called with invalid value {} for "
"`where'.".format(where)) "`where'.".format(where))
@ -771,7 +771,7 @@ class CommandDispatcher:
url_query.setQueryDelimiters('=', ';') url_query.setQueryDelimiters('=', ';')
url_query.setQuery(url_query_str) url_query.setQuery(url_query_str)
for key in dict(url_query.queryItems()): for key in dict(url_query.queryItems()):
if key in config.get('general', 'yank-ignored-url-parameters'): if key in config.val.url.yank_ignored_parameters:
url_query.removeQueryItem(key) url_query.removeQueryItem(key)
url.setQuery(url_query) url.setQuery(url_query)
return url.toString(flags) return url.toString(flags)
@ -842,7 +842,7 @@ class CommandDispatcher:
perc = tab.zoom.offset(count) perc = tab.zoom.offset(count)
except ValueError as e: except ValueError as e:
raise cmdexc.CommandError(e) raise cmdexc.CommandError(e)
message.info("Zoom level: {}%".format(perc), replace=True) message.info("Zoom level: {}%".format(int(perc)), replace=True)
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True) @cmdutils.argument('count', count=True)
@ -857,7 +857,7 @@ class CommandDispatcher:
perc = tab.zoom.offset(-count) perc = tab.zoom.offset(-count)
except ValueError as e: except ValueError as e:
raise cmdexc.CommandError(e) raise cmdexc.CommandError(e)
message.info("Zoom level: {}%".format(perc), replace=True) message.info("Zoom level: {}%".format(int(perc)), replace=True)
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True) @cmdutils.argument('count', count=True)
@ -881,14 +881,14 @@ class CommandDispatcher:
level = count if count is not None else zoom level = count if count is not None else zoom
if level is None: if level is None:
level = config.get('ui', 'default-zoom') level = config.val.zoom.default
tab = self._current_widget() tab = self._current_widget()
try: try:
tab.zoom.set_factor(float(level) / 100) tab.zoom.set_factor(float(level) / 100)
except ValueError: except ValueError:
raise cmdexc.CommandError("Can't zoom {}%!".format(level)) raise cmdexc.CommandError("Can't zoom {}%!".format(level))
message.info("Zoom level: {}%".format(level), replace=True) message.info("Zoom level: {}%".format(int(level)), replace=True)
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
def tab_only(self, prev=False, next_=False, force=False): def tab_only(self, prev=False, next_=False, force=False):
@ -947,7 +947,7 @@ class CommandDispatcher:
newidx = self._current_index() - count newidx = self._current_index() - count
if newidx >= 0: if newidx >= 0:
self._set_current_index(newidx) self._set_current_index(newidx)
elif config.get('tabs', 'wrap'): elif config.val.tabs.wrap:
self._set_current_index(newidx % self._count()) self._set_current_index(newidx % self._count())
else: else:
raise cmdexc.CommandError("First tab") raise cmdexc.CommandError("First tab")
@ -967,7 +967,7 @@ class CommandDispatcher:
newidx = self._current_index() + count newidx = self._current_index() + count
if newidx < self._count(): if newidx < self._count():
self._set_current_index(newidx) self._set_current_index(newidx)
elif config.get('tabs', 'wrap'): elif config.val.tabs.wrap:
self._set_current_index(newidx % self._count()) self._set_current_index(newidx % self._count())
else: else:
raise cmdexc.CommandError("Last tab") raise cmdexc.CommandError("Last tab")
@ -1124,7 +1124,7 @@ class CommandDispatcher:
elif index == '+': # pragma: no branch elif index == '+': # pragma: no branch
new_idx += delta new_idx += delta
if config.get('tabs', 'wrap'): if config.val.tabs.wrap:
new_idx %= self._count() new_idx %= self._count()
else: else:
# absolute moving # absolute moving
@ -1186,7 +1186,7 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
def home(self): def home(self):
"""Open main startpage in current tab.""" """Open main startpage in current tab."""
self.openurl(config.get('general', 'startpage')[0]) self._current_widget().openurl(config.val.url.start_pages[0])
def _run_userscript(self, cmd, *args, verbose=False): def _run_userscript(self, cmd, *args, verbose=False):
"""Run a userscript given as argument. """Run a userscript given as argument.
@ -1532,7 +1532,7 @@ class CommandDispatcher:
topic: The topic to show help for. topic: The topic to show help for.
- :__command__ for commands. - :__command__ for commands.
- __section__\->__option__ for settings. - __section__.__option__ for settings.
""" """
if topic is None: if topic is None:
path = 'index.html' path = 'index.html'
@ -1542,20 +1542,8 @@ class CommandDispatcher:
raise cmdexc.CommandError("Invalid command {}!".format( raise cmdexc.CommandError("Invalid command {}!".format(
command)) command))
path = 'commands.html#{}'.format(command) path = 'commands.html#{}'.format(command)
elif '->' in topic: elif topic in configdata.DATA:
parts = topic.split('->') path = 'settings.html#{}'.format(topic)
if len(parts) != 2:
raise cmdexc.CommandError("Invalid help topic {}!".format(
topic))
try:
config.get(*parts)
except configexc.NoSectionError:
raise cmdexc.CommandError("Invalid section {}!".format(
parts[0]))
except configexc.NoOptionError:
raise cmdexc.CommandError("Invalid option {}!".format(
parts[1]))
path = 'settings.html#{}'.format(topic.replace('->', '-'))
else: else:
raise cmdexc.CommandError("Invalid help topic {}!".format(topic)) raise cmdexc.CommandError("Invalid help topic {}!".format(topic))
url = QUrl('qute://help/{}'.format(path)) url = QUrl('qute://help/{}'.format(path))
@ -1608,7 +1596,7 @@ class CommandDispatcher:
"""Open an external editor with the currently selected form field. """Open an external editor with the currently selected form field.
The editor which should be launched can be configured via the The editor which should be launched can be configured via the
`general -> editor` config option. `editor.command` config option.
""" """
tab = self._current_widget() tab = self._current_widget()
tab.elements.find_focused(self._open_editor_cb) tab.elements.find_focused(self._open_editor_cb)
@ -1749,7 +1737,7 @@ class CommandDispatcher:
return return
options = { options = {
'ignore_case': config.get('general', 'ignore-case'), 'ignore_case': config.val.ignore_case,
'reverse': reverse, 'reverse': reverse,
} }
@ -2103,7 +2091,7 @@ class CommandDispatcher:
"""Navigate to a url formed in an external editor. """Navigate to a url formed in an external editor.
The editor which should be launched can be configured via the The editor which should be launched can be configured via the
`general -> editor` config option. `editor.command` config option.
Args: Args:
url: URL to edit; defaults to the current page url. url: URL to edit; defaults to the current page url.

View File

@ -69,15 +69,22 @@ class UnsupportedOperationError(Exception):
def download_dir(): def download_dir():
"""Get the download directory to use.""" """Get the download directory to use."""
directory = config.get('storage', 'download-directory') directory = config.val.downloads.location.directory
remember_dir = config.get('storage', 'remember-download-directory') remember_dir = config.val.downloads.location.remember
if remember_dir and last_used_directory is not None: if remember_dir and last_used_directory is not None:
return last_used_directory ddir = last_used_directory
elif directory is None: elif directory is None:
return standarddir.download() ddir = standarddir.download()
else: else:
return directory ddir = directory
try:
os.makedirs(ddir)
except FileExistsError:
pass
return ddir
def immediate_download_path(prompt_download_directory=None): def immediate_download_path(prompt_download_directory=None):
@ -88,11 +95,10 @@ def immediate_download_path(prompt_download_directory=None):
Args: Args:
prompt_download_directory: If this is something else than None, it prompt_download_directory: If this is something else than None, it
will overwrite the will overwrite the
storage->prompt-download-directory setting. downloads.location.prompt setting.
""" """
if prompt_download_directory is None: if prompt_download_directory is None:
prompt_download_directory = config.get('storage', prompt_download_directory = config.val.downloads.location.prompt
'prompt-download-directory')
if not prompt_download_directory: if not prompt_download_directory:
return download_dir() return download_dir()
@ -104,7 +110,7 @@ def _path_suggestion(filename):
Args: Args:
filename: The filename to use if included in the suggestion. filename: The filename to use if included in the suggestion.
""" """
suggestion = config.get('completion', 'download-path-suggestion') suggestion = config.val.downloads.location.suggestion
if suggestion == 'path': if suggestion == 'path':
# add trailing '/' if not present # add trailing '/' if not present
return os.path.join(download_dir(), '') return os.path.join(download_dir(), '')
@ -494,13 +500,13 @@ class AbstractDownloadItem(QObject):
Args: Args:
position: The color type requested, can be 'fg' or 'bg'. 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"] assert position in ["fg", "bg"]
start = config.get('colors', 'downloads.{}.start'.format(position)) # pylint: disable=bad-config-option
stop = config.get('colors', 'downloads.{}.stop'.format(position)) start = getattr(config.val.colors.downloads.start, position)
system = config.get('colors', 'downloads.{}.system'.format(position)) stop = getattr(config.val.colors.downloads.stop, position)
error = config.get('colors', 'downloads.{}.error'.format(position)) system = getattr(config.val.colors.downloads.system, position)
error = getattr(config.val.colors.downloads.error, position)
# pylint: enable=bad-config-option
if self.error_msg is not None: if self.error_msg is not None:
assert not self.successful assert not self.successful
return error return error
@ -572,7 +578,7 @@ class AbstractDownloadItem(QObject):
Args: Args:
cmdline: The command to use as string. A `{}` is expanded to the cmdline: The command to use as string. A `{}` is expanded to the
filename. None means to use the system's default filename. None means to use the system's default
application or `default-open-dispatcher` if set. If no application or `downloads.open_dispatcher` if set. If no
`{}` is found, the filename is appended to the cmdline. `{}` is found, the filename is appended to the cmdline.
""" """
assert self.successful assert self.successful
@ -757,7 +763,7 @@ class AbstractDownloadManager(QObject):
download.remove_requested.connect(functools.partial( download.remove_requested.connect(functools.partial(
self._remove_item, download)) self._remove_item, download))
delay = config.get('ui', 'remove-finished-downloads') delay = config.val.downloads.remove_finished
if delay > -1: if delay > -1:
download.finished.connect( download.finished.connect(
lambda: QTimer.singleShot(delay, download.remove)) lambda: QTimer.singleShot(delay, download.remove))

View File

@ -26,7 +26,7 @@ from PyQt5.QtCore import pyqtSlot, QSize, Qt, QTimer
from PyQt5.QtWidgets import QListView, QSizePolicy, QMenu, QStyleFactory from PyQt5.QtWidgets import QListView, QSizePolicy, QMenu, QStyleFactory
from qutebrowser.browser import downloads from qutebrowser.browser import downloads
from qutebrowser.config import style from qutebrowser.config import config
from qutebrowser.utils import qtutils, utils, objreg from qutebrowser.utils import qtutils, utils, objreg
@ -64,8 +64,8 @@ class DownloadView(QListView):
STYLESHEET = """ STYLESHEET = """
QListView { QListView {
background-color: {{ color['downloads.bg.bar'] }}; background-color: {{ conf.colors.downloads.bar.bg }};
font: {{ font['downloads'] }}; font: {{ conf.fonts.downloads }};
} }
QListView::item { QListView::item {
@ -76,7 +76,7 @@ class DownloadView(QListView):
def __init__(self, win_id, parent=None): def __init__(self, win_id, parent=None):
super().__init__(parent) super().__init__(parent)
self.setStyle(QStyleFactory.create('Fusion')) self.setStyle(QStyleFactory.create('Fusion'))
style.set_register_stylesheet(self) config.set_register_stylesheet(self)
self.setResizeMode(QListView.Adjust) self.setResizeMode(QListView.Adjust)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed) self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)

View File

@ -29,7 +29,7 @@ from string import ascii_lowercase
from PyQt5.QtCore import pyqtSlot, QObject, Qt, QUrl from PyQt5.QtCore import pyqtSlot, QObject, Qt, QUrl
from PyQt5.QtWidgets import QLabel from PyQt5.QtWidgets import QLabel
from qutebrowser.config import config, style from qutebrowser.config import config
from qutebrowser.keyinput import modeman, modeparsers from qutebrowser.keyinput import modeman, modeparsers
from qutebrowser.browser import webelem from qutebrowser.browser import webelem
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
@ -65,10 +65,10 @@ class HintLabel(QLabel):
STYLESHEET = """ STYLESHEET = """
QLabel { QLabel {
background-color: {{ color['hints.bg'] }}; background-color: {{ conf.colors.hints.bg }};
color: {{ color['hints.fg'] }}; color: {{ conf.colors.hints.fg }};
font: {{ font['hints'] }}; font: {{ conf.fonts.hints }};
border: {{ config.get('hints', 'border') }}; border: {{ conf.hints.border }};
padding-left: -3px; padding-left: -3px;
padding-right: -3px; padding-right: -3px;
} }
@ -80,7 +80,7 @@ class HintLabel(QLabel):
self.elem = elem self.elem = elem
self.setAttribute(Qt.WA_StyledBackground, True) self.setAttribute(Qt.WA_StyledBackground, True)
style.set_register_stylesheet(self) config.set_register_stylesheet(self)
self._context.tab.contents_size_changed.connect(self._move_to_elem) self._context.tab.contents_size_changed.connect(self._move_to_elem)
self._move_to_elem() self._move_to_elem()
@ -100,7 +100,7 @@ class HintLabel(QLabel):
matched: The part of the text which was typed. matched: The part of the text which was typed.
unmatched: The part of the text which was not typed yet. unmatched: The part of the text which was not typed yet.
""" """
if (config.get('hints', 'uppercase') and if (config.val.hints.uppercase and
self._context.hint_mode in ['letter', 'word']): self._context.hint_mode in ['letter', 'word']):
matched = html.escape(matched.upper()) matched = html.escape(matched.upper())
unmatched = html.escape(unmatched.upper()) unmatched = html.escape(unmatched.upper())
@ -108,7 +108,7 @@ class HintLabel(QLabel):
matched = html.escape(matched) matched = html.escape(matched)
unmatched = html.escape(unmatched) unmatched = html.escape(unmatched)
match_color = html.escape(config.get('colors', 'hints.fg.match')) match_color = html.escape(config.val.colors.hints.match.fg)
self.setText('<font color="{}">{}</font>{}'.format( self.setText('<font color="{}">{}</font>{}'.format(
match_color, matched, unmatched)) match_color, matched, unmatched))
self.adjustSize() self.adjustSize()
@ -121,7 +121,7 @@ class HintLabel(QLabel):
log.hints.debug("Frame for {!r} vanished!".format(self)) log.hints.debug("Frame for {!r} vanished!".format(self))
self.hide() self.hide()
return return
no_js = config.get('hints', 'find-implementation') != 'javascript' no_js = config.val.hints.find_implementation != 'javascript'
rect = self.elem.rect_on_view(no_js=no_js) rect = self.elem.rect_on_view(no_js=no_js)
self.move(rect.x(), rect.y()) self.move(rect.x(), rect.y())
@ -203,7 +203,7 @@ class HintActions:
Target.window: usertypes.ClickTarget.window, Target.window: usertypes.ClickTarget.window,
Target.hover: usertypes.ClickTarget.normal, Target.hover: usertypes.ClickTarget.normal,
} }
if config.get('tabs', 'background-tabs'): if config.val.tabs.background:
target_mapping[Target.tab] = usertypes.ClickTarget.tab_bg target_mapping[Target.tab] = usertypes.ClickTarget.tab_bg
else: else:
target_mapping[Target.tab] = usertypes.ClickTarget.tab target_mapping[Target.tab] = usertypes.ClickTarget.tab
@ -421,9 +421,9 @@ class HintManager(QObject):
if hint_mode == 'number': if hint_mode == 'number':
chars = '0123456789' chars = '0123456789'
else: else:
chars = config.get('hints', 'chars') chars = config.val.hints.chars
min_chars = config.get('hints', 'min-chars') min_chars = config.val.hints.min_chars
if config.get('hints', 'scatter') and hint_mode != 'number': if config.val.hints.scatter and hint_mode != 'number':
return self._hint_scattered(min_chars, chars, elems) return self._hint_scattered(min_chars, chars, elems)
else: else:
return self._hint_linear(min_chars, chars, elems) return self._hint_linear(min_chars, chars, elems)
@ -603,7 +603,7 @@ class HintManager(QObject):
modeman.enter(self._win_id, usertypes.KeyMode.hint, modeman.enter(self._win_id, usertypes.KeyMode.hint,
'HintManager.start') 'HintManager.start')
# to make auto-follow == 'always' work # to make auto_follow == 'always' work
self._handle_auto_follow() self._handle_auto_follow()
@cmdutils.register(instance='hintmanager', scope='tab', name='hint', @cmdutils.register(instance='hintmanager', scope='tab', name='hint',
@ -615,7 +615,7 @@ class HintManager(QObject):
Args: Args:
rapid: Whether to do rapid hinting. This is only possible with rapid: Whether to do rapid hinting. This is only possible with
targets `tab` (with background-tabs=true), `tab-bg`, targets `tab` (with `tabs.background_tabs=true`), `tab-bg`,
`window`, `run`, `hover`, `userscript` and `spawn`. `window`, `run`, `hover`, `userscript` and `spawn`.
add_history: Whether to add the spawned or yanked link to the add_history: Whether to add the spawned or yanked link to the
browsing history. browsing history.
@ -631,7 +631,7 @@ class HintManager(QObject):
- `normal`: Open the link. - `normal`: Open the link.
- `current`: Open the link in the current tab. - `current`: Open the link in the current tab.
- `tab`: Open the link in a new tab (honoring the - `tab`: Open the link in a new tab (honoring the
background-tabs setting). `tabs.background_tabs` setting).
- `tab-fg`: Open the link in a new foreground tab. - `tab-fg`: Open the link in a new foreground tab.
- `tab-bg`: Open the link in a new background tab. - `tab-bg`: Open the link in a new background tab.
- `window`: Open the link in a new window. - `window`: Open the link in a new window.
@ -649,7 +649,7 @@ class HintManager(QObject):
mode: The hinting mode to use. mode: The hinting mode to use.
- `number`: Use numeric hints. - `number`: Use numeric hints.
- `letter`: Use the chars in the hints->chars settings. - `letter`: Use the chars in the hints.chars setting.
- `word`: Use hint words based on the html elements and the - `word`: Use hint words based on the html elements and the
extra words. extra words.
@ -684,8 +684,7 @@ class HintManager(QObject):
Target.hover, Target.userscript, Target.spawn, Target.hover, Target.userscript, Target.spawn,
Target.download, Target.normal, Target.current]: Target.download, Target.normal, Target.current]:
pass pass
elif (target == Target.tab and elif target == Target.tab and config.val.tabs.background:
config.get('tabs', 'background-tabs')):
pass pass
else: else:
name = target.name.replace('_', '-') name = target.name.replace('_', '-')
@ -693,7 +692,7 @@ class HintManager(QObject):
"target {}!".format(name)) "target {}!".format(name))
if mode is None: if mode is None:
mode = config.get('hints', 'mode') mode = config.val.hints.mode
self._check_args(target, *args) self._check_args(target, *args)
self._context = HintContext() self._context = HintContext()
@ -720,7 +719,7 @@ class HintManager(QObject):
return self._context.hint_mode return self._context.hint_mode
def _handle_auto_follow(self, keystr="", filterstr="", visible=None): def _handle_auto_follow(self, keystr="", filterstr="", visible=None):
"""Handle the auto-follow option.""" """Handle the auto_follow option."""
if visible is None: if visible is None:
visible = {string: label visible = {string: label
for string, label in self._context.labels.items() for string, label in self._context.labels.items()
@ -729,7 +728,7 @@ class HintManager(QObject):
if len(visible) != 1: if len(visible) != 1:
return return
auto_follow = config.get('hints', 'auto-follow') auto_follow = config.val.hints.auto_follow
if auto_follow == "always": if auto_follow == "always":
follow = True follow = True
@ -746,8 +745,8 @@ class HintManager(QObject):
self._context.to_follow = list(visible.keys())[0] self._context.to_follow = list(visible.keys())[0]
if follow: if follow:
# apply auto-follow-timeout # apply auto_follow_timeout
timeout = config.get('hints', 'auto-follow-timeout') timeout = config.val.hints.auto_follow_timeout
keyparsers = objreg.get('keyparsers', scope='window', keyparsers = objreg.get('keyparsers', scope='window',
window=self._win_id) window=self._win_id)
normal_parser = keyparsers[usertypes.KeyMode.normal] normal_parser = keyparsers[usertypes.KeyMode.normal]
@ -771,9 +770,9 @@ class HintManager(QObject):
label.show() label.show()
else: else:
# element doesn't match anymore -> hide it, unless in rapid # element doesn't match anymore -> hide it, unless in rapid
# mode and hide-unmatched-rapid-hints is false (see #1799) # mode and hide_unmatched_rapid_hints is false (see #1799)
if (not self._context.rapid or if (not self._context.rapid or
config.get('hints', 'hide-unmatched-rapid-hints')): config.val.hints.hide_unmatched_rapid_hints):
label.hide() label.hide()
except webelem.Error: except webelem.Error:
pass pass
@ -793,6 +792,8 @@ class HintManager(QObject):
else: else:
self._context.filterstr = filterstr self._context.filterstr = filterstr
log.hints.debug("Filtering hints on {!r}".format(filterstr))
visible = [] visible = []
for label in self._context.all_labels: for label in self._context.all_labels:
try: try:
@ -938,7 +939,7 @@ class WordHinter:
def ensure_initialized(self): def ensure_initialized(self):
"""Generate the used words if yet uninitialized.""" """Generate the used words if yet uninitialized."""
dictionary = config.get("hints", "dictionary") dictionary = config.val.hints.dictionary
if not self.words or self.dictionary != dictionary: if not self.words or self.dictionary != dictionary:
self.words.clear() self.words.clear()
self.dictionary = dictionary self.dictionary = dictionary

View File

@ -85,7 +85,7 @@ class MouseEventFilter(QObject):
def _handle_mouse_press(self, e): def _handle_mouse_press(self, e):
"""Handle pressing of a mouse button.""" """Handle pressing of a mouse button."""
is_rocker_gesture = (config.get('input', 'rocker-gestures') and is_rocker_gesture = (config.val.input.rocker_gestures and
e.buttons() == Qt.LeftButton | Qt.RightButton) 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] or is_rocker_gesture:
@ -119,7 +119,7 @@ class MouseEventFilter(QObject):
return True return True
if e.modifiers() & Qt.ControlModifier: if e.modifiers() & Qt.ControlModifier:
divider = config.get('input', 'mouse-zoom-divider') divider = config.val.zoom.mouse_divider
if divider == 0: if divider == 0:
return False return False
factor = self._tab.zoom.factor() + (e.angleDelta().y() / divider) factor = self._tab.zoom.factor() + (e.angleDelta().y() / divider)
@ -139,7 +139,7 @@ class MouseEventFilter(QObject):
def _handle_context_menu(self, _e): def _handle_context_menu(self, _e):
"""Suppress context menus if rocker gestures are turned on.""" """Suppress context menus if rocker gestures are turned on."""
return config.get('input', 'rocker-gestures') return config.val.input.rocker_gestures
def _mousepress_insertmode_cb(self, elem): def _mousepress_insertmode_cb(self, elem):
"""Check if the clicked element is editable.""" """Check if the clicked element is editable."""
@ -157,7 +157,7 @@ class MouseEventFilter(QObject):
'click', only_if_normal=True) 'click', only_if_normal=True)
else: else:
log.mouse.debug("Clicked non-editable element!") log.mouse.debug("Clicked non-editable element!")
if config.get('input', 'auto-leave-insert-mode'): if config.val.input.insert_mode.auto_leave:
modeman.leave(self._tab.win_id, usertypes.KeyMode.insert, modeman.leave(self._tab.win_id, usertypes.KeyMode.insert,
'click', maybe=True) 'click', maybe=True)
@ -179,7 +179,7 @@ class MouseEventFilter(QObject):
'click-delayed', only_if_normal=True) 'click-delayed', only_if_normal=True)
else: else:
log.mouse.debug("Clicked non-editable element (delayed)!") log.mouse.debug("Clicked non-editable element (delayed)!")
if config.get('input', 'auto-leave-insert-mode'): if config.val.input.insert_mode.auto_leave:
modeman.leave(self._tab.win_id, usertypes.KeyMode.insert, modeman.leave(self._tab.win_id, usertypes.KeyMode.insert,
'click-delayed', maybe=True) 'click-delayed', maybe=True)

View File

@ -42,7 +42,7 @@ def incdec(url, count, inc_or_dec):
background: Open the link in a new background tab. background: Open the link in a new background tab.
window: Open the link in a new window. window: Open the link in a new window.
""" """
segments = set(config.get('general', 'url-incdec-segments')) segments = set(config.val.url.incdec_segments)
try: try:
new_url = urlutils.incdec_number(url, inc_or_dec, count, new_url = urlutils.incdec_number(url, inc_or_dec, count,
segments=segments) segments=segments)
@ -80,10 +80,13 @@ def _find_prevnext(prev, elems):
# Then check for regular links/buttons. # Then check for regular links/buttons.
elems = [e for e in elems if e.tag_name() != 'link'] elems = [e for e in elems if e.tag_name() != 'link']
option = 'prev-regexes' if prev else 'next-regexes' option = 'prev_regexes' if prev else 'next_regexes'
if not elems: if not elems:
return None return None
for regex in config.get('hints', option):
# pylint: disable=bad-config-option
for regex in getattr(config.val.hints, option):
# pylint: enable=bad-config-option
log.hints.vdebug("== Checking regex '{}'.".format(regex.pattern)) log.hints.vdebug("== Checking regex '{}'.".format(regex.pattern))
for e in elems: for e in elems:
text = str(e) text = str(e)

View File

@ -247,10 +247,21 @@ class PACFetcher(QObject):
self._pac_url = url self._pac_url = url
self._manager = QNetworkAccessManager() self._manager = QNetworkAccessManager()
self._manager.setProxy(QNetworkProxy(QNetworkProxy.NoProxy)) self._manager.setProxy(QNetworkProxy(QNetworkProxy.NoProxy))
self._reply = self._manager.get(QNetworkRequest(url))
self._reply.finished.connect(self._finish)
self._pac = None self._pac = None
self._error_message = None self._error_message = None
self._reply = None
def __eq__(self, other):
# pylint: disable=protected-access
return self._pac_url == other._pac_url
def __repr__(self):
return utils.get_repr(self, url=self._pac_url, constructor=True)
def fetch(self):
"""Fetch the proxy from the remote URL."""
self._reply = self._manager.get(QNetworkRequest(self._pac_url))
self._reply.finished.connect(self._finish)
@pyqtSlot() @pyqtSlot()
def _finish(self): def _finish(self):

View File

@ -44,7 +44,7 @@ class ProxyFactory(QNetworkProxyFactory):
Return: Return:
None if proxy is correct, otherwise an error message. None if proxy is correct, otherwise an error message.
""" """
proxy = config.get('network', 'proxy') proxy = config.val.content.proxy
if isinstance(proxy, pac.PACFetcher): if isinstance(proxy, pac.PACFetcher):
return proxy.fetch_error() return proxy.fetch_error()
else: else:
@ -59,7 +59,7 @@ class ProxyFactory(QNetworkProxyFactory):
Return: Return:
A list of QNetworkProxy objects in order of preference. A list of QNetworkProxy objects in order of preference.
""" """
proxy = config.get('network', 'proxy') proxy = config.val.content.proxy
if proxy is configtypes.SYSTEM_PROXY: if proxy is configtypes.SYSTEM_PROXY:
proxies = QNetworkProxyFactory.systemProxyForQuery(query) proxies = QNetworkProxyFactory.systemProxyForQuery(query)
elif isinstance(proxy, pac.PACFetcher): elif isinstance(proxy, pac.PACFetcher):
@ -69,7 +69,7 @@ class ProxyFactory(QNetworkProxyFactory):
for p in proxies: for p in proxies:
if p.type() != QNetworkProxy.NoProxy: if p.type() != QNetworkProxy.NoProxy:
capabilities = p.capabilities() capabilities = p.capabilities()
if config.get('network', 'proxy-dns-requests'): if config.val.content.proxy_dns_requests:
capabilities |= QNetworkProxy.HostNameLookupCapability capabilities |= QNetworkProxy.HostNameLookupCapability
else: else:
capabilities &= ~QNetworkProxy.HostNameLookupCapability capabilities &= ~QNetworkProxy.HostNameLookupCapability

View File

@ -368,7 +368,7 @@ class DownloadManager(downloads.AbstractDownloadManager):
super().__init__(parent) super().__init__(parent)
self._networkmanager = networkmanager.NetworkManager( self._networkmanager = networkmanager.NetworkManager(
win_id=win_id, tab_id=None, win_id=win_id, tab_id=None,
private=config.get('general', 'private-browsing'), parent=self) private=config.val.content.private_browsing, parent=self)
@pyqtSlot('QUrl') @pyqtSlot('QUrl')
def get(self, url, *, user_agent=None, **kwargs): def get(self, url, *, user_agent=None, **kwargs):
@ -483,7 +483,7 @@ class DownloadManager(downloads.AbstractDownloadManager):
reply: The QNetworkReply to download. reply: The QNetworkReply to download.
target: Where to save the download as downloads.DownloadTarget. target: Where to save the download as downloads.DownloadTarget.
auto_remove: Whether to remove the download even if auto_remove: Whether to remove the download even if
ui -> remove-finished-downloads is set to -1. downloads.remove_finished is set to -1.
Return: Return:
The created DownloadItem. The created DownloadItem.

View File

@ -29,12 +29,13 @@ import os
import time import time
import urllib.parse import urllib.parse
import datetime import datetime
import textwrap
import pkg_resources import pkg_resources
from PyQt5.QtCore import QUrlQuery, QUrl from PyQt5.QtCore import QUrlQuery, QUrl
import qutebrowser import qutebrowser
from qutebrowser.config import config from qutebrowser.config import config, configdata, configexc, configdiff
from qutebrowser.utils import (version, utils, jinja, log, message, docutils, from qutebrowser.utils import (version, utils, jinja, log, message, docutils,
objreg, usertypes, qtutils) objreg, usertypes, qtutils)
from qutebrowser.misc import objects from qutebrowser.misc import objects
@ -122,8 +123,7 @@ class add_handler: # pylint: disable=invalid-name
title="Error while opening qute://url", title="Error while opening qute://url",
url=url.toDisplayString(), url=url.toDisplayString(),
error='{} is not available with this ' error='{} is not available with this '
'backend'.format(url.toDisplayString()), 'backend'.format(url.toDisplayString()))
icon='')
return 'text/html', html return 'text/html', html
@ -225,14 +225,14 @@ def qute_history(url):
return 'text/html', json.dumps(history_data(start_time, offset)) return 'text/html', json.dumps(history_data(start_time, offset))
else: else:
if ( if (
config.get('content', 'allow-javascript') and config.val.content.javascript.enabled and
(objects.backend == usertypes.Backend.QtWebEngine or (objects.backend == usertypes.Backend.QtWebEngine or
qtutils.is_qtwebkit_ng()) qtutils.is_qtwebkit_ng())
): ):
return 'text/html', jinja.render( return 'text/html', jinja.render(
'history.html', 'history.html',
title='History', title='History',
session_interval=config.get('ui', 'history-session-interval') gap_interval=config.val.history_gap_interval
) )
else: else:
# Get current date from query parameter, if not given choose today. # Get current date from query parameter, if not given choose today.
@ -351,20 +351,6 @@ def qute_gpl(_url):
@add_handler('help') @add_handler('help')
def qute_help(url): def qute_help(url):
"""Handler for qute://help.""" """Handler for qute://help."""
try:
utils.read_file('html/doc/index.html')
except OSError:
html = jinja.render(
'error.html',
title="Error while loading documentation",
url=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. "
"If you're running a released version this is a bug, please "
"use :report to report it.",
icon='')
return 'text/html', html
urlpath = url.path() urlpath = url.path()
if not urlpath or urlpath == '/': if not urlpath or urlpath == '/':
urlpath = 'index.html' urlpath = 'index.html'
@ -373,11 +359,45 @@ def qute_help(url):
if not docutils.docs_up_to_date(urlpath): if not docutils.docs_up_to_date(urlpath):
message.error("Your documentation is outdated! Please re-run " message.error("Your documentation is outdated! Please re-run "
"scripts/asciidoc2html.py.") "scripts/asciidoc2html.py.")
path = 'html/doc/{}'.format(urlpath) path = 'html/doc/{}'.format(urlpath)
if urlpath.endswith('.png'): if urlpath.endswith('.png'):
return 'image/png', utils.read_file(path, binary=True) return 'image/png', utils.read_file(path, binary=True)
else:
try:
data = utils.read_file(path) data = utils.read_file(path)
except OSError:
# No .html around, let's see if we find the asciidoc
asciidoc_path = path.replace('.html', '.asciidoc')
if asciidoc_path.startswith('html/doc/'):
asciidoc_path = asciidoc_path.replace('html/doc/', '../doc/help/')
try:
asciidoc = utils.read_file(asciidoc_path)
except OSError:
asciidoc = None
if asciidoc is None:
raise
preamble = textwrap.dedent("""
There was an error loading the documentation!
This most likely means the documentation was not generated
properly. If you are running qutebrowser from the git repository,
please (re)run scripts/asciidoc2html.py and reload this page.
If you're running a released version this is a bug, please use
:report to report it.
Falling back to the plaintext version.
---------------------------------------------------------------
""")
return 'text/plain', (preamble + asciidoc).encode('utf-8')
else:
return 'text/html', data return 'text/html', data
@ -390,3 +410,47 @@ def qute_backend_warning(_url):
version=pkg_resources.parse_version, version=pkg_resources.parse_version,
title="Legacy backend warning") title="Legacy backend warning")
return 'text/html', html return 'text/html', html
def _qute_settings_set(url):
"""Handler for qute://settings/set."""
query = QUrlQuery(url)
option = query.queryItemValue('option', QUrl.FullyDecoded)
value = query.queryItemValue('value', QUrl.FullyDecoded)
# https://github.com/qutebrowser/qutebrowser/issues/727
if option == 'content.javascript.enabled' and value == 'false':
msg = ("Refusing to disable javascript via qute://settings "
"as it needs javascript support.")
message.error(msg)
return 'text/html', b'error: ' + msg.encode('utf-8')
try:
config.instance.set_str(option, value, save_yaml=True)
return 'text/html', b'ok'
except configexc.Error as e:
message.error(str(e))
return 'text/html', b'error: ' + str(e).encode('utf-8')
@add_handler('settings')
def qute_settings(url):
"""Handler for qute://settings. View/change qute configuration."""
if url.path() == '/set':
return _qute_settings_set(url)
html = jinja.render('settings.html', title='settings',
configdata=configdata,
confget=config.instance.get_str)
return 'text/html', html
@add_handler('configdiff')
def qute_configdiff(_url):
"""Handler for qute://configdiff."""
try:
return 'text/html', configdiff.get_diff()
except OSError as e:
error = (b'Failed to read old config: ' +
str(e.strerror).encode('utf-8'))
return 'text/plain', error

View File

@ -21,10 +21,8 @@
import html import html
import jinja2
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import usertypes, message, log, objreg from qutebrowser.utils import usertypes, message, log, objreg, jinja
class CallSuper(Exception): class CallSuper(Exception):
@ -35,16 +33,18 @@ class CallSuper(Exception):
def custom_headers(): def custom_headers():
"""Get the combined custom headers.""" """Get the combined custom headers."""
headers = {} headers = {}
dnt = b'1' if config.get('network', 'do-not-track') else b'0'
headers[b'DNT'] = dnt
headers[b'X-Do-Not-Track'] = dnt
config_headers = config.get('network', 'custom-headers') dnt_config = config.val.content.headers.do_not_track
if config_headers is not None: if dnt_config is not None:
for header, value in config_headers.items(): dnt = b'1' if dnt_config else b'0'
headers[header.encode('ascii')] = value.encode('ascii') headers[b'DNT'] = dnt
headers[b'X-Do-Not-Track'] = dnt
accept_language = config.get('network', 'accept-language') conf_headers = config.val.content.headers.custom
for header, value in conf_headers.items():
headers[header.encode('ascii')] = value.encode('ascii')
accept_language = config.val.content.headers.accept_language
if accept_language is not None: if accept_language is not None:
headers[b'Accept-Language'] = accept_language.encode('ascii') headers[b'Accept-Language'] = accept_language.encode('ascii')
@ -72,7 +72,7 @@ def authentication_required(url, authenticator, abort_on):
def javascript_confirm(url, js_msg, abort_on): def javascript_confirm(url, js_msg, abort_on):
"""Display a javascript confirm prompt.""" """Display a javascript confirm prompt."""
log.js.debug("confirm: {}".format(js_msg)) log.js.debug("confirm: {}".format(js_msg))
if config.get('ui', 'modal-js-dialog'): if config.val.content.javascript.modal_dialog:
raise CallSuper raise CallSuper
msg = 'From <b>{}</b>:<br/>{}'.format(html.escape(url.toDisplayString()), msg = 'From <b>{}</b>:<br/>{}'.format(html.escape(url.toDisplayString()),
@ -86,9 +86,9 @@ def javascript_confirm(url, js_msg, abort_on):
def javascript_prompt(url, js_msg, default, abort_on): def javascript_prompt(url, js_msg, default, abort_on):
"""Display a javascript prompt.""" """Display a javascript prompt."""
log.js.debug("prompt: {}".format(js_msg)) log.js.debug("prompt: {}".format(js_msg))
if config.get('ui', 'modal-js-dialog'): if config.val.content.javascript.modal_dialog:
raise CallSuper raise CallSuper
if config.get('content', 'ignore-javascript-prompt'): if not config.val.content.javascript.prompt:
return (False, "") return (False, "")
msg = '<b>{}</b> asks:<br/>{}'.format(html.escape(url.toDisplayString()), msg = '<b>{}</b> asks:<br/>{}'.format(html.escape(url.toDisplayString()),
@ -107,10 +107,10 @@ def javascript_prompt(url, js_msg, default, abort_on):
def javascript_alert(url, js_msg, abort_on): def javascript_alert(url, js_msg, abort_on):
"""Display a javascript alert.""" """Display a javascript alert."""
log.js.debug("alert: {}".format(js_msg)) log.js.debug("alert: {}".format(js_msg))
if config.get('ui', 'modal-js-dialog'): if config.val.content.javascript.modal_dialog:
raise CallSuper raise CallSuper
if config.get('content', 'ignore-javascript-alert'): if not config.val.content.javascript.alert:
return return
msg = 'From <b>{}</b>:<br/>{}'.format(html.escape(url.toDisplayString()), msg = 'From <b>{}</b>:<br/>{}'.format(html.escape(url.toDisplayString()),
@ -119,6 +119,22 @@ def javascript_alert(url, js_msg, abort_on):
abort_on=abort_on) abort_on=abort_on)
def javascript_log_message(level, source, line, msg):
"""Display a JavaScript log message."""
logstring = "[{}:{}] {}".format(source, line, msg)
# Needs to line up with the values allowed for the
# content.javascript.log setting.
logmap = {
'none': lambda arg: None,
'debug': log.js.debug,
'info': log.js.info,
'warning': log.js.warning,
'error': log.js.error,
}
logger = logmap[config.val.content.javascript.log[level.name]]
logger(logstring)
def ignore_certificate_errors(url, errors, abort_on): def ignore_certificate_errors(url, errors, abort_on):
"""Display a certificate error question. """Display a certificate error question.
@ -129,7 +145,7 @@ def ignore_certificate_errors(url, errors, abort_on):
Return: Return:
True if the error should be ignored, False otherwise. True if the error should be ignored, False otherwise.
""" """
ssl_strict = config.get('network', 'ssl-strict') ssl_strict = config.val.content.ssl_strict
log.webview.debug("Certificate errors {!r}, strict {}".format( log.webview.debug("Certificate errors {!r}, strict {}".format(
errors, ssl_strict)) errors, ssl_strict))
@ -137,7 +153,7 @@ def ignore_certificate_errors(url, errors, abort_on):
assert error.is_overridable(), repr(error) assert error.is_overridable(), repr(error)
if ssl_strict == 'ask': if ssl_strict == 'ask':
err_template = jinja2.Template(""" err_template = jinja.environment.from_string("""
Errors while loading <b>{{url.toDisplayString()}}</b>:<br/> Errors while loading <b>{{url.toDisplayString()}}</b>:<br/>
<ul> <ul>
{% for err in errors %} {% for err in errors %}
@ -155,7 +171,7 @@ def ignore_certificate_errors(url, errors, abort_on):
ignore = False ignore = False
return ignore return ignore
elif ssl_strict is False: elif ssl_strict is False:
log.webview.debug("ssl-strict is False, only warning about errors") log.webview.debug("ssl_strict is False, only warning about errors")
for err in errors: for err in errors:
# FIXME we might want to use warn here (non-fatal error) # FIXME we might want to use warn here (non-fatal error)
# https://github.com/qutebrowser/qutebrowser/issues/114 # https://github.com/qutebrowser/qutebrowser/issues/114
@ -173,7 +189,7 @@ def feature_permission(url, option, msg, yes_action, no_action, abort_on):
Args: Args:
url: The URL the request was done for. url: The URL the request was done for.
option: A (section, option) tuple for the option to check. option: An option name to check.
msg: A string like "show notifications" msg: A string like "show notifications"
yes_action: A callable to call if the request was approved yes_action: A callable to call if the request was approved
no_action: A callable to call if the request was denied no_action: A callable to call if the request was denied
@ -182,7 +198,7 @@ def feature_permission(url, option, msg, yes_action, no_action, abort_on):
Return: Return:
The Question object if a question was asked, None otherwise. The Question object if a question was asked, None otherwise.
""" """
config_val = config.get(*option) config_val = config.instance.get(option)
if config_val == 'ask': if config_val == 'ask':
if url.isValid(): if url.isValid():
text = "Allow the website at <b>{}</b> to {}?".format( text = "Allow the website at <b>{}</b> to {}?".format(
@ -233,15 +249,14 @@ def get_tab(win_id, target):
def get_user_stylesheet(): def get_user_stylesheet():
"""Get the combined user-stylesheet.""" """Get the combined user-stylesheet."""
filename = config.get('ui', 'user-stylesheet') css = ''
stylesheets = config.val.content.user_stylesheets
if filename is None: for filename in stylesheets:
css = ''
else:
with open(filename, 'r', encoding='utf-8') as f: with open(filename, 'r', encoding='utf-8') as f:
css = f.read() css += f.read()
if config.get('ui', 'hide-scrollbar'): if not config.val.scrolling.bar:
css += '\nhtml > ::-webkit-scrollbar { width: 0px; height: 0px; }' css += '\nhtml > ::-webkit-scrollbar { width: 0px; height: 0px; }'
return css return css

View File

@ -182,7 +182,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
# at least a classid attribute. Oh, and let's hope images/... # at least a classid attribute. Oh, and let's hope images/...
# DON'T have a classid attribute. HTML sucks. # DON'T have a classid attribute. HTML sucks.
log.webelem.debug("<object type='{}'> clicked.".format(objtype)) log.webelem.debug("<object type='{}'> clicked.".format(objtype))
return config.get('input', 'insert-mode-on-plugins') return config.val.input.insert_mode.plugins
else: else:
# Image/Audio/... # Image/Audio/...
return False return False
@ -247,7 +247,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
return self.is_writable() return self.is_writable()
elif tag in ['embed', 'applet']: elif tag in ['embed', 'applet']:
# Flash/Java/... # Flash/Java/...
return config.get('input', 'insert-mode-on-plugins') and not strict return config.val.input.insert_mode.plugins and not strict
elif tag == 'object': elif tag == 'object':
return self._is_editable_object() and not strict return self._is_editable_object() and not strict
elif tag in ['div', 'pre']: elif tag in ['div', 'pre']:
@ -329,7 +329,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
usertypes.ClickTarget.tab: Qt.ControlModifier, usertypes.ClickTarget.tab: Qt.ControlModifier,
usertypes.ClickTarget.tab_bg: Qt.ControlModifier, usertypes.ClickTarget.tab_bg: Qt.ControlModifier,
} }
if config.get('tabs', 'background-tabs'): if config.val.tabs.background:
modifiers[usertypes.ClickTarget.tab] |= Qt.ShiftModifier modifiers[usertypes.ClickTarget.tab] |= Qt.ShiftModifier
else: else:
modifiers[usertypes.ClickTarget.tab_bg] |= Qt.ShiftModifier modifiers[usertypes.ClickTarget.tab_bg] |= Qt.ShiftModifier

View File

@ -63,6 +63,6 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor):
for header, value in shared.custom_headers(): for header, value in shared.custom_headers():
info.setHttpHeader(header, value) info.setHttpHeader(header, value)
user_agent = config.get('network', 'user-agent') user_agent = config.val.content.headers.user_agent
if user_agent is not None: if user_agent is not None:
info.setHttpHeader(b'User-Agent', user_agent.encode('ascii')) info.setHttpHeader(b'User-Agent', user_agent.encode('ascii'))

View File

@ -162,7 +162,7 @@ class WebEngineElement(webelem.AbstractWebElement):
top = rect['top'] top = rect['top']
if width > 1 and height > 1: if width > 1 and height > 1:
# Fix coordinates according to zoom level # Fix coordinates according to zoom level
# We're not checking for zoom-text-only here as that doesn't # We're not checking for zoom.text_only here as that doesn't
# exist for QtWebEngine. # exist for QtWebEngine.
zoom = self._tab.zoom.factor() zoom = self._tab.zoom.factor()
rect = QRect(left * zoom, top * zoom, rect = QRect(left * zoom, top * zoom,

View File

@ -38,7 +38,7 @@ from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile,
from qutebrowser.browser import shared from qutebrowser.browser import shared
from qutebrowser.config import config, websettings from qutebrowser.config import config, websettings
from qutebrowser.utils import objreg, utils, standarddir, javascript, qtutils from qutebrowser.utils import utils, standarddir, javascript, qtutils
# The default QWebEngineProfile # The default QWebEngineProfile
@ -112,7 +112,7 @@ class DefaultProfileSetter(websettings.Base):
class PersistentCookiePolicy(DefaultProfileSetter): class PersistentCookiePolicy(DefaultProfileSetter):
"""The cookies -> store setting is different from other settings.""" """The content.cookies.store setting is different from other settings."""
def __init__(self): def __init__(self):
super().__init__('setPersistentCookiesPolicy') super().__init__('setPersistentCookiesPolicy')
@ -158,26 +158,29 @@ def _init_stylesheet(profile):
profile.scripts().insert(script) profile.scripts().insert(script)
def _set_user_agent(profile): def _set_http_headers(profile):
"""Set the user agent for the given profile. """Set the user agent and accept-language for the given profile.
We override this per request in the URL interceptor (to allow for We override those per request in the URL interceptor (to allow for
per-domain user agents), but this one still gets used for things like per-domain values), but this one still gets used for things like
window.navigator.userAgent in JS. window.navigator.userAgent/.languages in JS.
""" """
user_agent = config.get('network', 'user-agent') profile.setHttpUserAgent(config.val.content.headers.user_agent)
profile.setHttpUserAgent(user_agent) accept_language = config.val.content.headers.accept_language
if accept_language is not None:
profile.setHttpAcceptLanguage(accept_language)
def update_settings(section, option): def _update_settings(option):
"""Update global settings when qwebsettings changed.""" """Update global settings when qwebsettings changed."""
websettings.update_mappings(MAPPINGS, section, option) websettings.update_mappings(MAPPINGS, option)
if section == 'ui' and option in ['hide-scrollbar', 'user-stylesheet']: if option in ['scrollbar.hide', 'content.user_stylesheets']:
_init_stylesheet(default_profile) _init_stylesheet(default_profile)
_init_stylesheet(private_profile) _init_stylesheet(private_profile)
elif section == 'network' and option == 'user-agent': elif option in ['content.headers.user_agent',
_set_user_agent(default_profile) 'content.headers.accept_language']:
_set_user_agent(private_profile) _set_http_headers(default_profile)
_set_http_headers(private_profile)
def _init_profiles(): def _init_profiles():
@ -189,12 +192,12 @@ def _init_profiles():
default_profile.setPersistentStoragePath( default_profile.setPersistentStoragePath(
os.path.join(standarddir.data(), 'webengine')) os.path.join(standarddir.data(), 'webengine'))
_init_stylesheet(default_profile) _init_stylesheet(default_profile)
_set_user_agent(default_profile) _set_http_headers(default_profile)
private_profile = QWebEngineProfile() private_profile = QWebEngineProfile()
assert private_profile.isOffTheRecord() assert private_profile.isOffTheRecord()
_init_stylesheet(private_profile) _init_stylesheet(private_profile)
_set_user_agent(private_profile) _set_http_headers(private_profile)
def init(args): def init(args):
@ -212,11 +215,11 @@ def init(args):
# We need to do this here as a WORKAROUND for # We need to do this here as a WORKAROUND for
# https://bugreports.qt.io/browse/QTBUG-58650 # https://bugreports.qt.io/browse/QTBUG-58650
if not qtutils.version_check('5.9'): if not qtutils.version_check('5.9'):
PersistentCookiePolicy().set(config.get('content', 'cookies-store')) PersistentCookiePolicy().set(config.val.content.cookies.store)
Attribute(QWebEngineSettings.FullScreenSupportEnabled).set(True) Attribute(QWebEngineSettings.FullScreenSupportEnabled).set(True)
websettings.init_mappings(MAPPINGS) websettings.init_mappings(MAPPINGS)
objreg.get('config').changed.connect(update_settings) config.instance.changed.connect(_update_settings)
def shutdown(): def shutdown():
@ -237,79 +240,70 @@ def shutdown():
MAPPINGS = { MAPPINGS = {
'content': { 'content.images':
'allow-images': Attribute(QWebEngineSettings.AutoLoadImages),
Attribute(QWebEngineSettings.AutoLoadImages), 'content.javascript.enabled':
'allow-javascript': Attribute(QWebEngineSettings.JavascriptEnabled),
Attribute(QWebEngineSettings.JavascriptEnabled), 'content.javascript.can_open_tabs_automatically':
'javascript-can-open-windows-automatically': Attribute(QWebEngineSettings.JavascriptCanOpenWindows),
Attribute(QWebEngineSettings.JavascriptCanOpenWindows), 'content.javascript.can_access_clipboard':
'javascript-can-access-clipboard': Attribute(QWebEngineSettings.JavascriptCanAccessClipboard),
Attribute(QWebEngineSettings.JavascriptCanAccessClipboard), 'content.plugins':
'allow-plugins': Attribute(QWebEngineSettings.PluginsEnabled),
Attribute(QWebEngineSettings.PluginsEnabled), 'content.hyperlink_auditing':
'hyperlink-auditing': Attribute(QWebEngineSettings.HyperlinkAuditingEnabled),
Attribute(QWebEngineSettings.HyperlinkAuditingEnabled), 'content.local_content_can_access_remote_urls':
'local-content-can-access-remote-urls': Attribute(QWebEngineSettings.LocalContentCanAccessRemoteUrls),
Attribute(QWebEngineSettings.LocalContentCanAccessRemoteUrls), 'content.local_content_can_access_file_urls':
'local-content-can-access-file-urls': Attribute(QWebEngineSettings.LocalContentCanAccessFileUrls),
Attribute(QWebEngineSettings.LocalContentCanAccessFileUrls), 'content.webgl':
'webgl': Attribute(QWebEngineSettings.WebGLEnabled),
Attribute(QWebEngineSettings.WebGLEnabled), 'content.local_storage':
}, Attribute(QWebEngineSettings.LocalStorageEnabled),
'input': { 'content.cache.size':
'spatial-navigation': # 0: automatically managed by QtWebEngine
Attribute(QWebEngineSettings.SpatialNavigationEnabled), DefaultProfileSetter('setHttpCacheMaximumSize', default=0),
'links-included-in-focus-chain': 'content.xss_auditing':
Attribute(QWebEngineSettings.LinksIncludedInFocusChain), Attribute(QWebEngineSettings.XSSAuditingEnabled),
}, 'content.default_encoding':
'fonts': { Setter(QWebEngineSettings.setDefaultTextEncoding),
'web-family-standard':
FontFamilySetter(QWebEngineSettings.StandardFont), 'input.spatial_navigation':
'web-family-fixed': Attribute(QWebEngineSettings.SpatialNavigationEnabled),
FontFamilySetter(QWebEngineSettings.FixedFont), 'input.links_included_in_focus_chain':
'web-family-serif': Attribute(QWebEngineSettings.LinksIncludedInFocusChain),
FontFamilySetter(QWebEngineSettings.SerifFont),
'web-family-sans-serif': 'fonts.web.family.standard':
FontFamilySetter(QWebEngineSettings.SansSerifFont), FontFamilySetter(QWebEngineSettings.StandardFont),
'web-family-cursive': 'fonts.web.family.fixed':
FontFamilySetter(QWebEngineSettings.CursiveFont), FontFamilySetter(QWebEngineSettings.FixedFont),
'web-family-fantasy': 'fonts.web.family.serif':
FontFamilySetter(QWebEngineSettings.FantasyFont), FontFamilySetter(QWebEngineSettings.SerifFont),
'web-size-minimum': 'fonts.web.family.sans_serif':
Setter(QWebEngineSettings.setFontSize, FontFamilySetter(QWebEngineSettings.SansSerifFont),
args=[QWebEngineSettings.MinimumFontSize]), 'fonts.web.family.cursive':
'web-size-minimum-logical': FontFamilySetter(QWebEngineSettings.CursiveFont),
Setter(QWebEngineSettings.setFontSize, 'fonts.web.family.fantasy':
args=[QWebEngineSettings.MinimumLogicalFontSize]), FontFamilySetter(QWebEngineSettings.FantasyFont),
'web-size-default': 'fonts.web.size.minimum':
Setter(QWebEngineSettings.setFontSize, Setter(QWebEngineSettings.setFontSize,
args=[QWebEngineSettings.DefaultFontSize]), args=[QWebEngineSettings.MinimumFontSize]),
'web-size-default-fixed': 'fonts.web.size.minimum_logical':
Setter(QWebEngineSettings.setFontSize, Setter(QWebEngineSettings.setFontSize,
args=[QWebEngineSettings.DefaultFixedFontSize]), args=[QWebEngineSettings.MinimumLogicalFontSize]),
}, 'fonts.web.size.default':
'ui': { Setter(QWebEngineSettings.setFontSize,
'smooth-scrolling': args=[QWebEngineSettings.DefaultFontSize]),
Attribute(QWebEngineSettings.ScrollAnimatorEnabled), 'fonts.web.size.default_fixed':
}, Setter(QWebEngineSettings.setFontSize,
'storage': { args=[QWebEngineSettings.DefaultFixedFontSize]),
'local-storage':
Attribute(QWebEngineSettings.LocalStorageEnabled), 'scrolling.smooth':
'cache-size': Attribute(QWebEngineSettings.ScrollAnimatorEnabled),
# 0: automatically managed by QtWebEngine
DefaultProfileSetter('setHttpCacheMaximumSize', default=0),
},
'general': {
'xss-auditing':
Attribute(QWebEngineSettings.XSSAuditingEnabled),
'default-encoding':
Setter(QWebEngineSettings.setDefaultTextEncoding),
}
} }
try: try:
MAPPINGS['general']['print-element-backgrounds'] = Attribute( MAPPINGS['content.print_element_backgrounds'] = Attribute(
QWebEngineSettings.PrintElementBackgrounds) QWebEngineSettings.PrintElementBackgrounds)
except AttributeError: except AttributeError:
# Added in Qt 5.8 # Added in Qt 5.8
@ -318,4 +312,4 @@ except AttributeError:
if qtutils.version_check('5.9'): if qtutils.version_check('5.9'):
# https://bugreports.qt.io/browse/QTBUG-58650 # https://bugreports.qt.io/browse/QTBUG-58650
MAPPINGS['content']['cookies-store'] = PersistentCookiePolicy() MAPPINGS['content.cookies.store'] = PersistentCookiePolicy()

View File

@ -153,20 +153,16 @@ class WebEngineSearch(browsertab.AbstractSearch):
callback(found) callback(found)
self._widget.findText(text, flags, wrapped_callback) self._widget.findText(text, flags, wrapped_callback)
def search(self, text, *, ignore_case=False, reverse=False, def search(self, text, *, ignore_case='never', reverse=False,
result_cb=None): result_cb=None):
flags = QWebEnginePage.FindFlags(0)
if ignore_case == 'smart':
if not text.islower():
flags |= QWebEnginePage.FindCaseSensitively
elif not ignore_case:
flags |= QWebEnginePage.FindCaseSensitively
if reverse:
flags |= QWebEnginePage.FindBackward
self.text = text self.text = text
self._flags = flags self._flags = QWebEnginePage.FindFlags(0)
self._find(text, flags, result_cb, 'search') if self._is_case_sensitive(ignore_case):
self._flags |= QWebEnginePage.FindCaseSensitively
if reverse:
self._flags |= QWebEnginePage.FindBackward
self._find(text, self._flags, result_cb, 'search')
def clear(self): def clear(self):
self.search_displayed = False self.search_displayed = False
@ -699,7 +695,7 @@ class WebEngineTab(browsertab.AbstractTab):
error_page = jinja.render( error_page = jinja.render(
'error.html', 'error.html',
title="Error loading page: {}".format(url_string), title="Error loading page: {}".format(url_string),
url=url_string, error="Authentication required", icon='') url=url_string, error="Authentication required")
self.set_html(error_page) self.set_html(error_page)
@pyqtSlot('QWebEngineFullScreenRequest') @pyqtSlot('QWebEngineFullScreenRequest')

View File

@ -28,8 +28,7 @@ from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
from qutebrowser.browser import shared from qutebrowser.browser import shared
from qutebrowser.browser.webengine import certificateerror, webenginesettings from qutebrowser.browser.webengine import certificateerror, webenginesettings
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import (log, debug, usertypes, jinja, urlutils, message, from qutebrowser.utils import log, debug, usertypes, jinja, urlutils, message
objreg)
class WebEngineView(QWebEngineView): class WebEngineView(QWebEngineView):
@ -80,10 +79,10 @@ class WebEngineView(QWebEngineView):
The new QWebEngineView object. The new QWebEngineView object.
""" """
debug_type = debug.qenum_key(QWebEnginePage, wintype) debug_type = debug.qenum_key(QWebEnginePage, wintype)
background_tabs = config.get('tabs', 'background-tabs') background = config.val.tabs.background
log.webview.debug("createWindow with type {}, background_tabs " log.webview.debug("createWindow with type {}, background {}".format(
"{}".format(debug_type, background_tabs)) debug_type, background))
if wintype == QWebEnginePage.WebBrowserWindow: if wintype == QWebEnginePage.WebBrowserWindow:
# Shift-Alt-Click # Shift-Alt-Click
@ -95,13 +94,13 @@ class WebEngineView(QWebEngineView):
elif wintype == QWebEnginePage.WebBrowserTab: elif wintype == QWebEnginePage.WebBrowserTab:
# Middle-click / Ctrl-Click with Shift # Middle-click / Ctrl-Click with Shift
# FIXME:qtwebengine this also affects target=_blank links... # FIXME:qtwebengine this also affects target=_blank links...
if background_tabs: if background:
target = usertypes.ClickTarget.tab target = usertypes.ClickTarget.tab
else: else:
target = usertypes.ClickTarget.tab_bg target = usertypes.ClickTarget.tab_bg
elif wintype == QWebEnginePage.WebBrowserBackgroundTab: elif wintype == QWebEnginePage.WebBrowserBackgroundTab:
# Middle-click / Ctrl-Click # Middle-click / Ctrl-Click
if background_tabs: if background:
target = usertypes.ClickTarget.tab_bg target = usertypes.ClickTarget.tab_bg
else: else:
target = usertypes.ClickTarget.tab target = usertypes.ClickTarget.tab
@ -135,11 +134,11 @@ class WebEnginePage(QWebEnginePage):
self._on_feature_permission_requested) self._on_feature_permission_requested)
self._theme_color = theme_color self._theme_color = theme_color
self._set_bg_color() self._set_bg_color()
objreg.get('config').changed.connect(self._set_bg_color) config.instance.changed.connect(self._set_bg_color)
@config.change_filter('colors', 'webpage.bg') @config.change_filter('colors.webpage.bg')
def _set_bg_color(self): def _set_bg_color(self):
col = config.get('colors', 'webpage.bg') col = config.val.colors.webpage.bg
if col is None: if col is None:
col = self._theme_color col = self._theme_color
self.setBackgroundColor(col) self.setBackgroundColor(col)
@ -148,11 +147,10 @@ class WebEnginePage(QWebEnginePage):
def _on_feature_permission_requested(self, url, feature): def _on_feature_permission_requested(self, url, feature):
"""Ask the user for approval for geolocation/media/etc..""" """Ask the user for approval for geolocation/media/etc.."""
options = { options = {
QWebEnginePage.Geolocation: ('content', 'geolocation'), QWebEnginePage.Geolocation: 'content.geolocation',
QWebEnginePage.MediaAudioCapture: ('content', 'media-capture'), QWebEnginePage.MediaAudioCapture: 'content.media_capture',
QWebEnginePage.MediaVideoCapture: ('content', 'media-capture'), QWebEnginePage.MediaVideoCapture: 'content.media_capture',
QWebEnginePage.MediaAudioVideoCapture: QWebEnginePage.MediaAudioVideoCapture: 'content.media_capture',
('content', 'media-capture'),
} }
messages = { messages = {
QWebEnginePage.Geolocation: 'access your location', QWebEnginePage.Geolocation: 'access your location',
@ -214,7 +212,7 @@ class WebEnginePage(QWebEnginePage):
url_string = url.toDisplayString() url_string = url.toDisplayString()
error_page = jinja.render( error_page = jinja.render(
'error.html', title="Error loading page: {}".format(url_string), 'error.html', title="Error loading page: {}".format(url_string),
url=url_string, error=str(error), icon='') url=url_string, error=str(error))
if error.is_overridable(): if error.is_overridable():
ignore = shared.ignore_certificate_errors( ignore = shared.ignore_certificate_errors(
@ -276,19 +274,12 @@ class WebEnginePage(QWebEnginePage):
def javaScriptConsoleMessage(self, level, msg, line, source): def javaScriptConsoleMessage(self, level, msg, line, source):
"""Log javascript messages to qutebrowser's log.""" """Log javascript messages to qutebrowser's log."""
# FIXME:qtwebengine maybe unify this in the tab api somehow? level_map = {
setting = config.get('general', 'log-javascript-console') QWebEnginePage.InfoMessageLevel: usertypes.JsLogLevel.info,
if setting == 'none': QWebEnginePage.WarningMessageLevel: usertypes.JsLogLevel.warning,
return QWebEnginePage.ErrorMessageLevel: usertypes.JsLogLevel.error,
level_to_logger = {
QWebEnginePage.InfoMessageLevel: log.js.info,
QWebEnginePage.WarningMessageLevel: log.js.warning,
QWebEnginePage.ErrorMessageLevel: log.js.error,
} }
logstring = "[{}:{}] {}".format(source, line, msg) shared.javascript_log_message(level_map[level], source, line, msg)
logger = level_to_logger[level]
logger(logstring)
def acceptNavigationRequest(self, def acceptNavigationRequest(self,
url: QUrl, url: QUrl,

View File

@ -24,7 +24,7 @@ import os.path
from PyQt5.QtNetwork import QNetworkDiskCache from PyQt5.QtNetwork import QNetworkDiskCache
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import utils, objreg, qtutils from qutebrowser.utils import utils, qtutils
class DiskCache(QNetworkDiskCache): class DiskCache(QNetworkDiskCache):
@ -35,17 +35,17 @@ class DiskCache(QNetworkDiskCache):
super().__init__(parent) super().__init__(parent)
self.setCacheDirectory(os.path.join(cache_dir, 'http')) self.setCacheDirectory(os.path.join(cache_dir, 'http'))
self._set_cache_size() self._set_cache_size()
objreg.get('config').changed.connect(self._set_cache_size) config.instance.changed.connect(self._set_cache_size)
def __repr__(self): def __repr__(self):
return utils.get_repr(self, size=self.cacheSize(), return utils.get_repr(self, size=self.cacheSize(),
maxsize=self.maximumCacheSize(), maxsize=self.maximumCacheSize(),
path=self.cacheDirectory()) path=self.cacheDirectory())
@config.change_filter('storage', 'cache-size') @config.change_filter('content.cache.size')
def _set_cache_size(self): def _set_cache_size(self):
"""Set the cache size based on the config.""" """Set the cache size based on the config."""
size = config.get('storage', 'cache-size') size = config.val.content.cache.size
if size is None: if size is None:
size = 1024 * 1024 * 50 # default from QNetworkDiskCachePrivate size = 1024 * 1024 * 50 # default from QNetworkDiskCachePrivate
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-59909 # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-59909

View File

@ -50,7 +50,7 @@ class RAMCookieJar(QNetworkCookieJar):
Return: Return:
True if one or more cookies are set for 'url', otherwise False. True if one or more cookies are set for 'url', otherwise False.
""" """
if config.get('content', 'cookies-accept') == 'never': if config.val.content.cookies.accept == 'never':
return False return False
else: else:
self.changed.emit() self.changed.emit()
@ -74,10 +74,10 @@ class CookieJar(RAMCookieJar):
self._lineparser = lineparser.LineParser( self._lineparser = lineparser.LineParser(
standarddir.data(), 'cookies', binary=True, parent=self) standarddir.data(), 'cookies', binary=True, parent=self)
self.parse_cookies() self.parse_cookies()
objreg.get('config').changed.connect(self.cookies_store_changed) config.instance.changed.connect(self._on_cookies_store_changed)
objreg.get('save-manager').add_saveable( objreg.get('save-manager').add_saveable(
'cookies', self.save, self.changed, 'cookies', self.save, self.changed,
config_opt=('content', 'cookies-store')) config_opt='content.cookies.store')
def parse_cookies(self): def parse_cookies(self):
"""Parse cookies from lineparser and store them.""" """Parse cookies from lineparser and store them."""
@ -105,10 +105,10 @@ class CookieJar(RAMCookieJar):
self._lineparser.data = lines self._lineparser.data = lines
self._lineparser.save() self._lineparser.save()
@config.change_filter('content', 'cookies-store') @config.change_filter('content.cookies.store')
def cookies_store_changed(self): def _on_cookies_store_changed(self):
"""Delete stored cookies if cookies-store changed.""" """Delete stored cookies if cookies.store changed."""
if not config.get('content', 'cookies-store'): if not config.val.content.cookies.store:
self._lineparser.data = [] self._lineparser.data = []
self._lineparser.save() self._lineparser.save()
self.changed.emit() self.changed.emit()

View File

@ -101,13 +101,12 @@ def dirbrowser_html(path):
except OSError as e: except OSError as e:
html = jinja.render('error.html', html = jinja.render('error.html',
title="Error while reading directory", title="Error while reading directory",
url='file:///{}'.format(path), error=str(e), url='file:///{}'.format(path), error=str(e))
icon='')
return html.encode('UTF-8', errors='xmlcharrefreplace') return html.encode('UTF-8', errors='xmlcharrefreplace')
files = get_file_list(path, all_files, os.path.isfile) files = get_file_list(path, all_files, os.path.isfile)
directories = get_file_list(path, all_files, os.path.isdir) directories = get_file_list(path, all_files, os.path.isdir)
html = jinja.render('dirbrowser.html', title=title, url=path, icon='', html = jinja.render('dirbrowser.html', title=title, url=path,
parent=parent, files=files, directories=directories) parent=parent, files=files, directories=directories)
return html.encode('UTF-8', errors='xmlcharrefreplace') return html.encode('UTF-8', errors='xmlcharrefreplace')

View File

@ -274,7 +274,7 @@ class NetworkManager(QNetworkAccessManager):
# altogether. # altogether.
reply.netrc_used = True reply.netrc_used = True
try: try:
net = netrc.netrc(config.get('network', 'netrc-file')) net = netrc.netrc(config.val.content.netrc_file)
authenticators = net.authenticators(reply.url().host()) authenticators = net.authenticators(reply.url().host())
if authenticators is not None: if authenticators is not None:
(user, _account, password) = authenticators (user, _account, password) = authenticators
@ -338,7 +338,7 @@ class NetworkManager(QNetworkAccessManager):
def set_referer(self, req, current_url): def set_referer(self, req, current_url):
"""Set the referer header.""" """Set the referer header."""
referer_header_conf = config.get('network', 'referer-header') referer_header_conf = config.val.content.headers.referer
try: try:
if referer_header_conf == 'never': if referer_header_conf == 'never':

View File

@ -20,16 +20,12 @@
"""QtWebKit specific qute://* handlers and glue code.""" """QtWebKit specific qute://* handlers and glue code."""
import mimetypes import mimetypes
import functools
import configparser
from PyQt5.QtCore import pyqtSlot, QObject
from PyQt5.QtNetwork import QNetworkReply from PyQt5.QtNetwork import QNetworkReply
from qutebrowser.browser import pdfjs, qutescheme from qutebrowser.browser import pdfjs, qutescheme
from qutebrowser.browser.webkit.network import schemehandler, networkreply from qutebrowser.browser.webkit.network import schemehandler, networkreply
from qutebrowser.utils import jinja, log, message, objreg, usertypes, qtutils from qutebrowser.utils import log, usertypes, qtutils
from qutebrowser.config import configexc, configdata
class QuteSchemeHandler(schemehandler.SchemeHandler): class QuteSchemeHandler(schemehandler.SchemeHandler):
@ -70,34 +66,6 @@ class QuteSchemeHandler(schemehandler.SchemeHandler):
self.parent()) self.parent())
class JSBridge(QObject):
"""Javascript-bridge for special qute://... pages."""
@pyqtSlot(str, str, str)
def set(self, sectname, optname, value):
"""Slot to set a setting from qute://settings."""
# https://github.com/qutebrowser/qutebrowser/issues/727
if ((sectname, optname) == ('content', 'allow-javascript') and
value == 'false'):
message.error("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:
message.error(str(e))
@qutescheme.add_handler('settings', backend=usertypes.Backend.QtWebKit)
def qute_settings(_url):
"""Handler for qute://settings. View/change qute configuration."""
config_getter = functools.partial(objreg.get('config').get, raw=True)
html = jinja.render('settings.html', title='settings', config=configdata,
confget=config_getter)
return 'text/html', html
@qutescheme.add_handler('pdfjs', backend=usertypes.Backend.QtWebKit) @qutescheme.add_handler('pdfjs', backend=usertypes.Backend.QtWebKit)
def qute_pdfjs(url): def qute_pdfjs(url):
"""Handler for qute://pdfjs. Return the pdf.js viewer.""" """Handler for qute://pdfjs. Return the pdf.js viewer."""

View File

@ -168,7 +168,7 @@ class WebKitElement(webelem.AbstractWebElement):
if width > 1 and height > 1: if width > 1 and height > 1:
# fix coordinates according to zoom level # fix coordinates according to zoom level
zoom = self._elem.webFrame().zoomFactor() zoom = self._elem.webFrame().zoomFactor()
if not config.get('ui', 'zoom-text-only'): if not config.val.zoom.text_only:
rect["left"] *= zoom rect["left"] *= zoom
rect["top"] *= zoom rect["top"] *= zoom
width *= zoom width *= zoom

View File

@ -36,9 +36,9 @@ class WebKitInspector(inspector.AbstractWebInspector):
self._set_widget(qwebinspector) self._set_widget(qwebinspector)
def inspect(self, page): def inspect(self, page):
if not config.get('general', 'developer-extras'): if not config.val.content.developer_extras:
raise inspector.WebInspectorError( raise inspector.WebInspectorError(
"Please enable developer-extras before using the " "Please enable content.developer_extras before using the "
"webinspector!") "webinspector!")
self._widget.setPage(page) self._widget.setPage(page)
self.show() self.show()

View File

@ -33,7 +33,7 @@ from PyQt5.QtGui import QFont
from PyQt5.QtWebKit import QWebSettings from PyQt5.QtWebKit import QWebSettings
from qutebrowser.config import config, websettings from qutebrowser.config import config, websettings
from qutebrowser.utils import standarddir, objreg, urlutils, qtutils from qutebrowser.utils import standarddir, urlutils, qtutils
from qutebrowser.browser import shared from qutebrowser.browser import shared
@ -111,12 +111,11 @@ def _set_user_stylesheet():
QWebSettings.globalSettings().setUserStyleSheetUrl(url) QWebSettings.globalSettings().setUserStyleSheetUrl(url)
def update_settings(section, option): def _update_settings(option):
"""Update global settings when qwebsettings changed.""" """Update global settings when qwebsettings changed."""
if section == 'ui' and option in ['hide-scrollbar', 'user-stylesheet']: if option in ['scrollbar.hide', 'content.user_stylesheets']:
_set_user_stylesheet() _set_user_stylesheet()
websettings.update_mappings(MAPPINGS, option)
websettings.update_mappings(MAPPINGS, section, option)
def init(_args): def init(_args):
@ -132,7 +131,7 @@ def init(_args):
QWebSettings.setOfflineStoragePath( QWebSettings.setOfflineStoragePath(
os.path.join(data_path, 'offline-storage')) os.path.join(data_path, 'offline-storage'))
if (config.get('general', 'private-browsing') and if (config.val.content.private_browsing and
not qtutils.version_check('5.4.2')): not qtutils.version_check('5.4.2')):
# WORKAROUND for https://codereview.qt-project.org/#/c/108936/ # WORKAROUND for https://codereview.qt-project.org/#/c/108936/
# Won't work when private browsing is not enabled globally, but that's # Won't work when private browsing is not enabled globally, but that's
@ -141,7 +140,7 @@ def init(_args):
websettings.init_mappings(MAPPINGS) websettings.init_mappings(MAPPINGS)
_set_user_stylesheet() _set_user_stylesheet()
objreg.get('config').changed.connect(update_settings) config.instance.changed.connect(_update_settings)
def shutdown(): def shutdown():
@ -152,96 +151,79 @@ def shutdown():
MAPPINGS = { MAPPINGS = {
'content': { 'content.images':
'allow-images': Attribute(QWebSettings.AutoLoadImages),
Attribute(QWebSettings.AutoLoadImages), 'content.javascript.enabled':
'allow-javascript': Attribute(QWebSettings.JavascriptEnabled),
Attribute(QWebSettings.JavascriptEnabled), 'content.javascript.can_open_tabs_automatically':
'javascript-can-open-windows-automatically': Attribute(QWebSettings.JavascriptCanOpenWindows),
Attribute(QWebSettings.JavascriptCanOpenWindows), 'content.javascript.can_close_tabs':
'javascript-can-close-windows': Attribute(QWebSettings.JavascriptCanCloseWindows),
Attribute(QWebSettings.JavascriptCanCloseWindows), 'content.javascript.can_access_clipboard':
'javascript-can-access-clipboard': Attribute(QWebSettings.JavascriptCanAccessClipboard),
Attribute(QWebSettings.JavascriptCanAccessClipboard), 'content.plugins':
'allow-plugins': Attribute(QWebSettings.PluginsEnabled),
Attribute(QWebSettings.PluginsEnabled), 'content.webgl':
'webgl': Attribute(QWebSettings.WebGLEnabled),
Attribute(QWebSettings.WebGLEnabled), 'content.hyperlink_auditing':
'hyperlink-auditing': Attribute(QWebSettings.HyperlinkAuditingEnabled),
Attribute(QWebSettings.HyperlinkAuditingEnabled), 'content.local_content_can_access_remote_urls':
'local-content-can-access-remote-urls': Attribute(QWebSettings.LocalContentCanAccessRemoteUrls),
Attribute(QWebSettings.LocalContentCanAccessRemoteUrls), 'content.local_content_can_access_file_urls':
'local-content-can-access-file-urls': Attribute(QWebSettings.LocalContentCanAccessFileUrls),
Attribute(QWebSettings.LocalContentCanAccessFileUrls), 'content.cookies.accept':
'cookies-accept': CookiePolicy(),
CookiePolicy(), 'content.dns_prefetch':
}, Attribute(QWebSettings.DnsPrefetchEnabled),
'network': { 'content.frame_flattening':
'dns-prefetch': Attribute(QWebSettings.FrameFlatteningEnabled),
Attribute(QWebSettings.DnsPrefetchEnabled), 'content.cache.appcache':
}, Attribute(QWebSettings.OfflineWebApplicationCacheEnabled),
'input': { 'content.local_storage':
'spatial-navigation': Attribute(QWebSettings.LocalStorageEnabled,
Attribute(QWebSettings.SpatialNavigationEnabled), QWebSettings.OfflineStorageDatabaseEnabled),
'links-included-in-focus-chain': 'content.cache.maximum_pages':
Attribute(QWebSettings.LinksIncludedInFocusChain), StaticSetter(QWebSettings.setMaximumPagesInCache),
}, 'content.developer_extras':
'fonts': { Attribute(QWebSettings.DeveloperExtrasEnabled),
'web-family-standard': 'content.print_element_backgrounds':
FontFamilySetter(QWebSettings.StandardFont), Attribute(QWebSettings.PrintElementBackgrounds),
'web-family-fixed': 'content.xss_auditing':
FontFamilySetter(QWebSettings.FixedFont), Attribute(QWebSettings.XSSAuditingEnabled),
'web-family-serif': 'content.default_encoding':
FontFamilySetter(QWebSettings.SerifFont), Setter(QWebSettings.setDefaultTextEncoding),
'web-family-sans-serif': # content.user_stylesheets is handled separately
FontFamilySetter(QWebSettings.SansSerifFont),
'web-family-cursive': 'input.spatial_navigation':
FontFamilySetter(QWebSettings.CursiveFont), Attribute(QWebSettings.SpatialNavigationEnabled),
'web-family-fantasy': 'input.links_included_in_focus_chain':
FontFamilySetter(QWebSettings.FantasyFont), Attribute(QWebSettings.LinksIncludedInFocusChain),
'web-size-minimum':
Setter(QWebSettings.setFontSize, 'fonts.web.family.standard':
args=[QWebSettings.MinimumFontSize]), FontFamilySetter(QWebSettings.StandardFont),
'web-size-minimum-logical': 'fonts.web.family.fixed':
Setter(QWebSettings.setFontSize, FontFamilySetter(QWebSettings.FixedFont),
args=[QWebSettings.MinimumLogicalFontSize]), 'fonts.web.family.serif':
'web-size-default': FontFamilySetter(QWebSettings.SerifFont),
Setter(QWebSettings.setFontSize, 'fonts.web.family.sans_serif':
args=[QWebSettings.DefaultFontSize]), FontFamilySetter(QWebSettings.SansSerifFont),
'web-size-default-fixed': 'fonts.web.family.cursive':
Setter(QWebSettings.setFontSize, FontFamilySetter(QWebSettings.CursiveFont),
args=[QWebSettings.DefaultFixedFontSize]), 'fonts.web.family.fantasy':
}, FontFamilySetter(QWebSettings.FantasyFont),
'ui': { 'fonts.web.size.minimum':
'zoom-text-only': Setter(QWebSettings.setFontSize, args=[QWebSettings.MinimumFontSize]),
Attribute(QWebSettings.ZoomTextOnly), 'fonts.web.size.minimum_logical':
'frame-flattening': Setter(QWebSettings.setFontSize,
Attribute(QWebSettings.FrameFlatteningEnabled), args=[QWebSettings.MinimumLogicalFontSize]),
# user-stylesheet is handled separately 'fonts.web.size.default':
'smooth-scrolling': Setter(QWebSettings.setFontSize, args=[QWebSettings.DefaultFontSize]),
Attribute(QWebSettings.ScrollAnimatorEnabled), 'fonts.web.size.default_fixed':
#'accelerated-compositing': Setter(QWebSettings.setFontSize,
# Attribute(QWebSettings.AcceleratedCompositingEnabled), args=[QWebSettings.DefaultFixedFontSize]),
#'tiled-backing-store':
# Attribute(QWebSettings.TiledBackingStoreEnabled), 'zoom.text_only':
}, Attribute(QWebSettings.ZoomTextOnly),
'storage': { 'scrolling.smooth':
'offline-web-application-cache': Attribute(QWebSettings.ScrollAnimatorEnabled),
Attribute(QWebSettings.OfflineWebApplicationCacheEnabled),
'local-storage':
Attribute(QWebSettings.LocalStorageEnabled,
QWebSettings.OfflineStorageDatabaseEnabled),
'maximum-pages-in-cache':
StaticSetter(QWebSettings.setMaximumPagesInCache),
},
'general': {
'developer-extras':
Attribute(QWebSettings.DeveloperExtrasEnabled),
'print-element-backgrounds':
Attribute(QWebSettings.PrintElementBackgrounds),
'xss-auditing':
Attribute(QWebSettings.XSSAuditingEnabled),
'default-encoding':
Setter(QWebSettings.setDefaultTextEncoding),
}
} }

View File

@ -27,25 +27,15 @@ import sip
from PyQt5.QtCore import (pyqtSlot, Qt, QEvent, QUrl, QPoint, QTimer, QSizeF, from PyQt5.QtCore import (pyqtSlot, Qt, QEvent, QUrl, QPoint, QTimer, QSizeF,
QSize) QSize)
from PyQt5.QtGui import QKeyEvent from PyQt5.QtGui import QKeyEvent
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame
from PyQt5.QtWebKit import QWebSettings from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtPrintSupport import QPrinter from PyQt5.QtPrintSupport import QPrinter
from qutebrowser.browser import browsertab from qutebrowser.browser import browsertab
from qutebrowser.browser.webkit import webview, tabhistory, webkitelem from qutebrowser.browser.webkit import webview, tabhistory, webkitelem
from qutebrowser.browser.webkit.network import webkitqutescheme
from qutebrowser.utils import qtutils, objreg, usertypes, utils, log, debug from qutebrowser.utils import qtutils, objreg, usertypes, utils, log, debug
def init():
"""Initialize QtWebKit-specific modules."""
qapp = QApplication.instance()
log.init.debug("Initializing js-bridge...")
js_bridge = webkitqutescheme.JSBridge(qapp)
objreg.register('js-bridge', js_bridge)
class WebKitAction(browsertab.AbstractAction): class WebKitAction(browsertab.AbstractAction):
"""QtWebKit implementations related to web actions.""" """QtWebKit implementations related to web actions."""
@ -133,24 +123,21 @@ class WebKitSearch(browsertab.AbstractSearch):
self._widget.findText('') self._widget.findText('')
self._widget.findText('', QWebPage.HighlightAllOccurrences) self._widget.findText('', QWebPage.HighlightAllOccurrences)
def search(self, text, *, ignore_case=False, reverse=False, def search(self, text, *, ignore_case='never', reverse=False,
result_cb=None): result_cb=None):
self.search_displayed = True self.search_displayed = True
flags = QWebPage.FindWrapsAroundDocument self.text = text
if ignore_case == 'smart': self._flags = QWebPage.FindWrapsAroundDocument
if not text.islower(): if self._is_case_sensitive(ignore_case):
flags |= QWebPage.FindCaseSensitively self._flags |= QWebPage.FindCaseSensitively
elif not ignore_case:
flags |= QWebPage.FindCaseSensitively
if reverse: if reverse:
flags |= QWebPage.FindBackward self._flags |= QWebPage.FindBackward
# We actually search *twice* - once to highlight everything, then again # We actually search *twice* - once to highlight everything, then again
# to get a mark so we can navigate. # to get a mark so we can navigate.
found = self._widget.findText(text, flags) found = self._widget.findText(text, self._flags)
self._widget.findText(text, flags | QWebPage.HighlightAllOccurrences) self._widget.findText(text,
self.text = text self._flags | QWebPage.HighlightAllOccurrences)
self._flags = flags self._call_cb(result_cb, found, text, self._flags, 'search')
self._call_cb(result_cb, found, text, flags, 'search')
def next_result(self, *, result_cb=None): def next_result(self, *, result_cb=None):
self.search_displayed = True self.search_displayed = True

View File

@ -170,7 +170,7 @@ class BrowserPage(QWebPage):
title = "Error loading page: {}".format(urlstr) title = "Error loading page: {}".format(urlstr)
error_html = jinja.render( error_html = jinja.render(
'error.html', 'error.html',
title=title, url=urlstr, error=error_str, icon='') title=title, url=urlstr, error=error_str)
errpage.content = error_html.encode('utf-8') errpage.content = error_html.encode('utf-8')
errpage.encoding = 'utf-8' errpage.encoding = 'utf-8'
return True return True
@ -277,7 +277,7 @@ class BrowserPage(QWebPage):
reply.finished.connect(functools.partial( reply.finished.connect(functools.partial(
self.display_content, reply, 'image/jpeg')) self.display_content, reply, 'image/jpeg'))
elif (mimetype in ['application/pdf', 'application/x-pdf'] and elif (mimetype in ['application/pdf', 'application/x-pdf'] and
config.get('content', 'enable-pdfjs')): config.val.content.pdfjs):
# Use pdf.js to display the page # Use pdf.js to display the page
self._show_pdfjs(reply) self._show_pdfjs(reply)
else: else:
@ -304,8 +304,8 @@ class BrowserPage(QWebPage):
return return
options = { options = {
QWebPage.Notifications: ('content', 'notifications'), QWebPage.Notifications: 'content.notifications',
QWebPage.Geolocation: ('content', 'geolocation'), QWebPage.Geolocation: 'content.geolocation',
} }
messages = { messages = {
QWebPage.Notifications: 'show notifications', QWebPage.Notifications: 'show notifications',
@ -384,7 +384,7 @@ class BrowserPage(QWebPage):
def userAgentForUrl(self, url): def userAgentForUrl(self, url):
"""Override QWebPage::userAgentForUrl to customize the user agent.""" """Override QWebPage::userAgentForUrl to customize the user agent."""
ua = config.get('network', 'user-agent') ua = config.val.content.headers.user_agent
if ua is None: if ua is None:
return super().userAgentForUrl(url) return super().userAgentForUrl(url)
else: else:
@ -446,15 +446,8 @@ class BrowserPage(QWebPage):
def javaScriptConsoleMessage(self, msg, line, source): def javaScriptConsoleMessage(self, msg, line, source):
"""Override javaScriptConsoleMessage to use debug log.""" """Override javaScriptConsoleMessage to use debug log."""
log_javascript_console = config.get('general', shared.javascript_log_message(usertypes.JsLogLevel.unknown,
'log-javascript-console') source, line, msg)
logstring = "[{}:{}] {}".format(source, line, msg)
logmap = {
'debug': log.js.debug,
'info': log.js.info,
'none': lambda arg: None
}
logmap[log_javascript_console](logstring)
def acceptNavigationRequest(self, def acceptNavigationRequest(self,
_frame: QWebFrame, _frame: QWebFrame,

View File

@ -25,7 +25,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QUrl
from PyQt5.QtGui import QPalette from PyQt5.QtGui import QPalette
from PyQt5.QtWidgets import QStyleFactory from PyQt5.QtWidgets import QStyleFactory
from PyQt5.QtWebKit import QWebSettings from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtWebKitWidgets import QWebView, QWebPage, QWebFrame from PyQt5.QtWebKitWidgets import QWebView, QWebPage
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.keyinput import modeman from qutebrowser.keyinput import modeman
@ -88,7 +88,7 @@ class WebView(QWebView):
window=win_id) window=win_id)
mode_manager.entered.connect(self.on_mode_entered) mode_manager.entered.connect(self.on_mode_entered)
mode_manager.left.connect(self.on_mode_left) mode_manager.left.connect(self.on_mode_left)
objreg.get('config').changed.connect(self._set_bg_color) config.instance.changed.connect(self._set_bg_color)
def __repr__(self): def __repr__(self):
url = utils.elide(self.url().toDisplayString(QUrl.EncodeUnicode), 100) url = utils.elide(self.url().toDisplayString(QUrl.EncodeUnicode), 100)
@ -107,10 +107,10 @@ class WebView(QWebView):
# deleted # deleted
pass pass
@config.change_filter('colors', 'webpage.bg') @config.change_filter('colors.webpage.bg')
def _set_bg_color(self): def _set_bg_color(self):
"""Set the webpage background color as configured.""" """Set the webpage background color as configured."""
col = config.get('colors', 'webpage.bg') col = config.val.colors.webpage.bg
palette = self.palette() palette = self.palette()
if col is None: if col is None:
col = self.style().standardPalette().color(QPalette.Base) col = self.style().standardPalette().color(QPalette.Base)
@ -135,22 +135,6 @@ class WebView(QWebView):
url: The URL to load as QUrl url: The URL to load as QUrl
""" """
self.load(url) self.load(url)
if url.scheme() == 'qute':
frame = self.page().mainFrame()
frame.javaScriptWindowObjectCleared.connect(self.add_js_bridge)
@pyqtSlot()
def add_js_bridge(self):
"""Add the javascript bridge for qute://... pages."""
frame = self.sender()
if not isinstance(frame, QWebFrame):
log.webview.error("Got non-QWebFrame {!r} in "
"add_js_bridge!".format(frame))
return
if frame.url().scheme() == 'qute':
bridge = objreg.get('js-bridge')
frame.addToJavaScriptWindowObject('qute', bridge)
@pyqtSlot(usertypes.KeyMode) @pyqtSlot(usertypes.KeyMode)
def on_mode_entered(self, mode): def on_mode_entered(self, mode):
@ -285,10 +269,10 @@ class WebView(QWebView):
This is implemented here as we don't need it for QtWebEngine. This is implemented here as we don't need it for QtWebEngine.
""" """
if e.button() == Qt.MidButton or e.modifiers() & Qt.ControlModifier: if e.button() == Qt.MidButton or e.modifiers() & Qt.ControlModifier:
background_tabs = config.get('tabs', 'background-tabs') background = config.val.tabs.background
if e.modifiers() & Qt.ShiftModifier: if e.modifiers() & Qt.ShiftModifier:
background_tabs = not background_tabs background = not background
if background_tabs: if background:
target = usertypes.ClickTarget.tab_bg target = usertypes.ClickTarget.tab_bg
else: else:
target = usertypes.ClickTarget.tab target = usertypes.ClickTarget.tab

View File

@ -23,33 +23,33 @@ Defined here to avoid circular dependency hell.
""" """
class CommandError(Exception): class Error(Exception):
"""Base class for all cmdexc errors."""
class CommandError(Error):
"""Raised when a command encounters an error while running.""" """Raised when a command encounters an error while running."""
pass pass
class CommandMetaError(Exception): class NoSuchCommandError(Error):
"""Common base class for exceptions occurring before a command is run."""
class NoSuchCommandError(CommandMetaError):
"""Raised when a command wasn't found.""" """Raised when a command wasn't found."""
pass pass
class ArgumentTypeError(CommandMetaError): class ArgumentTypeError(Error):
"""Raised when an argument had an invalid type.""" """Raised when an argument had an invalid type."""
pass pass
class PrerequisitesError(CommandMetaError): class PrerequisitesError(Error):
"""Raised when a cmd can't be used because some prerequisites aren't met. """Raised when a cmd can't be used because some prerequisites aren't met.

View File

@ -21,7 +21,6 @@
Module attributes: Module attributes:
cmd_dict: A mapping from command-strings to command objects. cmd_dict: A mapping from command-strings to command objects.
aliases: A list of all aliases, needed for doc generation.
""" """
import inspect import inspect
@ -30,7 +29,6 @@ from qutebrowser.utils import qtutils, log
from qutebrowser.commands import command, cmdexc from qutebrowser.commands import command, cmdexc
cmd_dict = {} cmd_dict = {}
aliases = []
def check_overflow(arg, ctype): def check_overflow(arg, ctype):
@ -88,28 +86,6 @@ class register: # pylint: disable=invalid-name
self._name = name self._name = name
self._kwargs = kwargs self._kwargs = kwargs
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
transformed.
If it has been set, it can either be a string which is
used directly, or an iterable.
Args:
func: The function to get the name of.
Return:
A list of names, with the main name being the first item.
"""
if self._name is None:
return [func.__name__.lower().replace('_', '-')]
elif isinstance(self._name, str):
return [self._name]
else:
return self._name
def __call__(self, func): def __call__(self, func):
"""Register the command before running the function. """Register the command before running the function.
@ -124,17 +100,17 @@ class register: # pylint: disable=invalid-name
Return: Return:
The original function (unmodified). The original function (unmodified).
""" """
global aliases if self._name is None:
names = self._get_names(func) name = func.__name__.lower().replace('_', '-')
log.commands.vdebug("Registering command {}".format(names[0])) else:
for name in names: assert isinstance(self._name, str), self._name
if name in cmd_dict: name = self._name
raise ValueError("{} is already registered!".format(name)) log.commands.vdebug("Registering command {}".format(name))
cmd = command.Command(name=names[0], instance=self._instance, if name in cmd_dict:
raise ValueError("{} is already registered!".format(name))
cmd = command.Command(name=name, instance=self._instance,
handler=func, **self._kwargs) handler=func, **self._kwargs)
for name in names: cmd_dict[name] = cmd
cmd_dict[name] = cmd
aliases += names[1:]
return func return func

View File

@ -90,7 +90,7 @@ class Command:
def __init__(self, *, handler, name, instance=None, maxsplit=None, def __init__(self, *, handler, name, instance=None, maxsplit=None,
hide=False, modes=None, not_modes=None, debug=False, hide=False, modes=None, not_modes=None, debug=False,
ignore_args=False, deprecated=False, no_cmd_split=False, deprecated=False, no_cmd_split=False,
star_args_optional=False, scope='global', backend=None, star_args_optional=False, scope='global', backend=None,
no_replace_variables=False): no_replace_variables=False):
# I really don't know how to solve this in a better way, I tried. # I really don't know how to solve this in a better way, I tried.
@ -121,7 +121,6 @@ class Command:
self._scope = scope self._scope = scope
self._star_args_optional = star_args_optional self._star_args_optional = star_args_optional
self.debug = debug self.debug = debug
self.ignore_args = ignore_args
self.handler = handler self.handler = handler
self.no_cmd_split = no_cmd_split self.no_cmd_split = no_cmd_split
self.backend = backend self.backend = backend
@ -225,33 +224,31 @@ class Command:
else: else:
self.desc = "" self.desc = ""
if not self.ignore_args: for param in signature.parameters.values():
for param in signature.parameters.values(): # https://docs.python.org/3/library/inspect.html#inspect.Parameter.kind
# https://docs.python.org/3/library/inspect.html#inspect.Parameter.kind # "Python has no explicit syntax for defining positional-only
# "Python has no explicit syntax for defining positional-only # parameters, but many built-in and extension module functions
# parameters, but many built-in and extension module functions # (especially those that accept only one or two parameters) accept
# (especially those that accept only one or two parameters) # them."
# accept them." assert param.kind != inspect.Parameter.POSITIONAL_ONLY
assert param.kind != inspect.Parameter.POSITIONAL_ONLY if param.name == 'self':
if param.name == 'self': continue
continue if self._inspect_special_param(param):
if self._inspect_special_param(param): continue
continue if (param.kind == inspect.Parameter.KEYWORD_ONLY and
if (param.kind == inspect.Parameter.KEYWORD_ONLY and param.default is inspect.Parameter.empty):
param.default is inspect.Parameter.empty): raise TypeError("{}: handler has keyword only argument {!r} "
raise TypeError("{}: handler has keyword only argument " "without default!".format(
"{!r} without default!".format(self.name, self.name, param.name))
param.name)) typ = self._get_type(param)
typ = self._get_type(param) is_bool = typ is bool
is_bool = typ is bool kwargs = self._param_to_argparse_kwargs(param, is_bool)
kwargs = self._param_to_argparse_kwargs(param, is_bool) args = self._param_to_argparse_args(param, is_bool)
args = self._param_to_argparse_args(param, is_bool) callsig = debug_utils.format_call(self.parser.add_argument, args,
callsig = debug_utils.format_call( kwargs, full=False)
self.parser.add_argument, args, kwargs, log.commands.vdebug('Adding arg {} of type {} -> {}'.format(
full=False) param.name, typ, callsig))
log.commands.vdebug('Adding arg {} of type {} -> {}'.format( self.parser.add_argument(*args, **kwargs)
param.name, typ, callsig))
self.parser.add_argument(*args, **kwargs)
return signature.parameters.values() return signature.parameters.values()
def _param_to_argparse_kwargs(self, param, is_bool): def _param_to_argparse_kwargs(self, param, is_bool):
@ -453,12 +450,6 @@ class Command:
kwargs = {} kwargs = {}
signature = inspect.signature(self.handler) signature = inspect.signature(self.handler)
if self.ignore_args:
if self._instance is not None:
param = list(signature.parameters.values())[0]
self._get_self_arg(win_id, param, args)
return args, kwargs
for i, param in enumerate(signature.parameters.values()): for i, param in enumerate(signature.parameters.values()):
arg_info = self.get_arg_info(param) arg_info = self.get_arg_info(param)
if i == 0 and self._instance is not None: if i == 0 and self._instance is not None:

View File

@ -25,7 +25,7 @@ import re
from PyQt5.QtCore import pyqtSlot, QUrl, QObject from PyQt5.QtCore import pyqtSlot, QUrl, QObject
from qutebrowser.config import config, configexc from qutebrowser.config import config
from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.utils import message, objreg, qtutils, usertypes, utils from qutebrowser.utils import message, objreg, qtutils, usertypes, utils
from qutebrowser.misc import split from qutebrowser.misc import split
@ -81,19 +81,17 @@ def replace_variables(win_id, arglist):
return args return args
class CommandRunner(QObject): class CommandParser:
"""Parse and run qutebrowser commandline commands. """Parse qutebrowser commandline commands.
Attributes: Attributes:
_win_id: The window this CommandRunner is associated with.
_partial_match: Whether to allow partial command matches. _partial_match: Whether to allow partial command matches.
""" """
def __init__(self, win_id, partial_match=False, parent=None): def __init__(self, partial_match=False):
super().__init__(parent)
self._partial_match = partial_match self._partial_match = partial_match
self._win_id = win_id
def _get_alias(self, text, default=None): def _get_alias(self, text, default=None):
"""Get an alias from the config. """Get an alias from the config.
@ -108,9 +106,10 @@ class CommandRunner(QObject):
""" """
parts = text.strip().split(maxsplit=1) parts = text.strip().split(maxsplit=1)
try: try:
alias = config.get('aliases', parts[0]) alias = config.val.aliases[parts[0]]
except (configexc.NoOptionError, configexc.NoSectionError): except KeyError:
return default return default
try: try:
new_cmd = '{} {}'.format(alias, parts[1]) new_cmd = '{} {}'.format(alias, parts[1])
except IndexError: except IndexError:
@ -119,7 +118,7 @@ class CommandRunner(QObject):
new_cmd += ' ' new_cmd += ' '
return new_cmd return new_cmd
def parse_all(self, text, aliases=True, *args, **kwargs): def _parse_all_gen(self, text, aliases=True, *args, **kwargs):
"""Split a command on ;; and parse all parts. """Split a command on ;; and parse all parts.
If the first command in the commandline is a non-split one, it only If the first command in the commandline is a non-split one, it only
@ -154,6 +153,10 @@ class CommandRunner(QObject):
for sub in sub_texts: for sub in sub_texts:
yield self.parse(sub, *args, **kwargs) yield self.parse(sub, *args, **kwargs)
def parse_all(self, *args, **kwargs):
"""Wrapper over parse_all."""
return list(self._parse_all_gen(*args, **kwargs))
def parse(self, text, *, fallback=False, keep=False): def parse(self, text, *, fallback=False, keep=False):
"""Split the commandline text into command and arguments. """Split the commandline text into command and arguments.
@ -253,6 +256,20 @@ class CommandRunner(QObject):
# already. # already.
return split_args return split_args
class CommandRunner(QObject):
"""Parse and run qutebrowser commandline commands.
Attributes:
_win_id: The window this CommandRunner is associated with.
"""
def __init__(self, win_id, partial_match=False, parent=None):
super().__init__(parent)
self._parser = CommandParser(partial_match=partial_match)
self._win_id = win_id
def run(self, text, count=None): def run(self, text, count=None):
"""Parse a command from a line of text and run it. """Parse a command from a line of text and run it.
@ -267,7 +284,7 @@ class CommandRunner(QObject):
window=self._win_id) window=self._win_id)
cur_mode = mode_manager.mode cur_mode = mode_manager.mode
for result in self.parse_all(text): for result in self._parser.parse_all(text):
if result.cmd.no_replace_variables: if result.cmd.no_replace_variables:
args = result.args args = result.args
else: else:
@ -294,7 +311,7 @@ class CommandRunner(QObject):
"""Run a command and display exceptions in the statusbar.""" """Run a command and display exceptions in the statusbar."""
try: try:
self.run(text, count) self.run(text, count)
except (cmdexc.CommandMetaError, cmdexc.CommandError) as e: except cmdexc.Error as e:
message.error(str(e), stack=traceback.format_exc()) message.error(str(e), stack=traceback.format_exc())
@pyqtSlot(str, int) @pyqtSlot(str, int)
@ -306,5 +323,5 @@ class CommandRunner(QObject):
""" """
try: try:
self.run(text, count) self.run(text, count)
except (cmdexc.CommandMetaError, cmdexc.CommandError) as e: except cmdexc.Error as e:
message.error(str(e), stack=traceback.format_exc()) message.error(str(e), stack=traceback.format_exc())

View File

@ -376,7 +376,7 @@ def _lookup_path(cmd):
""" """
directories = [ directories = [
os.path.join(standarddir.data(), "userscripts"), os.path.join(standarddir.data(), "userscripts"),
os.path.join(standarddir.system_data(), "userscripts"), os.path.join(standarddir.data(system=True), "userscripts"),
] ]
for directory in directories: for directory in directories:
cmd_path = os.path.join(directory, cmd) cmd_path = os.path.join(directory, cmd)
@ -417,7 +417,7 @@ def run_async(tab, cmd, *args, win_id, env, verbose=False):
lambda cmd: lambda cmd:
log.commands.debug("Got userscript command: {}".format(cmd))) log.commands.debug("Got userscript command: {}".format(cmd)))
runner.got_cmd.connect(commandrunner.run_safely) runner.got_cmd.connect(commandrunner.run_safely)
user_agent = config.get('network', 'user-agent') user_agent = config.val.content.headers.user_agent
if user_agent is not None: if user_agent is not None:
env['QUTE_USER_AGENT'] = user_agent env['QUTE_USER_AGENT'] = user_agent

View File

@ -19,6 +19,8 @@
"""Completer attached to a CompletionView.""" """Completer attached to a CompletionView."""
import collections
from PyQt5.QtCore import pyqtSlot, QObject, QTimer from PyQt5.QtCore import pyqtSlot, QObject, QTimer
from qutebrowser.config import config from qutebrowser.config import config
@ -27,6 +29,11 @@ from qutebrowser.utils import log, utils, debug
from qutebrowser.completion.models import miscmodels from qutebrowser.completion.models import miscmodels
# Context passed into all completion functions
CompletionInfo = collections.namedtuple('CompletionInfo',
['config', 'keyconf'])
class Completer(QObject): class Completer(QObject):
"""Completer which manages completions in a CompletionView. """Completer which manages completions in a CompletionView.
@ -34,7 +41,6 @@ class Completer(QObject):
Attributes: Attributes:
_cmd: The statusbar Command object this completer belongs to. _cmd: The statusbar Command object this completer belongs to.
_ignore_change: Whether to ignore the next completion update. _ignore_change: Whether to ignore the next completion update.
_win_id: The window ID this completer is in.
_timer: The timer used to trigger the completion update. _timer: The timer used to trigger the completion update.
_last_cursor_pos: The old cursor position so we avoid double completion _last_cursor_pos: The old cursor position so we avoid double completion
updates. updates.
@ -42,9 +48,8 @@ class Completer(QObject):
_last_completion_func: The completion function used for the last text. _last_completion_func: The completion function used for the last text.
""" """
def __init__(self, cmd, win_id, parent=None): def __init__(self, cmd, parent=None):
super().__init__(parent) super().__init__(parent)
self._win_id = win_id
self._cmd = cmd self._cmd = cmd
self._ignore_change = False self._ignore_change = False
self._timer = QTimer() self._timer = QTimer()
@ -106,7 +111,7 @@ class Completer(QObject):
""" """
if not s: if not s:
return "''" return "''"
elif any(c in s for c in ' \'\t\n\\'): elif any(c in s for c in ' "\'\t\n\\'):
# use single quotes, and put single quotes into double quotes # use single quotes, and put single quotes into double quotes
# the string $'b is then quoted as '$'"'"'b' # the string $'b is then quoted as '$'"'"'b'
return "'" + s.replace("'", "'\"'\"'") + "'" return "'" + s.replace("'", "'\"'\"'") + "'"
@ -123,8 +128,8 @@ class Completer(QObject):
if not text or not text.strip(): if not text or not text.strip():
# Only ":", empty part under the cursor with nothing before/after # Only ":", empty part under the cursor with nothing before/after
return [], '', [] return [], '', []
runner = runners.CommandRunner(self._win_id) parser = runners.CommandParser()
result = runner.parse(text, fallback=True, keep=True) result = parser.parse(text, fallback=True, keep=True)
parts = [x for x in result.cmdline if x] parts = [x for x in result.cmdline if x]
pos = self._cmd.cursorPosition() - len(self._cmd.prefix()) pos = self._cmd.cursorPosition() - len(self._cmd.prefix())
pos = min(pos, len(text)) # Qt treats 2-byte UTF-16 chars as 2 chars pos = min(pos, len(text)) # Qt treats 2-byte UTF-16 chars as 2 chars
@ -164,7 +169,7 @@ class Completer(QObject):
if maxsplit is None: if maxsplit is None:
text = self._quote(text) text = self._quote(text)
model = self._model() model = self._model()
if model.count() == 1 and config.get('completion', 'quick-complete'): if model.count() == 1 and config.val.completion.quick:
# If we only have one item, we want to apply it immediately # If we only have one item, we want to apply it immediately
# and go on to the next part. # and go on to the next part.
self._change_completed_part(text, before, after, immediate=True) self._change_completed_part(text, before, after, immediate=True)
@ -233,7 +238,9 @@ class Completer(QObject):
args = (x for x in before_cursor[1:] if not x.startswith('-')) args = (x for x in before_cursor[1:] if not x.startswith('-'))
with debug.log_time(log.completion, with debug.log_time(log.completion,
'Starting {} completion'.format(func.__name__)): 'Starting {} completion'.format(func.__name__)):
model = func(*args) info = CompletionInfo(config=config.instance,
keyconf=config.key_instance)
model = func(*args, info=info)
with debug.log_time(log.completion, 'Set completion model'): with debug.log_time(log.completion, 'Set completion model'):
completion.set_model(model) completion.set_model(model)

View File

@ -30,8 +30,8 @@ from PyQt5.QtCore import QRectF, QSize, Qt
from PyQt5.QtGui import (QIcon, QPalette, QTextDocument, QTextOption, from PyQt5.QtGui import (QIcon, QPalette, QTextDocument, QTextOption,
QAbstractTextDocumentLayout) QAbstractTextDocumentLayout)
from qutebrowser.config import config, configexc, style from qutebrowser.config import config
from qutebrowser.utils import qtutils from qutebrowser.utils import qtutils, jinja
class CompletionItemDelegate(QStyledItemDelegate): class CompletionItemDelegate(QStyledItemDelegate):
@ -147,16 +147,15 @@ class CompletionItemDelegate(QStyledItemDelegate):
# We can't use drawContents because then the color would be ignored. # We can't use drawContents because then the color would be ignored.
clip = QRectF(0, 0, rect.width(), rect.height()) clip = QRectF(0, 0, rect.width(), rect.height())
self._painter.save() self._painter.save()
if self._opt.state & QStyle.State_Selected: if self._opt.state & QStyle.State_Selected:
option = 'completion.item.selected.fg' color = config.val.colors.completion.item.selected.fg
elif not self._opt.state & QStyle.State_Enabled: elif not self._opt.state & QStyle.State_Enabled:
option = 'completion.category.fg' color = config.val.colors.completion.category.fg
else: else:
option = 'completion.fg' color = config.val.colors.completion.fg
try: self._painter.setPen(color)
self._painter.setPen(config.get('colors', option))
except configexc.NoOptionError:
self._painter.setPen(config.get('colors', 'completion.fg'))
ctx = QAbstractTextDocumentLayout.PaintContext() ctx = QAbstractTextDocumentLayout.PaintContext()
ctx.palette.setColor(QPalette.Text, self._painter.pen().color()) ctx.palette.setColor(QPalette.Text, self._painter.pen().color())
if clip.isValid(): if clip.isValid():
@ -188,13 +187,17 @@ class CompletionItemDelegate(QStyledItemDelegate):
self._doc = QTextDocument(self) self._doc = QTextDocument(self)
self._doc.setDefaultFont(self._opt.font) self._doc.setDefaultFont(self._opt.font)
self._doc.setDefaultTextOption(text_option) self._doc.setDefaultTextOption(text_option)
self._doc.setDefaultStyleSheet(style.get_stylesheet("""
.highlight {
color: {{ color['completion.match.fg'] }};
}
"""))
self._doc.setDocumentMargin(2) self._doc.setDocumentMargin(2)
stylesheet = """
.highlight {
color: {{ conf.colors.completion.match.fg }};
}
"""
with jinja.environment.no_autoescape():
template = jinja.environment.from_string(stylesheet)
self._doc.setDefaultStyleSheet(template.render(conf=config.val))
if index.parent().isValid(): if index.parent().isValid():
view = self.parent() view = self.parent()
pattern = view.pattern pattern = view.pattern
@ -209,7 +212,7 @@ class CompletionItemDelegate(QStyledItemDelegate):
else: else:
self._doc.setHtml( self._doc.setHtml(
'<span style="font: {};">{}</span>'.format( '<span style="font: {};">{}</span>'.format(
html.escape(config.get('fonts', 'completion.category')), html.escape(config.val.fonts.completion.category),
html.escape(self._opt.text))) html.escape(self._opt.text)))
def _draw_focus_rect(self): def _draw_focus_rect(self):

View File

@ -26,9 +26,9 @@ subclasses to provide completions.
from PyQt5.QtWidgets import QStyle, QTreeView, QSizePolicy, QStyleFactory from PyQt5.QtWidgets import QStyle, QTreeView, QSizePolicy, QStyleFactory
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel, QSize from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel, QSize
from qutebrowser.config import config, style from qutebrowser.config import config
from qutebrowser.completion import completiondelegate from qutebrowser.completion import completiondelegate
from qutebrowser.utils import utils, usertypes, objreg, debug, log from qutebrowser.utils import utils, usertypes, debug, log
from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.commands import cmdexc, cmdutils
@ -57,27 +57,27 @@ class CompletionView(QTreeView):
# don't define that in this stylesheet. # don't define that in this stylesheet.
STYLESHEET = """ STYLESHEET = """
QTreeView { QTreeView {
font: {{ font['completion'] }}; font: {{ conf.fonts.completion.entry }};
background-color: {{ color['completion.bg'] }}; background-color: {{ conf.colors.completion.even.bg }};
alternate-background-color: {{ color['completion.alternate-bg'] }}; alternate-background-color: {{ conf.colors.completion.odd.bg }};
outline: 0; outline: 0;
border: 0px; border: 0px;
} }
QTreeView::item:disabled { QTreeView::item:disabled {
background-color: {{ color['completion.category.bg'] }}; background-color: {{ conf.colors.completion.category.bg }};
border-top: 1px solid border-top: 1px solid
{{ color['completion.category.border.top'] }}; {{ conf.colors.completion.category.border.top }};
border-bottom: 1px solid border-bottom: 1px solid
{{ color['completion.category.border.bottom'] }}; {{ conf.colors.completion.category.border.bottom }};
} }
QTreeView::item:selected, QTreeView::item:selected:hover { QTreeView::item:selected, QTreeView::item:selected:hover {
border-top: 1px solid border-top: 1px solid
{{ color['completion.item.selected.border.top'] }}; {{ conf.colors.completion.item.selected.border.top }};
border-bottom: 1px solid border-bottom: 1px solid
{{ color['completion.item.selected.border.bottom'] }}; {{ conf.colors.completion.item.selected.border.bottom }};
background-color: {{ color['completion.item.selected.bg'] }}; background-color: {{ conf.colors.completion.item.selected.bg }};
} }
QTreeView:item::hover { QTreeView:item::hover {
@ -85,14 +85,14 @@ class CompletionView(QTreeView):
} }
QTreeView QScrollBar { QTreeView QScrollBar {
width: {{ config.get('completion', 'scrollbar-width') }}px; width: {{ conf.completion.scrollbar.width }}px;
background: {{ color['completion.scrollbar.bg'] }}; background: {{ conf.colors.completion.scrollbar.bg }};
} }
QTreeView QScrollBar::handle { QTreeView QScrollBar::handle {
background: {{ color['completion.scrollbar.fg'] }}; background: {{ conf.colors.completion.scrollbar.fg }};
border: {{ config.get('completion', 'scrollbar-padding') }}px solid border: {{ conf.completion.scrollbar.padding }}px solid
{{ color['completion.scrollbar.bg'] }}; {{ conf.colors.completion.scrollbar.bg }};
min-height: 10px; min-height: 10px;
} }
@ -109,14 +109,14 @@ class CompletionView(QTreeView):
super().__init__(parent) super().__init__(parent)
self.pattern = '' self.pattern = ''
self._win_id = win_id self._win_id = win_id
objreg.get('config').changed.connect(self._on_config_changed) config.instance.changed.connect(self._on_config_changed)
self._active = False self._active = False
self._delegate = completiondelegate.CompletionItemDelegate(self) self._delegate = completiondelegate.CompletionItemDelegate(self)
self.setItemDelegate(self._delegate) self.setItemDelegate(self._delegate)
self.setStyle(QStyleFactory.create('Fusion')) self.setStyle(QStyleFactory.create('Fusion'))
style.set_register_stylesheet(self) config.set_register_stylesheet(self)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.setHeaderHidden(True) self.setHeaderHidden(True)
self.setAlternatingRowColors(True) self.setAlternatingRowColors(True)
@ -139,11 +139,9 @@ class CompletionView(QTreeView):
def __repr__(self): def __repr__(self):
return utils.get_repr(self) return utils.get_repr(self)
@pyqtSlot(str, str) @pyqtSlot(str)
def _on_config_changed(self, section, option): def _on_config_changed(self, option):
if section != 'completion': if option in ['completion.height', 'completion.shrink']:
return
if option in ['height', 'shrink']:
self.update_geometry.emit() self.update_geometry.emit()
def _resize_columns(self): def _resize_columns(self):
@ -262,9 +260,9 @@ class CompletionView(QTreeView):
count = self.model().count() count = self.model().count()
if count == 0: if count == 0:
self.hide() self.hide()
elif count == 1 and config.get('completion', 'quick-complete'): elif count == 1 and config.val.completion.quick:
self.hide() self.hide()
elif config.get('completion', 'show') == 'auto': elif config.val.completion.show == 'auto':
self.show() self.show()
def set_model(self, model): def set_model(self, model):
@ -306,7 +304,7 @@ class CompletionView(QTreeView):
self._maybe_show() self._maybe_show()
def _maybe_show(self): def _maybe_show(self):
if (config.get('completion', 'show') == 'always' and if (config.val.completion.show == 'always' and
self.model().count() > 0): self.model().count() > 0):
self.show() self.show()
else: else:
@ -314,7 +312,7 @@ class CompletionView(QTreeView):
def _maybe_update_geometry(self): def _maybe_update_geometry(self):
"""Emit the update_geometry signal if the config says so.""" """Emit the update_geometry signal if the config says so."""
if config.get('completion', 'shrink'): if config.val.completion.shrink:
self.update_geometry.emit() self.update_geometry.emit()
@pyqtSlot() @pyqtSlot()
@ -329,14 +327,14 @@ class CompletionView(QTreeView):
def sizeHint(self): def sizeHint(self):
"""Get the completion size according to the config.""" """Get the completion size according to the config."""
# Get the configured height/percentage. # Get the configured height/percentage.
confheight = str(config.get('completion', 'height')) confheight = str(config.val.completion.height)
if confheight.endswith('%'): if confheight.endswith('%'):
perc = int(confheight.rstrip('%')) perc = int(confheight.rstrip('%'))
height = self.window().height() * perc / 100 height = self.window().height() * perc / 100
else: else:
height = int(confheight) height = int(confheight)
# Shrink to content size if needed and shrinking is enabled # Shrink to content size if needed and shrinking is enabled
if config.get('completion', 'shrink'): if config.val.completion.shrink:
contents_height = ( contents_height = (
self.viewportSizeHint().height() + self.viewportSizeHint().height() +
self.horizontalScrollBar().sizeHint().height()) self.horizontalScrollBar().sizeHint().height())

View File

@ -20,78 +20,63 @@
"""Functions that return config-related completion models.""" """Functions that return config-related completion models."""
from qutebrowser.config import configdata, configexc from qutebrowser.config import configdata, configexc
from qutebrowser.completion.models import completionmodel, listcategory from qutebrowser.completion.models import completionmodel, listcategory, util
from qutebrowser.utils import objreg from qutebrowser.commands import cmdutils
def section(): def option(*, info):
"""A CompletionModel filled with settings sections.""" """A CompletionModel filled with settings and their descriptions."""
model = completionmodel.CompletionModel(column_widths=(20, 70, 10)) model = completionmodel.CompletionModel(column_widths=(20, 70, 10))
sections = ((name, configdata.SECTION_DESC[name].splitlines()[0].strip()) options = ((opt.name, opt.description, info.config.get_str(opt.name))
for name in configdata.DATA) for opt in configdata.DATA.values())
model.add_category(listcategory.ListCategory("Sections", sorted(sections))) model.add_category(listcategory.ListCategory("Options", sorted(options)))
return model return model
def option(sectname): def value(optname, *_values, info):
"""A CompletionModel filled with settings and their descriptions.
Args:
sectname: The name of the config section this model shows.
"""
model = completionmodel.CompletionModel(column_widths=(20, 70, 10))
try:
sectdata = configdata.DATA[sectname]
except KeyError:
return None
options = []
for name in sectdata:
try:
desc = sectdata.descriptions[name]
except (KeyError, AttributeError):
# Some stuff (especially ValueList items) don't have a
# description.
desc = ""
else:
desc = desc.splitlines()[0]
config = objreg.get('config')
val = config.get(sectname, name, raw=True)
options.append((name, desc, val))
model.add_category(listcategory.ListCategory(sectname, sorted(options)))
return model
def value(sectname, optname):
"""A CompletionModel filled with setting values. """A CompletionModel filled with setting values.
Args: Args:
sectname: The name of the config section this model shows.
optname: The name of the config option this model shows. optname: The name of the config option this model shows.
_values: The values already provided on the command line.
info: A CompletionInfo instance.
""" """
model = completionmodel.CompletionModel(column_widths=(20, 70, 10)) model = completionmodel.CompletionModel(column_widths=(30, 70, 0))
config = objreg.get('config')
try: try:
current = config.get(sectname, optname, raw=True) or '""' current = info.config.get_str(optname) or '""'
except (configexc.NoSectionError, configexc.NoOptionError): except configexc.NoOptionError:
return None return None
default = configdata.DATA[sectname][optname].default() or '""' opt = info.config.get_opt(optname)
default = opt.typ.to_str(opt.default)
if hasattr(configdata.DATA[sectname], 'valtype'):
# Same type for all values (ValueList)
vals = configdata.DATA[sectname].valtype.complete()
else:
if optname is None:
raise ValueError("optname may only be None for ValueList "
"sections, but {} is not!".format(sectname))
# Different type for each value (KeyValue)
vals = configdata.DATA[sectname][optname].typ.complete()
cur_cat = listcategory.ListCategory("Current/Default", cur_cat = listcategory.ListCategory("Current/Default",
[(current, "Current value"), (default, "Default value")]) [(current, "Current value"), (default, "Default value")])
model.add_category(cur_cat) model.add_category(cur_cat)
vals = opt.typ.complete()
if vals is not None: if vals is not None:
model.add_category(listcategory.ListCategory("Completions", model.add_category(listcategory.ListCategory("Completions",
sorted(vals))) sorted(vals)))
return model return model
def bind(key, *, info):
"""A CompletionModel filled with all bindable commands and descriptions.
Args:
key: the key being bound.
"""
model = completionmodel.CompletionModel(column_widths=(20, 60, 20))
cmd_text = info.keyconf.get_command(key, 'normal')
if cmd_text:
cmd_name = cmd_text.split(' ')[0]
cmd = cmdutils.cmd_dict.get(cmd_name)
data = [(cmd_text, cmd.desc, key)]
model.add_category(listcategory.ListCategory("Current", data))
cmdlist = util.get_cmd_completions(info, include_hidden=True,
include_aliases=True)
model.add_category(listcategory.ListCategory("Commands", cmdlist))
return model

View File

@ -38,9 +38,9 @@ class HistoryCategory(QSqlQueryModel):
self.name = "History" self.name = "History"
# replace ' in timestamp-format to avoid breaking the query # replace ' in timestamp-format to avoid breaking the query
timestamp_format = config.val.completion.timestamp_format
timefmt = ("strftime('{}', last_atime, 'unixepoch', 'localtime')" timefmt = ("strftime('{}', last_atime, 'unixepoch', 'localtime')"
.format(config.get('completion', 'timestamp-format') .format(timestamp_format.replace("'", "`")))
.replace("'", "`")))
self._query = sql.Query(' '.join([ self._query = sql.Query(' '.join([
"SELECT url, title, {}".format(timefmt), "SELECT url, title, {}".format(timefmt),
@ -58,7 +58,7 @@ class HistoryCategory(QSqlQueryModel):
def _atime_expr(self): def _atime_expr(self):
"""If max_items is set, return an expression to limit the query.""" """If max_items is set, return an expression to limit the query."""
max_items = config.get('completion', 'web-history-max-items') max_items = config.val.completion.web_history_max_items
# HistoryCategory should not be added to the completion in that case. # HistoryCategory should not be added to the completion in that case.
assert max_items != 0 assert max_items != 0

View File

@ -19,46 +19,35 @@
"""Functions that return miscellaneous completion models.""" """Functions that return miscellaneous completion models."""
from qutebrowser.config import config, configdata from qutebrowser.config import configdata
from qutebrowser.utils import objreg, log from qutebrowser.utils import objreg, log
from qutebrowser.commands import cmdutils from qutebrowser.completion.models import completionmodel, listcategory, util
from qutebrowser.completion.models import completionmodel, listcategory
def command(): def command(*, info):
"""A CompletionModel filled with non-hidden commands and descriptions.""" """A CompletionModel filled with non-hidden commands and descriptions."""
model = completionmodel.CompletionModel(column_widths=(20, 60, 20)) model = completionmodel.CompletionModel(column_widths=(20, 60, 20))
cmdlist = _get_cmd_completions(include_aliases=True, include_hidden=False) cmdlist = util.get_cmd_completions(info, include_aliases=True,
include_hidden=False)
model.add_category(listcategory.ListCategory("Commands", cmdlist)) model.add_category(listcategory.ListCategory("Commands", cmdlist))
return model return model
def helptopic(): def helptopic(*, info):
"""A CompletionModel filled with help topics.""" """A CompletionModel filled with help topics."""
model = completionmodel.CompletionModel() model = completionmodel.CompletionModel()
cmdlist = _get_cmd_completions(include_aliases=False, include_hidden=True, cmdlist = util.get_cmd_completions(info, include_aliases=False,
prefix=':') include_hidden=True, prefix=':')
settings = [] settings = ((opt.name, opt.description)
for sectname, sectdata in configdata.DATA.items(): for opt in configdata.DATA.values())
for optname in sectdata:
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)
settings.append((name, desc))
model.add_category(listcategory.ListCategory("Commands", cmdlist)) model.add_category(listcategory.ListCategory("Commands", cmdlist))
model.add_category(listcategory.ListCategory("Settings", sorted(settings))) model.add_category(listcategory.ListCategory("Settings", sorted(settings)))
return model return model
def quickmark(): def quickmark(*, info=None): # pylint: disable=unused-argument
"""A CompletionModel filled with all quickmarks.""" """A CompletionModel filled with all quickmarks."""
def delete(data): def delete(data):
"""Delete a quickmark from the completion menu.""" """Delete a quickmark from the completion menu."""
@ -74,7 +63,7 @@ def quickmark():
return model return model
def bookmark(): def bookmark(*, info=None): # pylint: disable=unused-argument
"""A CompletionModel filled with all bookmarks.""" """A CompletionModel filled with all bookmarks."""
def delete(data): def delete(data):
"""Delete a bookmark from the completion menu.""" """Delete a bookmark from the completion menu."""
@ -90,7 +79,7 @@ def bookmark():
return model return model
def session(): def session(*, info=None): # pylint: disable=unused-argument
"""A CompletionModel filled with session names.""" """A CompletionModel filled with session names."""
model = completionmodel.CompletionModel() model = completionmodel.CompletionModel()
try: try:
@ -103,7 +92,7 @@ def session():
return model return model
def buffer(): def buffer(*, info=None): # pylint: disable=unused-argument
"""A model to complete on open tabs across all windows. """A model to complete on open tabs across all windows.
Used for switching the buffer command. Used for switching the buffer command.
@ -133,51 +122,3 @@ def buffer():
model.add_category(cat) model.add_category(cat)
return model return model
def bind(key):
"""A CompletionModel filled with all bindable commands and descriptions.
Args:
key: the key being bound.
"""
model = completionmodel.CompletionModel(column_widths=(20, 60, 20))
cmd_text = objreg.get('key-config').get_bindings_for('normal').get(key)
if cmd_text:
cmd_name = cmd_text.split(' ')[0]
cmd = cmdutils.cmd_dict.get(cmd_name)
data = [(cmd_text, cmd.desc, key)]
model.add_category(listcategory.ListCategory("Current", data))
cmdlist = _get_cmd_completions(include_hidden=True, include_aliases=True)
model.add_category(listcategory.ListCategory("Commands", cmdlist))
return model
def _get_cmd_completions(include_hidden, include_aliases, prefix=''):
"""Get a list of completions info for commands, sorted by name.
Args:
include_hidden: True to include commands annotated with hide=True.
include_aliases: True to include command aliases.
prefix: String to append to the command name.
Return: A list of tuples of form (name, description, bindings).
"""
assert cmdutils.cmd_dict
cmdlist = []
cmd_to_keys = objreg.get('key-config').get_reverse_bindings_for('normal')
for obj in set(cmdutils.cmd_dict.values()):
hide_debug = obj.debug and not objreg.get('args').debug
hide_hidden = obj.hide and not include_hidden
if not (hide_debug or hide_hidden or obj.deprecated):
bindings = ', '.join(cmd_to_keys.get(obj.name, []))
cmdlist.append((prefix + obj.name, obj.desc, bindings))
if include_aliases:
for name, cmd in config.section('aliases').items():
bindings = ', '.join(cmd_to_keys.get(name, []))
cmdlist.append((name, "Alias for '{}'".format(cmd), bindings))
return sorted(cmdlist)

View File

@ -22,7 +22,6 @@
from qutebrowser.completion.models import (completionmodel, listcategory, from qutebrowser.completion.models import (completionmodel, listcategory,
histcategory) histcategory)
from qutebrowser.utils import log, objreg from qutebrowser.utils import log, objreg
from qutebrowser.config import config
_URLCOL = 0 _URLCOL = 0
@ -50,7 +49,7 @@ def _delete_quickmark(data):
quickmark_manager.delete(name) quickmark_manager.delete(name)
def url(): def url(*, info):
"""A model which combines bookmarks, quickmarks and web history URLs. """A model which combines bookmarks, quickmarks and web history URLs.
Used for the `open` command. Used for the `open` command.
@ -66,7 +65,7 @@ def url():
model.add_category(listcategory.ListCategory( model.add_category(listcategory.ListCategory(
'Bookmarks', bookmarks, delete_func=_delete_bookmark)) 'Bookmarks', bookmarks, delete_func=_delete_bookmark))
if config.get('completion', 'web-history-max-items') != 0: if info.config.get('completion.web_history_max_items') != 0:
hist_cat = histcategory.HistoryCategory(delete_func=_delete_history) hist_cat = histcategory.HistoryCategory(delete_func=_delete_history)
model.add_category(hist_cat) model.add_category(hist_cat)
return model return model

View File

@ -0,0 +1,52 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2017 Ryan Roden-Corrent (rcorre) <ryan@rcorre.net>
#
# 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/>.
"""Utility functions for completion models."""
from qutebrowser.utils import objreg
from qutebrowser.commands import cmdutils
def get_cmd_completions(info, include_hidden, include_aliases, prefix=''):
"""Get a list of completions info for commands, sorted by name.
Args:
info: The CompletionInfo.
include_hidden: True to include commands annotated with hide=True.
include_aliases: True to include command aliases.
prefix: String to append to the command name.
Return: A list of tuples of form (name, description, bindings).
"""
assert cmdutils.cmd_dict
cmdlist = []
cmd_to_keys = info.keyconf.get_reverse_bindings_for('normal')
for obj in set(cmdutils.cmd_dict.values()):
hide_debug = obj.debug and not objreg.get('args').debug
hide_hidden = obj.hide and not include_hidden
if not (hide_debug or hide_hidden or obj.deprecated):
bindings = ', '.join(cmd_to_keys.get(obj.name, []))
cmdlist.append((prefix + obj.name, obj.desc, bindings))
if include_aliases:
for name, cmd in info.config.get('aliases').items():
bindings = ', '.join(cmd_to_keys.get(name, []))
cmdlist.append((name, "Alias for '{}'".format(cmd), bindings))
return sorted(cmdlist)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,760 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2017 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/>.
"""Code to show a diff of the legacy config format."""
import difflib
import os.path
import pygments
import pygments.lexers
import pygments.formatters
from qutebrowser.utils import standarddir
OLD_CONF = """
[general]
ignore-case = smart
startpage = https://start.duckduckgo.com
yank-ignored-url-parameters = ref,utm_source,utm_medium,utm_campaign,utm_term,utm_content
default-open-dispatcher =
default-page = ${startpage}
auto-search = naive
auto-save-config = true
auto-save-interval = 15000
editor = gvim -f "{}"
editor-encoding = utf-8
private-browsing = false
developer-extras = false
print-element-backgrounds = true
xss-auditing = false
default-encoding = iso-8859-1
new-instance-open-target = tab
new-instance-open-target.window = last-focused
log-javascript-console = debug
save-session = false
session-default-name =
url-incdec-segments = path,query
[ui]
history-session-interval = 30
zoom-levels = 25%,33%,50%,67%,75%,90%,100%,110%,125%,150%,175%,200%,250%,300%,400%,500%
default-zoom = 100%
downloads-position = top
status-position = bottom
message-timeout = 2000
message-unfocused = false
confirm-quit = never
zoom-text-only = false
frame-flattening = false
user-stylesheet =
hide-scrollbar = true
smooth-scrolling = false
remove-finished-downloads = -1
hide-statusbar = false
statusbar-padding = 1,1,0,0
window-title-format = {perc}{title}{title_sep}qutebrowser
modal-js-dialog = false
hide-wayland-decoration = false
keyhint-blacklist =
keyhint-delay = 500
prompt-radius = 8
prompt-filebrowser = true
[network]
do-not-track = true
accept-language = en-US,en
referer-header = same-domain
user-agent =
proxy = system
proxy-dns-requests = true
ssl-strict = ask
dns-prefetch = true
custom-headers =
netrc-file =
[completion]
show = always
download-path-suggestion = path
timestamp-format = %Y-%m-%d
height = 50%
cmd-history-max-items = 100
web-history-max-items = -1
quick-complete = true
shrink = false
scrollbar-width = 12
scrollbar-padding = 2
[input]
timeout = 500
partial-timeout = 5000
insert-mode-on-plugins = false
auto-leave-insert-mode = true
auto-insert-mode = false
forward-unbound-keys = auto
spatial-navigation = false
links-included-in-focus-chain = true
rocker-gestures = false
mouse-zoom-divider = 512
[tabs]
background-tabs = false
select-on-remove = next
new-tab-position = next
new-tab-position-explicit = last
last-close = ignore
show = always
show-switching-delay = 800
wrap = true
movable = true
close-mouse-button = middle
position = top
show-favicons = true
favicon-scale = 1.0
width = 20%
pinned-width = 43
indicator-width = 3
tabs-are-windows = false
title-format = {index}: {title}
title-format-pinned = {index}
title-alignment = left
mousewheel-tab-switching = true
padding = 0,0,5,5
indicator-padding = 2,2,0,4
[storage]
download-directory =
prompt-download-directory = true
remember-download-directory = true
maximum-pages-in-cache = 0
offline-web-application-cache = true
local-storage = true
cache-size =
[content]
allow-images = true
allow-javascript = true
allow-plugins = false
webgl = true
hyperlink-auditing = false
geolocation = ask
notifications = ask
media-capture = ask
javascript-can-open-windows-automatically = false
javascript-can-close-windows = false
javascript-can-access-clipboard = false
ignore-javascript-prompt = false
ignore-javascript-alert = false
local-content-can-access-remote-urls = false
local-content-can-access-file-urls = true
cookies-accept = no-3rdparty
cookies-store = true
host-block-lists = https://www.malwaredomainlist.com/hostslist/hosts.txt,http://someonewhocares.org/hosts/hosts,http://winhelp2002.mvps.org/hosts.zip,http://malwaredomains.lehigh.edu/files/justdomains.zip,https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&mimetype=plaintext
host-blocking-enabled = true
host-blocking-whitelist = piwik.org
enable-pdfjs = false
[hints]
border = 1px solid #E3BE23
mode = letter
chars = asdfghjkl
min-chars = 1
scatter = true
uppercase = false
dictionary = /usr/share/dict/words
auto-follow = unique-match
auto-follow-timeout = 0
next-regexes = \\bnext\\b,\\bmore\\b,\\bnewer\\b,\\b[>\u2192\u226b]\\b,\\b(>>|\xbb)\\b,\\bcontinue\\b
prev-regexes = \\bprev(ious)?\\b,\\bback\\b,\\bolder\\b,\\b[<\u2190\u226a]\\b,\\b(<<|\xab)\\b
find-implementation = python
hide-unmatched-rapid-hints = true
[searchengines]
DEFAULT = https://duckduckgo.com/?q={}
[aliases]
[colors]
completion.fg = white
completion.bg = #333333
completion.alternate-bg = #444444
completion.category.fg = white
completion.category.bg = qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #888888, stop:1 #505050)
completion.category.border.top = black
completion.category.border.bottom = ${completion.category.border.top}
completion.item.selected.fg = black
completion.item.selected.bg = #e8c000
completion.item.selected.border.top = #bbbb00
completion.item.selected.border.bottom = ${completion.item.selected.border.top}
completion.match.fg = #ff4444
completion.scrollbar.fg = ${completion.fg}
completion.scrollbar.bg = ${completion.bg}
statusbar.fg = white
statusbar.bg = black
statusbar.fg.private = ${statusbar.fg}
statusbar.bg.private = #666666
statusbar.fg.insert = ${statusbar.fg}
statusbar.bg.insert = darkgreen
statusbar.fg.command = ${statusbar.fg}
statusbar.bg.command = ${statusbar.bg}
statusbar.fg.command.private = ${statusbar.fg.private}
statusbar.bg.command.private = ${statusbar.bg.private}
statusbar.fg.caret = ${statusbar.fg}
statusbar.bg.caret = purple
statusbar.fg.caret-selection = ${statusbar.fg}
statusbar.bg.caret-selection = #a12dff
statusbar.progress.bg = white
statusbar.url.fg = ${statusbar.fg}
statusbar.url.fg.success = white
statusbar.url.fg.success.https = lime
statusbar.url.fg.error = orange
statusbar.url.fg.warn = yellow
statusbar.url.fg.hover = aqua
tabs.fg.odd = white
tabs.bg.odd = grey
tabs.fg.even = white
tabs.bg.even = darkgrey
tabs.fg.selected.odd = white
tabs.bg.selected.odd = black
tabs.fg.selected.even = ${tabs.fg.selected.odd}
tabs.bg.selected.even = ${tabs.bg.selected.odd}
tabs.bg.bar = #555555
tabs.indicator.start = #0000aa
tabs.indicator.stop = #00aa00
tabs.indicator.error = #ff0000
tabs.indicator.system = rgb
hints.fg = black
hints.bg = qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 rgba(255, 247, 133, 0.8), stop:1 rgba(255, 197, 66, 0.8))
hints.fg.match = green
downloads.bg.bar = black
downloads.fg.start = white
downloads.bg.start = #0000aa
downloads.fg.stop = ${downloads.fg.start}
downloads.bg.stop = #00aa00
downloads.fg.system = rgb
downloads.bg.system = rgb
downloads.fg.error = white
downloads.bg.error = red
webpage.bg = white
keyhint.fg = #FFFFFF
keyhint.fg.suffix = #FFFF00
keyhint.bg = rgba(0, 0, 0, 80%)
messages.fg.error = white
messages.bg.error = red
messages.border.error = #bb0000
messages.fg.warning = white
messages.bg.warning = darkorange
messages.border.warning = #d47300
messages.fg.info = white
messages.bg.info = black
messages.border.info = #333333
prompts.fg = white
prompts.bg = darkblue
prompts.selected.bg = #308cc6
[fonts]
_monospace = xos4 Terminus, Terminus, Monospace, "DejaVu Sans Mono", Monaco, "Bitstream Vera Sans Mono", "Andale Mono", "Courier New", Courier, "Liberation Mono", monospace, Fixed, Consolas, Terminal
completion = 8pt ${_monospace}
completion.category = bold ${completion}
tabbar = 8pt ${_monospace}
statusbar = 8pt ${_monospace}
downloads = 8pt ${_monospace}
hints = bold 13px ${_monospace}
debug-console = 8pt ${_monospace}
web-family-standard =
web-family-fixed =
web-family-serif =
web-family-sans-serif =
web-family-cursive =
web-family-fantasy =
web-size-minimum = 0
web-size-minimum-logical = 6
web-size-default = 16
web-size-default-fixed = 13
keyhint = 8pt ${_monospace}
messages.error = 8pt ${_monospace}
messages.warning = 8pt ${_monospace}
messages.info = 8pt ${_monospace}
prompts = 8pt sans-serif
"""
OLD_KEYS_CONF = """
[!normal]
leave-mode
<escape>
<ctrl-[>
[normal]
clear-keychain ;; search ;; fullscreen --leave
<escape>
<ctrl-[>
set-cmd-text -s :open
o
set-cmd-text :open {url:pretty}
go
set-cmd-text -s :open -t
O
set-cmd-text :open -t -i {url:pretty}
gO
set-cmd-text -s :open -b
xo
set-cmd-text :open -b -i {url:pretty}
xO
set-cmd-text -s :open -w
wo
set-cmd-text :open -w {url:pretty}
wO
set-cmd-text /
/
set-cmd-text ?
?
set-cmd-text :
:
open -t
ga
<ctrl-t>
open -w
<ctrl-n>
tab-close
d
<ctrl-w>
tab-close -o
D
tab-only
co
tab-focus
T
tab-move
gm
tab-move -
gl
tab-move +
gr
tab-next
J
<ctrl-pgdown>
tab-prev
K
<ctrl-pgup>
tab-clone
gC
reload
r
<f5>
reload -f
R
<ctrl-f5>
back
H
<back>
back -t
th
back -w
wh
forward
L
<forward>
forward -t
tl
forward -w
wl
fullscreen
<f11>
hint
f
hint all tab
F
hint all window
wf
hint all tab-bg
;b
hint all tab-fg
;f
hint all hover
;h
hint images
;i
hint images tab
;I
hint links fill :open {hint-url}
;o
hint links fill :open -t -i {hint-url}
;O
hint links yank
;y
hint links yank-primary
;Y
hint --rapid links tab-bg
;r
hint --rapid links window
;R
hint links download
;d
hint inputs
;t
scroll left
h
scroll down
j
scroll up
k
scroll right
l
undo
u
<ctrl-shift-t>
scroll-perc 0
gg
scroll-perc
G
search-next
n
search-prev
N
enter-mode insert
i
enter-mode caret
v
enter-mode set_mark
`
enter-mode jump_mark
'
yank
yy
yank -s
yY
yank title
yt
yank title -s
yT
yank domain
yd
yank domain -s
yD
yank pretty-url
yp
yank pretty-url -s
yP
open -- {clipboard}
pp
open -- {primary}
pP
open -t -- {clipboard}
Pp
open -t -- {primary}
PP
open -w -- {clipboard}
wp
open -w -- {primary}
wP
quickmark-save
m
set-cmd-text -s :quickmark-load
b
set-cmd-text -s :quickmark-load -t
B
set-cmd-text -s :quickmark-load -w
wb
bookmark-add
M
set-cmd-text -s :bookmark-load
gb
set-cmd-text -s :bookmark-load -t
gB
set-cmd-text -s :bookmark-load -w
wB
save
sf
set-cmd-text -s :set
ss
set-cmd-text -s :set -t
sl
set-cmd-text -s :bind
sk
zoom-out
-
zoom-in
+
zoom
=
navigate prev
[[
navigate next
]]
navigate prev -t
{{
navigate next -t
}}
navigate up
gu
navigate up -t
gU
navigate increment
<ctrl-a>
navigate decrement
<ctrl-x>
inspector
wi
download
gd
download-cancel
ad
download-clear
cd
view-source
gf
set-cmd-text -s :buffer
gt
tab-focus last
<ctrl-tab>
<ctrl-6>
<ctrl-^>
enter-mode passthrough
<ctrl-v>
quit
<ctrl-q>
ZQ
wq
ZZ
scroll-page 0 1
<ctrl-f>
scroll-page 0 -1
<ctrl-b>
scroll-page 0 0.5
<ctrl-d>
scroll-page 0 -0.5
<ctrl-u>
tab-focus 1
<alt-1>
g0
g^
tab-focus 2
<alt-2>
tab-focus 3
<alt-3>
tab-focus 4
<alt-4>
tab-focus 5
<alt-5>
tab-focus 6
<alt-6>
tab-focus 7
<alt-7>
tab-focus 8
<alt-8>
tab-focus -1
<alt-9>
g$
home
<ctrl-h>
stop
<ctrl-s>
print
<ctrl-alt-p>
open qute://settings
Ss
follow-selected
<return>
<ctrl-m>
<ctrl-j>
<shift-return>
<enter>
<shift-enter>
follow-selected -t
<ctrl-return>
<ctrl-enter>
repeat-command
.
tab-pin
<ctrl-p>
record-macro
q
run-macro
@
[insert]
open-editor
<ctrl-e>
insert-text {primary}
<shift-ins>
[hint]
follow-hint
<return>
<ctrl-m>
<ctrl-j>
<shift-return>
<enter>
<shift-enter>
hint --rapid links tab-bg
<ctrl-r>
hint links
<ctrl-f>
hint all tab-bg
<ctrl-b>
[passthrough]
[command]
command-history-prev
<ctrl-p>
command-history-next
<ctrl-n>
completion-item-focus prev
<shift-tab>
<up>
completion-item-focus next
<tab>
<down>
completion-item-focus next-category
<ctrl-tab>
completion-item-focus prev-category
<ctrl-shift-tab>
completion-item-del
<ctrl-d>
command-accept
<return>
<ctrl-m>
<ctrl-j>
<shift-return>
<enter>
<shift-enter>
[prompt]
prompt-accept
<return>
<ctrl-m>
<ctrl-j>
<shift-return>
<enter>
<shift-enter>
prompt-accept yes
y
prompt-accept no
n
prompt-open-download
<ctrl-x>
prompt-item-focus prev
<shift-tab>
<up>
prompt-item-focus next
<tab>
<down>
[command,prompt]
rl-backward-char
<ctrl-b>
rl-forward-char
<ctrl-f>
rl-backward-word
<alt-b>
rl-forward-word
<alt-f>
rl-beginning-of-line
<ctrl-a>
rl-end-of-line
<ctrl-e>
rl-unix-line-discard
<ctrl-u>
rl-kill-line
<ctrl-k>
rl-kill-word
<alt-d>
rl-unix-word-rubout
<ctrl-w>
rl-backward-kill-word
<alt-backspace>
rl-yank
<ctrl-y>
rl-delete-char
<ctrl-?>
rl-backward-delete-char
<ctrl-h>
[caret]
toggle-selection
v
<space>
drop-selection
<ctrl-space>
enter-mode normal
c
move-to-next-line
j
move-to-prev-line
k
move-to-next-char
l
move-to-prev-char
h
move-to-end-of-word
e
move-to-next-word
w
move-to-prev-word
b
move-to-start-of-next-block
]
move-to-start-of-prev-block
[
move-to-end-of-next-block
}
move-to-end-of-prev-block
{
move-to-start-of-line
0
move-to-end-of-line
$
move-to-start-of-document
gg
move-to-end-of-document
G
yank selection -s
Y
yank selection
y
<return>
<ctrl-m>
<ctrl-j>
<shift-return>
<enter>
<shift-enter>
scroll left
H
scroll down
J
scroll up
K
scroll right
L
"""
def get_diff():
"""Get a HTML diff for the old config files."""
old_conf_lines = []
old_key_lines = []
for filename, dest in [('qutebrowser.conf', old_conf_lines),
('keys.conf', old_key_lines)]:
path = os.path.join(standarddir.config(), filename)
with open(path, 'r', encoding='utf-8') as f:
for line in f:
if not line.strip() or line.startswith('#'):
continue
dest.append(line.rstrip())
conf_delta = difflib.unified_diff(OLD_CONF.lstrip().splitlines(),
old_conf_lines)
key_delta = difflib.unified_diff(OLD_KEYS_CONF.lstrip().splitlines(),
old_key_lines)
conf_diff = '\n'.join(conf_delta)
key_diff = '\n'.join(key_delta)
# pylint: disable=no-member
# WORKAROUND for https://bitbucket.org/logilab/pylint/issue/491/
lexer = pygments.lexers.DiffLexer()
formatter = pygments.formatters.HtmlFormatter(
full=True, linenos='table',
title='Config diff')
# pylint: enable=no-member
return pygments.highlight(conf_diff + key_diff, lexer, formatter)

View File

@ -41,46 +41,79 @@ class ValidationError(Error):
"""Raised when a value for a config type was invalid. """Raised when a value for a config type was invalid.
Attributes: Attributes:
section: Section in which the error occurred (added when catching and value: Config value that triggered the error.
re-raising the exception). msg: Additional error message.
option: Option in which the error occurred.
""" """
def __init__(self, value, msg): def __init__(self, value, msg):
super().__init__("Invalid value '{}' - {}".format(value, msg)) super().__init__("Invalid value '{}' - {}".format(value, msg))
self.section = None
self.option = None self.option = None
class NoSectionError(Error): class KeybindingError(Error):
"""Raised when no section matches a requested option.""" """Raised for issues with keybindings."""
def __init__(self, section):
super().__init__("Section {!r} does not exist!".format(section)) class DuplicateKeyError(KeybindingError):
self.section = section
"""Raised when there was a duplicate key."""
def __init__(self, key):
super().__init__("Duplicate key {}".format(key))
class NoOptionError(Error): class NoOptionError(Error):
"""Raised when an option was not found.""" """Raised when an option was not found."""
def __init__(self, option, section): def __init__(self, option):
super().__init__("No option {!r} in section {!r}".format( super().__init__("No option {!r}".format(option))
option, section))
self.option = option self.option = option
self.section = section
class InterpolationSyntaxError(Error): class ConfigErrorDesc:
"""Raised when the source text contains invalid syntax. """A description of an error happening while reading the config.
Current implementation raises this exception when the source text into Attributes:
which substitutions are made does not conform to the required syntax. text: The text to show.
exception: The exception which happened.
traceback: The formatted traceback of the exception.
""" """
def __init__(self, option, section, msg): def __init__(self, text, exception, traceback=None):
super().__init__(msg) self.text = text
self.option = option self.exception = exception
self.section = section self.traceback = traceback
class ConfigFileErrors(Error):
"""Raised when multiple errors occurred inside the config."""
def __init__(self, basename, errors):
super().__init__("Errors occurred while reading {}".format(basename))
self.basename = basename
self.errors = errors
def to_html(self):
"""Get the error texts as a HTML snippet."""
from qutebrowser.utils import jinja
template = jinja.environment.from_string("""
Errors occurred while reading {{ basename }}:
<ul>
{% for error in errors %}
<li>
<b>{{ error.text }}</b>: {{ error.exception }}
{% if error.traceback != none %}
<pre>
""".rstrip() + "\n{{ error.traceback }}" + """
</pre>
{% endif %}
</li>
{% endfor %}
</ul>
""")
return template.render(basename=self.basename, errors=self.errors)

View File

@ -0,0 +1,235 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2017 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/>.
"""Configuration files residing on disk."""
import types
import os.path
import textwrap
import traceback
import configparser
import contextlib
import yaml
from PyQt5.QtCore import QSettings
from qutebrowser.config import configexc
from qutebrowser.utils import objreg, standarddir, utils, qtutils
class StateConfig(configparser.ConfigParser):
"""The "state" file saving various application state."""
def __init__(self):
super().__init__()
save_manager = objreg.get('save-manager')
self._filename = os.path.join(standarddir.data(), 'state')
self.read(self._filename, encoding='utf-8')
for sect in ['general', 'geometry']:
try:
self.add_section(sect)
except configparser.DuplicateSectionError:
pass
# See commit a98060e020a4ba83b663813a4b9404edb47f28ad.
self['general'].pop('fooled', None)
save_manager.add_saveable('state-config', self._save)
def _save(self):
"""Save the state file to the configured location."""
with open(self._filename, 'w', encoding='utf-8') as f:
self.write(f)
class YamlConfig:
"""A config stored on disk as YAML file.
Class attributes:
VERSION: The current version number of the config file.
"""
VERSION = 1
def __init__(self):
save_manager = objreg.get('save-manager')
self._filename = os.path.join(standarddir.config(auto=True),
'autoconfig.yml')
save_manager.add_saveable('yaml-config', self._save)
self.values = {}
def _save(self):
"""Save the changed settings to the YAML file."""
data = {'config_version': self.VERSION, 'global': self.values}
with qtutils.savefile_open(self._filename) as f:
f.write(textwrap.dedent("""
# DO NOT edit this file by hand, qutebrowser will overwrite it.
# Instead, create a config.py - see :help for details.
""".lstrip('\n')))
utils.yaml_dump(data, f)
def load(self):
"""Load self.values from the configured YAML file."""
try:
with open(self._filename, 'r', encoding='utf-8') as f:
yaml_data = utils.yaml_load(f)
except FileNotFoundError:
return
except OSError as e:
desc = configexc.ConfigErrorDesc("While reading", e)
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
except yaml.YAMLError as e:
desc = configexc.ConfigErrorDesc("While parsing", e)
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
try:
global_obj = yaml_data['global']
except KeyError:
desc = configexc.ConfigErrorDesc(
"While loading data",
"Toplevel object does not contain 'global' key")
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
except TypeError:
desc = configexc.ConfigErrorDesc("While loading data",
"Toplevel object is not a dict")
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
if not isinstance(global_obj, dict):
desc = configexc.ConfigErrorDesc(
"While loading data",
"'global' object is not a dict")
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
self.values = global_obj
class ConfigAPI:
"""Object which gets passed to config.py as "config" object.
This is a small wrapper over the Config object, but with more
straightforward method names (get/set call get_obj/set_obj) and a more
shallow API.
Attributes:
_config: The main Config object to use.
_keyconfig: The KeyConfig object.
load_autoconfig: Whether autoconfig.yml should be loaded.
errors: Errors which occurred while setting options.
"""
def __init__(self, config, keyconfig):
self._config = config
self._keyconfig = keyconfig
self.load_autoconfig = True
self.errors = []
@contextlib.contextmanager
def _handle_error(self, action, name):
try:
yield
except configexc.Error as e:
text = "While {} '{}'".format(action, name)
self.errors.append(configexc.ConfigErrorDesc(text, e))
def finalize(self):
"""Do work which needs to be done after reading config.py."""
self._config.update_mutables()
def get(self, name):
with self._handle_error('getting', name):
return self._config.get_obj(name)
def set(self, name, value):
with self._handle_error('setting', name):
self._config.set_obj(name, value)
def bind(self, key, command, *, mode, force=False):
with self._handle_error('binding', key):
self._keyconfig.bind(key, command, mode=mode, force=force)
def unbind(self, key, *, mode):
with self._handle_error('unbinding', key):
self._keyconfig.unbind(key, mode=mode)
def read_config_py(filename=None):
"""Read a config.py file."""
from qutebrowser.config import config
api = ConfigAPI(config.instance, config.key_instance)
if filename is None:
filename = os.path.join(standarddir.config(), 'config.py')
if not os.path.exists(filename):
return api
container = config.ConfigContainer(config.instance, configapi=api)
basename = os.path.basename(filename)
module = types.ModuleType('config')
module.config = api
module.c = container
module.__file__ = filename
try:
with open(filename, mode='rb') as f:
source = f.read()
except OSError as e:
text = "Error while reading {}".format(basename)
desc = configexc.ConfigErrorDesc(text, e)
raise configexc.ConfigFileErrors(basename, [desc])
try:
code = compile(source, filename, 'exec')
except (ValueError, TypeError) as e:
# source contains NUL bytes
desc = configexc.ConfigErrorDesc("Error while compiling", e)
raise configexc.ConfigFileErrors(basename, [desc])
except SyntaxError as e:
desc = configexc.ConfigErrorDesc("Syntax Error", e,
traceback=traceback.format_exc())
raise configexc.ConfigFileErrors(basename, [desc])
try:
exec(code, module.__dict__)
except Exception as e:
api.errors.append(configexc.ConfigErrorDesc(
"Unhandled exception",
exception=e, traceback=traceback.format_exc()))
api.finalize()
return api
def init():
"""Initialize config storage not related to the main config."""
state = StateConfig()
objreg.register('state-config', state)
# 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/qutebrowser/qutebrowser/issues/515
path = os.path.join(standarddir.config(auto=True), 'qsettings')
for fmt in [QSettings.NativeFormat, QSettings.IniFormat]:
QSettings.setPath(fmt, QSettings.UserScope, path)

File diff suppressed because it is too large Load Diff

View File

@ -1,20 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2017 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 different configuration formats."""

View File

@ -1,74 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2017 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/>.
"""Parsers for INI-like config files, based on Python's ConfigParser."""
import os
import os.path
import configparser
from qutebrowser.utils import log, utils, qtutils
class ReadConfigParser(configparser.ConfigParser):
"""Our own ConfigParser subclass to read the main config.
Attributes:
_configdir: The directory to read the config from.
_fname: The filename of the config.
_configfile: The config file path.
"""
def __init__(self, configdir, fname):
"""Config constructor.
Args:
configdir: Directory to read the config from.
fname: Filename of the config file.
"""
super().__init__(interpolation=None, comment_prefixes='#')
self.optionxform = lambda opt: opt # be case-insensitive
self._configdir = configdir
self._fname = fname
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))
self.read(self._configfile, encoding='utf-8')
def __repr__(self):
return utils.get_repr(self, constructor=True,
configdir=self._configdir, fname=self._fname)
class ReadWriteConfigParser(ReadConfigParser):
"""ConfigParser subclass used for auxiliary 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))
with qtutils.savefile_open(self._configfile) as f:
self.write(f)

View File

@ -1,486 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2017 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 the key configuration."""
import collections
import os.path
import itertools
import sys
from PyQt5.QtCore import pyqtSignal, QObject
from qutebrowser.config import configdata, textwrapper
from qutebrowser.commands import cmdutils, cmdexc
from qutebrowser.utils import (log, utils, qtutils, message, usertypes, objreg,
standarddir, error)
from qutebrowser.completion.models import miscmodels
def init(parent=None):
"""Read and save keybindings.
Args:
parent: The parent to use for the KeyConfigParser.
"""
args = objreg.get('args')
try:
key_config = KeyConfigParser(standarddir.config(), 'keys.conf',
args.relaxed_config, parent=parent)
except (KeyConfigError, UnicodeDecodeError) 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)
# We didn't really initialize much so far, so we just quit hard.
sys.exit(usertypes.Exit.err_key_config)
else:
objreg.register('key-config', key_config)
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)
class KeyConfigError(Exception):
"""Raised on errors with the key config.
Attributes:
lineno: The config line in which the exception occurred.
"""
def __init__(self, msg=None):
super().__init__(msg)
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.
Attributes:
_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.
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):
"""Constructor.
Args:
configdir: The directory to save the configs in.
fname: The filename of the config.
relaxed: If given, unknown 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.
self.keybindings = collections.OrderedDict()
self._configfile = os.path.join(configdir, fname)
if not os.path.exists(self._configfile):
self._load_default()
else:
self._read(relaxed)
self._load_default(only_new=True)
log.init.debug("Loaded bindings: {}".format(self.keybindings))
def __str__(self):
"""Get the config as string."""
lines = configdata.KEY_FIRST_COMMENT.strip('\n').splitlines()
lines.append('')
for sectname, sect in self.keybindings.items():
lines.append('[{}]'.format(sectname))
lines += self._str_section_desc(sectname)
lines.append('')
data = collections.OrderedDict()
for key, cmd in sect.items():
if cmd in data:
data[cmd].append(key)
else:
data[cmd] = [key]
for cmd, keys in data.items():
lines.append(cmd)
for k in keys:
lines.append(' ' * 4 + k)
lines.append('')
return '\n'.join(lines) + '\n'
def __repr__(self):
return utils.get_repr(self, constructor=True,
configfile=self._configfile)
def _str_section_desc(self, sectname):
"""Get the section description string for sectname."""
wrapper = textwrapper.TextWrapper()
lines = []
try:
seclines = configdata.KEY_SECTION_DESC[sectname].splitlines()
except KeyError:
return []
else:
for secline in seclines:
if 'http://' in secline or 'https://' in secline:
lines.append('# ' + secline)
else:
lines += wrapper.wrap(secline)
return lines
def save(self):
"""Save the key config file."""
log.destroy.debug("Saving key config to {}".format(self._configfile))
try:
with qtutils.savefile_open(self._configfile,
encoding='utf-8') as f:
data = str(self)
f.write(data)
except OSError as e:
message.error("Could not save key config: {}".format(e))
@cmdutils.register(instance='key-config', maxsplit=1, no_cmd_split=True,
no_replace_variables=True)
@cmdutils.argument('command', completion=miscmodels.bind)
def bind(self, key, command=None, *, mode='normal', force=False):
"""Bind a key to a command.
Args:
key: The keychain or special key (inside `<...>`) to bind.
command: The command to execute, with optional args, or None to
print the current binding.
mode: A comma-separated list of modes to bind the key in
(default: `normal`).
force: Rebind the key if it is already bound.
"""
if utils.is_special_key(key):
# <Ctrl-t>, <ctrl-T>, and <ctrl-t> should be considered equivalent
key = key.lower()
if command is None:
cmd = self.get_bindings_for(mode).get(key, None)
if cmd is None:
message.info("{} is unbound in {} mode".format(key, mode))
else:
message.info("{} is bound to '{}' in {} mode".format(key, cmd,
mode))
return
modenames = self._normalize_sectname(mode).split(',')
for m in modenames:
if m not in configdata.KEY_DATA:
raise cmdexc.CommandError("Invalid mode {}!".format(m))
try:
modes = [usertypes.KeyMode[m] for m in modenames]
self._validate_command(command, modes)
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)))
except KeyConfigError as e:
raise cmdexc.CommandError(e)
for m in modenames:
self.changed.emit(m)
self._mark_config_dirty()
@cmdutils.register(instance='key-config')
def unbind(self, key, mode='normal'):
"""Unbind a keychain.
Args:
key: The keychain or special key (inside <...>) to unbind.
mode: A comma-separated list of modes to unbind the key in
(default: `normal`).
"""
if utils.is_special_key(key):
# <Ctrl-t>, <ctrl-T>, and <ctrl-t> should be considered equivalent
key = key.lower()
mode = self._normalize_sectname(mode)
for m in mode.split(','):
if m not in configdata.KEY_DATA:
raise cmdexc.CommandError("Invalid mode {}!".format(m))
try:
sect = self.keybindings[mode]
except KeyError:
raise cmdexc.CommandError("Can't find mode section '{}'!".format(
mode))
try:
del sect[key]
except KeyError:
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'."""
if s.startswith('!'):
inverted = True
s = s[1:]
else:
inverted = False
sections = ','.join(sorted(s.split(',')))
if inverted:
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()
mark_dirty = False
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
mark_dirty = True
for sectname, sect in bindings_to_add.items():
if not sect:
if not only_new:
self.keybindings[sectname] = collections.OrderedDict()
else:
for keychain, command in sect.items():
self._add_binding(sectname, keychain, command)
self.changed.emit(sectname)
if mark_dirty:
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.
"""
if utils.is_special_key(keychain):
keychain = keychain.lower()
try:
bindings = self.keybindings[sectname]
except KeyError:
return True
if keychain in bindings:
return False
else:
return command not in bindings.values()
def _read(self, relaxed=False):
"""Read the config file from disk and parse it.
Args:
relaxed: Ignore unknown commands.
"""
try:
with open(self._configfile, 'r', encoding='utf-8') as f:
for i, line in enumerate(f):
line = line.rstrip()
try:
if not line.strip() or line.startswith('#'):
continue
elif line.startswith('[') and line.endswith(']'):
sectname = line[1:-1]
self._cur_section = self._normalize_sectname(
sectname)
elif line.startswith((' ', '\t')):
line = line.strip()
self._read_keybinding(line)
else:
line = line.strip()
self._read_command(line)
except (KeyConfigError, cmdexc.CommandError) as e:
if relaxed:
continue
else:
e.lineno = i
raise
except OSError:
log.keyboard.exception("Failed to read key bindings!")
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, modes=None):
"""Check if a given command is valid.
Args:
line: The commandline to validate.
modes: A list of modes to validate the commands for, or None.
"""
from qutebrowser.config import config
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:
aliases = config.section('aliases')
if cmd in cmdutils.cmd_dict:
cmdname = cmd
elif cmd in aliases:
cmdname = aliases[cmd].split(maxsplit=1)[0].strip()
else:
raise KeyConfigError("Invalid command '{}'!".format(cmd))
cmd_obj = cmdutils.cmd_dict[cmdname]
for m in modes or []:
cmd_obj.validate_mode(m)
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:
for rgx, repl in configdata.CHANGED_KEY_COMMANDS:
if rgx.match(line):
line = rgx.sub(repl, line)
self._mark_config_dirty()
break
self._validate_command(line)
self._cur_command = line
def _read_keybinding(self, line):
"""Read a key binding from a line."""
if self._cur_command is None:
raise KeyConfigError("Got key binding '{}' 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):
"""Add a new binding from keychain to command in section sectname."""
if utils.is_special_key(keychain):
# <Ctrl-t>, <ctrl-T>, and <ctrl-t> should be considered equivalent
keychain = keychain.lower()
log.keyboard.vdebug("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]
self.keybindings[sectname][keychain] = command
def get_bindings_for(self, section):
"""Get a dict with all merged key bindings for a section."""
bindings = {}
for sectstring, d in self.keybindings.items():
if sectstring.startswith('!'):
inverted = True
sectstring = sectstring[1:]
else:
inverted = False
sects = [s.strip() for s in sectstring.split(',')]
matches = any(s == section for s in sects)
if (not inverted and matches) or (inverted and not matches):
bindings.update(d)
try:
bindings.update(self.keybindings['all'])
except KeyError:
pass
bindings = {k: v for k, v in bindings.items()
if v != self.UNBOUND_COMMAND}
return bindings
def get_reverse_bindings_for(self, section):
"""Get a dict of commands to a list of bindings for the section."""
cmd_to_keys = {}
for key, full_cmd in self.get_bindings_for(section).items():
for cmd in full_cmd.split(';;'):
cmd = cmd.strip()
cmd_to_keys.setdefault(cmd, [])
# put special bindings last
if utils.is_special_key(key):
cmd_to_keys[cmd].append(key)
else:
cmd_to_keys[cmd].insert(0, key)
return cmd_to_keys

View File

@ -1,231 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2017 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/>.
"""Setting sections used for qutebrowser."""
import collections
from qutebrowser.config import value as confvalue
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
descriptions: A dict with the description strings for the keys.
"""
def __init__(self):
self.values = None
self.descriptions = {}
self._readonly = False
def __getitem__(self, key):
"""Get the value for key.
Args:
key: The key to get a value for, as a string.
Return:
The value, as value class.
"""
return self.values[key]
def __iter__(self):
"""Iterate over all set values."""
return iter(self.values)
def __bool__(self):
"""Get boolean state of section."""
return bool(self.values)
def __contains__(self, key):
"""Return whether the section contains a given key."""
return key in self.values
def items(self):
"""Get dict items."""
return self.values.items()
def keys(self):
"""Get value keys."""
return self.values.keys()
def delete(self, key):
"""Delete item with given key."""
del self.values[key]
def setv(self, layer, key, value, interpolated):
"""Set the value on a layer.
Args:
layer: The layer to set the value on, an element name of the
ValueLayers dict.
key: The key of the element to set.
value: The value to set.
interpolated: The interpolated value, for checking, or None.
"""
raise NotImplementedError
def dump_userconfig(self):
"""Dump the part of the config which was changed by the user.
Return:
A list of (key, valuestr) tuples.
"""
raise NotImplementedError
class KeyValue(Section):
"""Representation of a section with ordinary key-value mappings.
This is a section which contains normal "key = value" pairs with a fixed
set of keys.
"""
def __init__(self, *defaults, readonly=False):
"""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()
for (k, v, desc) in defaults:
assert k not in self.values, k
self.values[k] = v
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):
changed = []
for k, v in self.items():
vals = v.values
if vals['temp'] is not None and vals['temp'] != vals['default']:
changed.append((k, vals['temp']))
elif vals['conf'] is not None and vals['conf'] != vals['default']:
changed.append((k, vals['conf']))
return changed
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
searchengines.
They basically consist of two different SettingValues.
Attributes:
layers: An OrderedDict of the config layers.
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):
"""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
self.layers = collections.OrderedDict([
('default', collections.OrderedDict()),
('conf', collections.OrderedDict()),
('temp', collections.OrderedDict()),
])
defaultlayer = self.layers['default']
for key, value in defaults:
assert key not in defaultlayer, key
defaultlayer[key] = confvalue.SettingValue(valtype, value)
self.values = collections.ChainMap(
self.layers['temp'], self.layers['conf'], self.layers['default'])
def _ordered_values(self):
"""Get ordered values in layers.
This is more expensive than the ChainMap, but we need this for
iterating/items/etc. when order matters.
"""
if self._ordered_value_cache is None:
self._ordered_value_cache = collections.OrderedDict()
for layer in self.layers.values():
self._ordered_value_cache.update(layer)
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)
else:
val = confvalue.SettingValue(self.valtype)
val.setv(layer, value, interpolated)
self.layers[layer][key] = val
self._ordered_value_cache = None
def dump_userconfig(self):
changed = []
mapping = collections.ChainMap(self.layers['temp'],
self.layers['conf'])
for k, v in mapping.items():
try:
if v.value() != self.layers['default'][k].value():
changed.append((k, v.value()))
except KeyError:
changed.append((k, v.value()))
return changed
def __iter__(self):
"""Iterate over all set values."""
return self._ordered_values().__iter__()
def items(self):
"""Get dict items."""
return self._ordered_values().items()
def keys(self):
"""Get value keys."""
return self._ordered_values().keys()

View File

@ -1,100 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2017 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 the look&feel of qutebrowser."""
import functools
import collections
import jinja2
import sip
from PyQt5.QtGui import QColor
from qutebrowser.config import config
from qutebrowser.utils import log, objreg
@functools.lru_cache(maxsize=16)
def get_stylesheet(template_str):
"""Format a stylesheet based on a template.
Args:
template_str: The stylesheet template as string.
Return:
The formatted template as string.
"""
colordict = ColorDict(config.section('colors'))
template = jinja2.Template(template_str)
return template.render(color=colordict, font=config.section('fonts'),
config=objreg.get('config'))
def set_register_stylesheet(obj):
"""Set the stylesheet for an object based on it's STYLESHEET attribute.
Also, register an update when the config is changed.
Args:
obj: The object to set the stylesheet for and register.
Must have a STYLESHEET attribute.
"""
qss = get_stylesheet(obj.STYLESHEET)
log.config.vdebug("stylesheet for {}: {}".format(
obj.__class__.__name__, qss))
obj.setStyleSheet(qss)
objreg.get('config').changed.connect(
functools.partial(_update_stylesheet, obj))
def _update_stylesheet(obj):
"""Update the stylesheet for obj."""
get_stylesheet.cache_clear()
if not sip.isdeleted(obj):
obj.setStyleSheet(get_stylesheet(obj.STYLESHEET))
class ColorDict(collections.UserDict):
"""A dict aimed at Qt stylesheet colors."""
def __getitem__(self, key):
"""Override dict __getitem__.
Args:
key: The key to get from the dict.
Return:
If a value wasn't found, return an empty string.
(Color not defined, so no output in the stylesheet)
else, return the plain value.
"""
try:
val = self.data[key]
except KeyError:
log.config.exception("No color defined for {}!".format(key))
return ''
if isinstance(val, QColor):
# This could happen when accidentally declaring 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!")
else:
return val

View File

@ -1,39 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2017 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/>.
"""Textwrapper used for config files."""
import textwrap
class TextWrapper(textwrap.TextWrapper):
"""Text wrapper customized to be used in configs."""
def __init__(self, **kwargs):
kw = {
'width': 72,
'replace_whitespace': False,
'break_long_words': False,
'break_on_hyphens': False,
'initial_indent': '# ',
'subsequent_indent': '# ',
}
kw.update(kwargs)
super().__init__(**kw)

View File

@ -1,101 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2017 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/>.
"""A single value (with multiple layers possibly) in the config."""
import collections
class SettingValue:
"""Base class for setting values.
Intended to be sub-classed by config value "types".
Attributes:
typ: A BaseType subclass instance.
value: (readonly property) The currently valid, most important value.
values: An OrderedDict with the values on different layers, with the
most significant layer first.
"""
def __init__(self, typ, default=None, *, backends=None):
"""Constructor.
Args:
typ: The BaseType to use.
default: Raw value to set.
backend: A list of usertypes.Backend enum members to mark this
setting as unsupported with other backends.
"""
self.typ = typ
self.values = collections.OrderedDict.fromkeys(
['temp', 'conf', 'default'])
self.values['default'] = default
self.backends = backends
def __str__(self):
"""Get raw string value."""
return self.value()
def default(self):
"""Get the default value."""
return self.values['default']
def getlayers(self, startlayer):
"""Get a dict of values starting with startlayer.
Args:
startlayer: The first layer to include.
"""
idx = list(self.values.keys()).index(startlayer)
d = collections.OrderedDict(list(self.values.items())[idx:])
return d
def value(self, startlayer=None):
"""Get the first valid value starting from startlayer.
Args:
startlayer: The first layer to include.
"""
if startlayer is None:
d = self.values
else:
d = self.getlayers(startlayer)
for val in d.values():
if val is not None:
return val
raise ValueError("No valid config value found!")
def transformed(self):
"""Get the transformed value."""
return self.typ.transform(self.value())
def setv(self, layer, value, interpolated):
"""Set the value on a layer.
Args:
layer: The layer to set the value on, an element name of the
ValueLayers dict.
value: The value to set.
interpolated: The interpolated value, for typechecking (or None).
"""
if interpolated is not None:
self.typ.validate(interpolated)
self.values[layer] = value

View File

@ -192,21 +192,19 @@ class FontFamilySetter(Setter):
def init_mappings(mappings): def init_mappings(mappings):
"""Initialize all settings based on a settings mapping.""" """Initialize all settings based on a settings mapping."""
for sectname, section in mappings.items(): for option, mapping in mappings.items():
for optname, mapping in section.items(): value = config.instance.get(option)
value = config.get(sectname, optname) log.config.vdebug("Setting {} to {!r}".format(option, value))
log.config.vdebug("Setting {} -> {} to {!r}".format( mapping.set(value)
sectname, optname, value))
mapping.set(value)
def update_mappings(mappings, section, option): def update_mappings(mappings, option):
"""Update global settings when QWeb(Engine)Settings changed.""" """Update global settings when QWeb(Engine)Settings changed."""
try: try:
mapping = mappings[section][option] mapping = mappings[option]
except KeyError: except KeyError:
return return
value = config.get(section, option) value = config.instance.get(option)
mapping.set(value) mapping.set(value)

View File

@ -7,9 +7,6 @@ vim: ft=html fileencoding=utf-8 sts=4 sw=4 et:
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>{{ title }}</title> <title>{{ title }}</title>
{% if icon %}
<link rel='icon' type='image/png' href="{{ icon }}">
{% endif %}
<style type="text/css"> <style type="text/css">
{% block style %} {% block style %}
body { body {

View File

@ -54,13 +54,13 @@ ul.files > li {
<ul class="folders"> <ul class="folders">
{% for item in directories %} {% for item in directories %}
<li><a href="{{ file_url(item.absname) }}">{{item.name}}</a></li> <li><a href="{{ file_url(item['absname']) }}">{{item['name']}}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
<ul class="files"> <ul class="files">
{% for item in files %} {% for item in files %}
<li><a href="{{ file_url(item.absname) }}">{{item.name}}</a></li> <li><a href="{{ file_url(item['absname']) }}">{{item['name']}}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>

View File

@ -54,7 +54,7 @@ table {
<a href="#" id="load" style="display: none">Show more</a> <a href="#" id="load" style="display: none">Show more</a>
<script type="text/javascript" src="qute://javascript/history.js"></script> <script type="text/javascript" src="qute://javascript/history.js"></script>
<script type="text/javascript"> <script type="text/javascript">
window.SESSION_INTERVAL = {{session_interval}} * 60 * 1000; window.GAP_INTERVAL = {{gap_interval}} * 60 * 1000;
window.onload = function() { window.onload = function() {
var loadLink = document.getElementById('load'); var loadLink = document.getElementById('load');

View File

@ -74,7 +74,7 @@ li {
<p>Error while opening {{ url }}<br> <p>Error while opening {{ url }}<br>
<p id="error-message-text" style="color: #a31a1a;">qutebrowser can't find a suitable pdf.js installation</p></p> <p id="error-message-text" style="color: #a31a1a;">qutebrowser can't find a suitable pdf.js installation</p></p>
<p>It looks like you set <code>content -> enable-pdfjs</code> <p>It looks like you set <code>content.pdfjs</code>
to <em>true</em> but qutebrowser can't find the required files.</p> to <em>true</em> but qutebrowser can't find the required files.</p>
<br> <br>
@ -82,7 +82,7 @@ li {
<h2>Possible fixes</h2> <h2>Possible fixes</h2>
<ul> <ul>
<li> <li>
Disable <code>content -> enable-pdfjs</code> and reload the page. Disable <code>content.pdfjs</code> and reload the page.
You will need to download the pdf-file and open it with an external You will need to download the pdf-file and open it with an external
tool instead. tool instead.
</li> </li>

View File

@ -1,9 +1,13 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block script %} {% block script %}
var cset = function(section, option, el) { var cset = function(option, value) {
value = el.value; // FIXME:conf we might want some error handling here?
window.qute.set(section, option, value); var url = "qute://settings/set?option=" + encodeURIComponent(option);
url += "&value=" + encodeURIComponent(value);
var xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.send();
} }
{% endblock %} {% endblock %}
@ -22,24 +26,23 @@ th pre { color: grey; text-align: left; }
<noscript><h1 class="noscript">View Only</h1><p class="noscript-text">Changing settings requires javascript to be enabled!</p></noscript> <noscript><h1 class="noscript">View Only</h1><p class="noscript-text">Changing settings requires javascript to be enabled!</p></noscript>
<header><h1>{{ title }}</h1></header> <header><h1>{{ title }}</h1></header>
<table> <table>
{% for section in config.DATA %} {% for option in configdata.DATA.values() %}
<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> <tr>
<td>{{ d }} (Current: {{ confget(section, d)|truncate(100) }}) <!-- FIXME: convert to string properly -->
{% if config.DATA.get(section).descriptions[d] %} <td>{{ option.name }} (Current: {{ confget(option.name) | string |truncate(100) }})
<p class="option_description">{{ config.DATA.get(section).descriptions[d]|e }}</p> {% if option.description %}
<p class="option_description">{{ option.description|e }}</p>
{% endif %} {% endif %}
</td> </td>
<td> <td>
<input type="text" <input type="text"
onblur="cset('{{ section }}', '{{ d }}', this)" id="input-{{ option.name }}"
value="{{ confget(section, d) }}"> onblur="cset('{{ option.name }}', this.value)"
value="{{ confget(option.name) }}">
</input> </input>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
{% endfor %}
</table> </table>
{% endblock %} {% endblock %}

View File

@ -79,9 +79,9 @@ window.loadHistory = (function() {
// Create session-separator and new tbody if necessary // Create session-separator and new tbody if necessary
if (tbody.lastChild !== null && lastItemDate !== null && if (tbody.lastChild !== null && lastItemDate !== null &&
window.SESSION_INTERVAL > 0) { window.GAP_INTERVAL > 0) {
var interval = lastItemDate.getTime() - date.getTime(); var interval = lastItemDate.getTime() - date.getTime();
if (interval > window.SESSION_INTERVAL) { if (interval > window.GAP_INTERVAL) {
// Add session-separator // Add session-separator
var sessionSeparator = document.createElement("td"); var sessionSeparator = document.createElement("td");
sessionSeparator.className = "session-separator"; sessionSeparator.className = "session-separator";

View File

@ -20,13 +20,12 @@
"""Base class for vim-like key sequence parser.""" """Base class for vim-like key sequence parser."""
import re import re
import functools
import unicodedata import unicodedata
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject from PyQt5.QtCore import pyqtSignal, QObject
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import usertypes, log, utils, objreg from qutebrowser.utils import usertypes, log, utils
class BaseKeyParser(QObject): class BaseKeyParser(QObject):
@ -41,7 +40,6 @@ class BaseKeyParser(QObject):
partial: No keychain matched yet, but it's still possible in the partial: No keychain matched yet, but it's still possible in the
future. future.
definitive: Keychain matches exactly. definitive: Keychain matches exactly.
ambiguous: There are both a partial and a definitive match.
none: No more matches possible. none: No more matches possible.
Types: type of a key binding. Types: type of a key binding.
@ -59,7 +57,6 @@ class BaseKeyParser(QObject):
_warn_on_keychains: Whether a warning should be logged when binding _warn_on_keychains: Whether a warning should be logged when binding
keychains in a section which does not support them. keychains in a section which does not support them.
_keystring: The currently entered key sequence _keystring: The currently entered key sequence
_ambiguous_timer: Timer for delayed execution with ambiguous bindings.
_modename: The name of the input mode associated with this keyparser. _modename: The name of the input mode associated with this keyparser.
_supports_count: Whether count is supported _supports_count: Whether count is supported
_supports_chains: Whether keychains are supported _supports_chains: Whether keychains are supported
@ -78,16 +75,13 @@ class BaseKeyParser(QObject):
do_log = True do_log = True
passthrough = False passthrough = False
Match = usertypes.enum('Match', ['partial', 'definitive', 'ambiguous', Match = usertypes.enum('Match', ['partial', 'definitive', 'other', 'none'])
'other', 'none'])
Type = usertypes.enum('Type', ['chain', 'special']) Type = usertypes.enum('Type', ['chain', 'special'])
def __init__(self, win_id, parent=None, supports_count=None, def __init__(self, win_id, parent=None, supports_count=None,
supports_chains=False): supports_chains=False):
super().__init__(parent) super().__init__(parent)
self._win_id = win_id self._win_id = win_id
self._ambiguous_timer = usertypes.Timer(self, 'ambiguous-match')
self._ambiguous_timer.setSingleShot(True)
self._modename = None self._modename = None
self._keystring = '' self._keystring = ''
if supports_count is None: if supports_count is None:
@ -97,6 +91,7 @@ class BaseKeyParser(QObject):
self._warn_on_keychains = True self._warn_on_keychains = True
self.bindings = {} self.bindings = {}
self.special_bindings = {} self.special_bindings = {}
config.instance.changed.connect(self._on_config_changed)
def __repr__(self): def __repr__(self):
return utils.get_repr(self, supports_count=self._supports_count, return utils.get_repr(self, supports_count=self._supports_count,
@ -126,7 +121,13 @@ class BaseKeyParser(QObject):
if binding is None: if binding is None:
self._debug_log("Ignoring only-modifier keyeevent.") self._debug_log("Ignoring only-modifier keyeevent.")
return False return False
binding = binding.lower()
key_mappings = config.val.bindings.key_mappings
try:
binding = key_mappings['<{}>'.format(binding)][1:-1]
except KeyError:
pass
try: try:
cmdstr = self.special_bindings[binding] cmdstr = self.special_bindings[binding]
except KeyError: except KeyError:
@ -182,7 +183,8 @@ class BaseKeyParser(QObject):
self._debug_log("Ignoring, no text char") self._debug_log("Ignoring, no text char")
return self.Match.none return self.Match.none
self._stop_timers() key_mappings = config.val.bindings.key_mappings
txt = key_mappings.get(txt, txt)
self._keystring += txt self._keystring += txt
count, cmd_input = self._split_count() count, cmd_input = self._split_count()
@ -198,10 +200,6 @@ class BaseKeyParser(QObject):
self._keystring)) self._keystring))
self.clear_keystring() self.clear_keystring()
self.execute(binding, self.Type.chain, count) self.execute(binding, self.Type.chain, count)
elif match == self.Match.ambiguous:
self._debug_log("Ambiguous match for '{}'.".format(
self._keystring))
self._handle_ambiguous_match(binding, count)
elif match == self.Match.partial: elif match == self.Match.partial:
self._debug_log("No match for '{}' (added {})".format( self._debug_log("No match for '{}' (added {})".format(
self._keystring, txt)) self._keystring, txt))
@ -221,11 +219,9 @@ class BaseKeyParser(QObject):
Return: Return:
A tuple (matchtype, binding). A tuple (matchtype, binding).
matchtype: Match.definitive, Match.ambiguous, Match.partial or matchtype: Match.definitive, Match.partial or Match.none.
Match.none binding: - None with Match.partial/Match.none.
binding: - None with Match.partial/Match.none - The found binding with Match.definitive.
- The found binding with Match.definitive/
Match.ambiguous
""" """
# A (cmd_input, binding) tuple (k, v of bindings) or None. # A (cmd_input, binding) tuple (k, v of bindings) or None.
definitive_match = None definitive_match = None
@ -243,58 +239,13 @@ class BaseKeyParser(QObject):
elif binding.startswith(cmd_input): elif binding.startswith(cmd_input):
partial_match = True partial_match = True
break break
if definitive_match is not None and partial_match: if definitive_match is not None:
return (self.Match.ambiguous, definitive_match[1])
elif definitive_match is not None:
return (self.Match.definitive, definitive_match[1]) return (self.Match.definitive, definitive_match[1])
elif partial_match: elif partial_match:
return (self.Match.partial, None) return (self.Match.partial, None)
else: else:
return (self.Match.none, None) return (self.Match.none, None)
def _stop_timers(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
def _handle_ambiguous_match(self, binding, count):
"""Handle an ambiguous match.
Args:
binding: The command-string to execute.
count: The count to pass.
"""
self._debug_log("Ambiguous match for '{}'".format(self._keystring))
time = config.get('input', 'timeout')
if time == 0:
# execute immediately
self.clear_keystring()
self.execute(binding, self.Type.chain, count)
else:
# 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(
functools.partial(self.delayed_exec, binding, count))
self._ambiguous_timer.start()
def delayed_exec(self, command, count):
"""Execute a delayed command.
Args:
command/count: As if passed to self.execute()
"""
self._debug_log("Executing delayed command now!")
self.clear_keystring()
self.execute(command, self.Type.chain, count)
def handle(self, e): def handle(self, e):
"""Handle a new keypress and call the respective handlers. """Handle a new keypress and call the respective handlers.
@ -314,7 +265,11 @@ class BaseKeyParser(QObject):
self.keystring_updated.emit(self._keystring) self.keystring_updated.emit(self._keystring)
return match != self.Match.none return match != self.Match.none
def read_config(self, modename=None): @config.change_filter('bindings')
def _on_config_changed(self):
self._read_config()
def _read_config(self, modename=None):
"""Read the configuration. """Read the configuration.
Config format: key = command, e.g.: Config format: key = command, e.g.:
@ -332,16 +287,15 @@ class BaseKeyParser(QObject):
self._modename = modename self._modename = modename
self.bindings = {} self.bindings = {}
self.special_bindings = {} self.special_bindings = {}
keyconfparser = objreg.get('key-config')
for (key, cmd) in keyconfparser.get_bindings_for(modename).items(): for key, cmd in config.key_instance.get_bindings_for(modename).items():
assert cmd assert cmd
self._parse_key_command(modename, key, cmd) self._parse_key_command(modename, key, cmd)
def _parse_key_command(self, modename, key, cmd): def _parse_key_command(self, modename, key, cmd):
"""Parse the keys and their command and store them in the object.""" """Parse the keys and their command and store them in the object."""
if utils.is_special_key(key): if utils.is_special_key(key):
keystr = utils.normalize_keystr(key[1:-1]) self.special_bindings[key[1:-1]] = cmd
self.special_bindings[keystr] = cmd
elif self._supports_chains: elif self._supports_chains:
self.bindings[key] = cmd self.bindings[key] = cmd
elif self._warn_on_keychains: elif self._warn_on_keychains:
@ -359,15 +313,6 @@ class BaseKeyParser(QObject):
""" """
raise NotImplementedError raise NotImplementedError
@pyqtSlot(str)
def on_keyconfig_changed(self, mode):
"""Re-read the config if a key binding was changed."""
if self._modename is None:
raise AssertionError("on_keyconfig_changed called but no section "
"defined!")
if mode == self._modename:
self.read_config()
def clear_keystring(self): def clear_keystring(self):
"""Clear the currently entered key sequence.""" """Clear the currently entered key sequence."""
if self._keystring: if self._keystring:

View File

@ -42,7 +42,7 @@ class CommandKeyParser(BaseKeyParser):
def execute(self, cmdstr, _keytype, count=None): def execute(self, cmdstr, _keytype, count=None):
try: try:
self._commandrunner.run(cmdstr, count) self._commandrunner.run(cmdstr, count)
except (cmdexc.CommandMetaError, cmdexc.CommandError) as e: except cmdexc.Error as e:
message.error(str(e), stack=traceback.format_exc()) message.error(str(e), stack=traceback.format_exc())
@ -69,7 +69,7 @@ class PassthroughKeyParser(CommandKeyParser):
""" """
super().__init__(win_id, parent, supports_chains=False) super().__init__(win_id, parent, supports_chains=False)
self._warn_on_keychains = warn self._warn_on_keychains = warn
self.read_config(mode) self._read_config(mode)
self._mode = mode self._mode = mode
def __repr__(self): def __repr__(self):

View File

@ -144,9 +144,6 @@ class ModeManager(QObject):
self._parsers = {} self._parsers = {}
self.mode = usertypes.KeyMode.normal self.mode = usertypes.KeyMode.normal
self._releaseevents_to_pass = set() self._releaseevents_to_pass = set()
self._forward_unbound_keys = config.get(
'input', 'forward-unbound-keys')
objreg.get('config').changed.connect(self.set_forward_unbound_keys)
def __repr__(self): def __repr__(self):
return utils.get_repr(self, mode=self.mode) return utils.get_repr(self, mode=self.mode)
@ -171,10 +168,12 @@ class ModeManager(QObject):
event.modifiers() not in [Qt.NoModifier, Qt.ShiftModifier] or event.modifiers() not in [Qt.NoModifier, Qt.ShiftModifier] or
not event.text().strip()) not event.text().strip())
forward_unbound_keys = config.val.input.forward_unbound_keys
if handled: if handled:
filter_this = True filter_this = True
elif (parser.passthrough or self._forward_unbound_keys == 'all' or elif (parser.passthrough or forward_unbound_keys == 'all' or
(self._forward_unbound_keys == 'auto' and is_non_alnum)): (forward_unbound_keys == 'auto' and is_non_alnum)):
filter_this = False filter_this = False
else: else:
filter_this = True filter_this = True
@ -184,10 +183,10 @@ class ModeManager(QObject):
if curmode != usertypes.KeyMode.insert: if curmode != usertypes.KeyMode.insert:
focus_widget = QApplication.instance().focusWidget() focus_widget = QApplication.instance().focusWidget()
log.modes.debug("handled: {}, forward-unbound-keys: {}, " log.modes.debug("handled: {}, forward_unbound_keys: {}, "
"passthrough: {}, is_non_alnum: {} --> " "passthrough: {}, is_non_alnum: {} --> "
"filter: {} (focused: {!r})".format( "filter: {} (focused: {!r})".format(
handled, self._forward_unbound_keys, handled, forward_unbound_keys,
parser.passthrough, is_non_alnum, filter_this, parser.passthrough, is_non_alnum, filter_this,
focus_widget)) focus_widget))
return filter_this return filter_this
@ -313,12 +312,6 @@ class ModeManager(QObject):
raise ValueError("Can't leave normal mode!") raise ValueError("Can't leave normal mode!")
self.leave(self.mode, 'leave current') self.leave(self.mode, 'leave current')
@config.change_filter('input', 'forward-unbound-keys')
def set_forward_unbound_keys(self):
"""Update local setting when config changed."""
self._forward_unbound_keys = config.get(
'input', 'forward-unbound-keys')
def eventFilter(self, event): def eventFilter(self, event):
"""Filter all events based on the currently set mode. """Filter all events based on the currently set mode.

View File

@ -48,7 +48,7 @@ class NormalKeyParser(keyparser.CommandKeyParser):
def __init__(self, win_id, parent=None): def __init__(self, win_id, parent=None):
super().__init__(win_id, parent, supports_count=True, super().__init__(win_id, parent, supports_count=True,
supports_chains=True) supports_chains=True)
self.read_config('normal') self._read_config('normal')
self._partial_timer = usertypes.Timer(self, 'partial-match') self._partial_timer = usertypes.Timer(self, 'partial-match')
self._partial_timer.setSingleShot(True) self._partial_timer.setSingleShot(True)
self._inhibited = False self._inhibited = False
@ -74,7 +74,7 @@ class NormalKeyParser(keyparser.CommandKeyParser):
return self.Match.none return self.Match.none
match = super()._handle_single_key(e) match = super()._handle_single_key(e)
if match == self.Match.partial: if match == self.Match.partial:
timeout = config.get('input', 'partial-timeout') timeout = config.val.input.partial_timeout
if timeout != 0: if timeout != 0:
self._partial_timer.setInterval(timeout) self._partial_timer.setInterval(timeout)
self._partial_timer.timeout.connect(self._clear_partial_match) self._partial_timer.timeout.connect(self._clear_partial_match)
@ -130,7 +130,7 @@ class PromptKeyParser(keyparser.CommandKeyParser):
supports_chains=True) supports_chains=True)
# We don't want an extra section for this in the config, so we just # We don't want an extra section for this in the config, so we just
# abuse the prompt section. # abuse the prompt section.
self.read_config('prompt') self._read_config('prompt')
def __repr__(self): def __repr__(self):
return utils.get_repr(self) return utils.get_repr(self)
@ -150,7 +150,7 @@ class HintKeyParser(keyparser.CommandKeyParser):
supports_chains=True) supports_chains=True)
self._filtertext = '' self._filtertext = ''
self._last_press = LastPress.none self._last_press = LastPress.none
self.read_config('hint') self._read_config('hint')
self.keystring_updated.connect(self.on_keystring_updated) self.keystring_updated.connect(self.on_keystring_updated)
def _handle_special_key(self, e): def _handle_special_key(self, e):
@ -264,7 +264,7 @@ class CaretKeyParser(keyparser.CommandKeyParser):
def __init__(self, win_id, parent=None): def __init__(self, win_id, parent=None):
super().__init__(win_id, parent, supports_count=True, super().__init__(win_id, parent, supports_count=True,
supports_chains=True) supports_chains=True)
self.read_config('caret') self._read_config('caret')
class RegisterKeyParser(keyparser.CommandKeyParser): class RegisterKeyParser(keyparser.CommandKeyParser):
@ -280,7 +280,7 @@ class RegisterKeyParser(keyparser.CommandKeyParser):
super().__init__(win_id, parent, supports_count=False, super().__init__(win_id, parent, supports_count=False,
supports_chains=False) supports_chains=False)
self._mode = mode self._mode = mode
self.read_config('register') self._read_config('register')
def handle(self, e): def handle(self, e):
"""Override handle to always match the next key and use the register. """Override handle to always match the next key and use the register.
@ -316,14 +316,9 @@ class RegisterKeyParser(keyparser.CommandKeyParser):
else: else:
raise ValueError( raise ValueError(
"{} is not a valid register mode".format(self._mode)) "{} is not a valid register mode".format(self._mode))
except (cmdexc.CommandMetaError, cmdexc.CommandError) as err: except cmdexc.Error as err:
message.error(str(err), stack=traceback.format_exc()) message.error(str(err), stack=traceback.format_exc())
self.request_leave.emit(self._mode, "valid register key", True) self.request_leave.emit(self._mode, "valid register key", True)
return True return True
@pyqtSlot(str)
def on_keyconfig_changed(self, mode):
"""RegisterKeyParser has no config section (no bindable keys)."""
pass

View File

@ -24,14 +24,13 @@ import base64
import itertools import itertools
import functools import functools
import jinja2
from PyQt5.QtCore import pyqtSlot, QRect, QPoint, QTimer, Qt from PyQt5.QtCore import pyqtSlot, QRect, QPoint, QTimer, Qt
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QSizePolicy from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QSizePolicy
from qutebrowser.commands import runners, cmdutils from qutebrowser.commands import runners, cmdutils
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import (message, log, usertypes, qtutils, objreg, utils, from qutebrowser.utils import (message, log, usertypes, qtutils, objreg, utils,
debug) jinja, debug)
from qutebrowser.mainwindow import tabbedbrowser, messageview, prompt from qutebrowser.mainwindow import tabbedbrowser, messageview, prompt
from qutebrowser.mainwindow.statusbar import bar from qutebrowser.mainwindow.statusbar import bar
from qutebrowser.completion import completionwidget, completer from qutebrowser.completion import completionwidget, completer
@ -52,7 +51,7 @@ def get_window(via_ipc, force_window=False, force_tab=False,
via_ipc: Whether the request was made via IPC. via_ipc: Whether the request was made via IPC.
force_window: Whether to force opening in a window. force_window: Whether to force opening in a window.
force_tab: Whether to force opening in a tab. force_tab: Whether to force opening in a tab.
force_target: Override the new-instance-open-target config force_target: Override the new_instance_open_target config
""" """
if force_window and force_tab: if force_window and force_tab:
raise ValueError("force_window and force_tab are mutually exclusive!") raise ValueError("force_window and force_tab are mutually exclusive!")
@ -61,7 +60,7 @@ def get_window(via_ipc, force_window=False, force_tab=False,
# Initial main window # Initial main window
return 0 return 0
open_target = config.get('general', 'new-instance-open-target') open_target = config.val.new_instance_open_target
# Apply any target overrides, ordered by precedence # Apply any target overrides, ordered by precedence
if force_target is not None: if force_target is not None:
@ -99,7 +98,7 @@ def get_window(via_ipc, force_window=False, force_tab=False,
def get_target_window(): def get_target_window():
"""Get the target window for new tabs, or None if none exist.""" """Get the target window for new tabs, or None if none exist."""
try: try:
win_mode = config.get('general', 'new-instance-open-target.window') win_mode = config.val.new_instance_open_target_window
if win_mode == 'last-focused': if win_mode == 'last-focused':
return objreg.last_focused_window() return objreg.last_focused_window()
elif win_mode == 'first-opened': elif win_mode == 'first-opened':
@ -165,7 +164,7 @@ class MainWindow(QWidget):
self._init_downloadmanager() self._init_downloadmanager()
self._downloadview = downloadview.DownloadView(self.win_id) self._downloadview = downloadview.DownloadView(self.win_id)
if config.get('general', 'private-browsing'): if config.val.content.private_browsing:
# This setting always trumps what's passed in. # This setting always trumps what's passed in.
private = True private = True
else: else:
@ -215,7 +214,7 @@ class MainWindow(QWidget):
# resizing will fail. Therefore, we use singleShot QTimers to make sure # resizing will fail. Therefore, we use singleShot QTimers to make sure
# we defer this until everything else is initialized. # we defer this until everything else is initialized.
QTimer.singleShot(0, self._connect_overlay_signals) QTimer.singleShot(0, self._connect_overlay_signals)
objreg.get('config').changed.connect(self.on_config_changed) config.instance.changed.connect(self._on_config_changed)
objreg.get("app").new_window.emit(self) objreg.get("app").new_window.emit(self)
@ -254,7 +253,7 @@ class MainWindow(QWidget):
left = (self.width() - width) / 2 if centered else 0 left = (self.width() - width) / 2 if centered else 0
height_padding = 20 height_padding = 20
status_position = config.get('ui', 'status-position') status_position = config.val.statusbar.position
if status_position == 'bottom': if status_position == 'bottom':
if self.status.isVisible(): if self.status.isVisible():
status_height = self.status.height() status_height = self.status.height()
@ -308,7 +307,7 @@ class MainWindow(QWidget):
def _init_completion(self): def _init_completion(self):
self._completion = completionwidget.CompletionView(self.win_id, self) self._completion = completionwidget.CompletionView(self.win_id, self)
cmd = objreg.get('status-command', scope='window', window=self.win_id) cmd = objreg.get('status-command', scope='window', window=self.win_id)
completer_obj = completer.Completer(cmd, self.win_id, self._completion) completer_obj = completer.Completer(cmd, self._completion)
self._completion.selection_changed.connect( self._completion.selection_changed.connect(
completer_obj.on_selection_changed) completer_obj.on_selection_changed)
objreg.register('completion', self._completion, scope='window', objreg.register('completion', self._completion, scope='window',
@ -327,16 +326,14 @@ class MainWindow(QWidget):
def __repr__(self): def __repr__(self):
return utils.get_repr(self) return utils.get_repr(self)
@pyqtSlot(str, str) @pyqtSlot(str)
def on_config_changed(self, section, option): def _on_config_changed(self, option):
"""Resize the completion if related config options changed.""" """Resize the completion if related config options changed."""
if section != 'ui': if option == 'statusbar.padding':
return
if option == 'statusbar-padding':
self._update_overlay_geometries() self._update_overlay_geometries()
elif option == 'downloads-position': elif option == 'downloads.position':
self._add_widgets() self._add_widgets()
elif option == 'status-position': elif option == 'statusbar.position':
self._add_widgets() self._add_widgets()
self._update_overlay_geometries() self._update_overlay_geometries()
@ -345,10 +342,9 @@ class MainWindow(QWidget):
self._vbox.removeWidget(self.tabbed_browser) self._vbox.removeWidget(self.tabbed_browser)
self._vbox.removeWidget(self._downloadview) self._vbox.removeWidget(self._downloadview)
self._vbox.removeWidget(self.status) self._vbox.removeWidget(self.status)
downloads_position = config.get('ui', 'downloads-position')
status_position = config.get('ui', 'status-position')
widgets = [self.tabbed_browser] widgets = [self.tabbed_browser]
downloads_position = config.val.downloads.position
if downloads_position == 'top': if downloads_position == 'top':
widgets.insert(0, self._downloadview) widgets.insert(0, self._downloadview)
elif downloads_position == 'bottom': elif downloads_position == 'bottom':
@ -356,6 +352,7 @@ class MainWindow(QWidget):
else: else:
raise ValueError("Invalid position {}!".format(downloads_position)) raise ValueError("Invalid position {}!".format(downloads_position))
status_position = config.val.statusbar.position
if status_position == 'top': if status_position == 'top':
widgets.insert(0, self.status) widgets.insert(0, self.status)
elif status_position == 'bottom': elif status_position == 'bottom':
@ -417,8 +414,6 @@ class MainWindow(QWidget):
def _connect_signals(self): def _connect_signals(self):
"""Connect all mainwindow signals.""" """Connect all mainwindow signals."""
key_config = objreg.get('key-config')
status = self._get_object('statusbar') status = self._get_object('statusbar')
keyparsers = self._get_object('keyparsers') keyparsers = self._get_object('keyparsers')
completion_obj = self._get_object('completion') completion_obj = self._get_object('completion')
@ -448,10 +443,6 @@ class MainWindow(QWidget):
parser.keystring_updated.connect(functools.partial( parser.keystring_updated.connect(functools.partial(
self._keyhint.update_keyhint, mode.name)) self._keyhint.update_keyhint, mode.name))
# config
for obj in keyparsers.values():
key_config.changed.connect(obj.on_keyconfig_changed)
# messages # messages
message.global_bridge.show_message.connect( message.global_bridge.show_message.connect(
self._messageview.show_message) self._messageview.show_message)
@ -545,24 +536,23 @@ class MainWindow(QWidget):
if crashsignal.is_crashing: if crashsignal.is_crashing:
e.accept() e.accept()
return return
confirm_quit = config.get('ui', 'confirm-quit')
tab_count = self.tabbed_browser.count() tab_count = self.tabbed_browser.count()
download_model = objreg.get('download-model', scope='window', download_model = objreg.get('download-model', scope='window',
window=self.win_id) window=self.win_id)
download_count = download_model.running_downloads() download_count = download_model.running_downloads()
quit_texts = [] quit_texts = []
# Ask if multiple-tabs are open # Ask if multiple-tabs are open
if 'multiple-tabs' in confirm_quit and tab_count > 1: if 'multiple-tabs' in config.val.confirm_quit and tab_count > 1:
quit_texts.append("{} {} open.".format( quit_texts.append("{} {} open.".format(
tab_count, "tab is" if tab_count == 1 else "tabs are")) tab_count, "tab is" if tab_count == 1 else "tabs are"))
# Ask if multiple downloads running # Ask if multiple downloads running
if 'downloads' in confirm_quit and download_count > 0: if 'downloads' in config.val.confirm_quit and download_count > 0:
quit_texts.append("{} {} running.".format( quit_texts.append("{} {} running.".format(
download_count, download_count,
"download is" if download_count == 1 else "downloads are")) "download is" if download_count == 1 else "downloads are"))
# Process all quit messages that user must confirm # Process all quit messages that user must confirm
if quit_texts or 'always' in confirm_quit: if quit_texts or 'always' in config.val.confirm_quit:
msg = jinja2.Template(""" msg = jinja.environment.from_string("""
<ul> <ul>
{% for text in quit_texts %} {% for text in quit_texts %}
<li>{{text}}</li> <li>{{text}}</li>

View File

@ -23,8 +23,8 @@
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer, Qt, QSize from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer, Qt, QSize
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QSizePolicy from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QSizePolicy
from qutebrowser.config import config, style from qutebrowser.config import config
from qutebrowser.utils import usertypes, objreg from qutebrowser.utils import usertypes
class Message(QLabel): class Message(QLabel):
@ -41,31 +41,32 @@ class Message(QLabel):
""" """
if level == usertypes.MessageLevel.error: if level == usertypes.MessageLevel.error:
stylesheet += """ stylesheet += """
background-color: {{ color['messages.bg.error'] }}; background-color: {{ conf.colors.messages.error.bg }};
color: {{ color['messages.fg.error'] }}; color: {{ conf.colors.messages.error.fg }};
font: {{ font['messages.error'] }}; font: {{ conf.fonts.messages.error }};
border-bottom: 1px solid {{ color['messages.border.error'] }}; border-bottom: 1px solid {{ conf.colors.messages.error.border }};
""" """
elif level == usertypes.MessageLevel.warning: elif level == usertypes.MessageLevel.warning:
stylesheet += """ stylesheet += """
background-color: {{ color['messages.bg.warning'] }}; background-color: {{ conf.colors.messages.warning.bg }};
color: {{ color['messages.fg.warning'] }}; color: {{ conf.colors.messages.warning.fg }};
font: {{ font['messages.warning'] }}; font: {{ conf.fonts.messages.warning }};
border-bottom: border-bottom:
1px solid {{ color['messages.border.warning'] }}; 1px solid {{ conf.colors.messages.warning.border }};
""" """
elif level == usertypes.MessageLevel.info: elif level == usertypes.MessageLevel.info:
stylesheet += """ stylesheet += """
background-color: {{ color['messages.bg.info'] }}; background-color: {{ conf.colors.messages.info.bg }};
color: {{ color['messages.fg.info'] }}; color: {{ conf.colors.messages.info.fg }};
font: {{ font['messages.info'] }}; font: {{ conf.fonts.messages.info }};
border-bottom: 1px solid {{ color['messages.border.info'] }} border-bottom: 1px solid {{ conf.colors.messages.info.border }}
""" """
else: # pragma: no cover else: # pragma: no cover
raise ValueError("Invalid level {!r}".format(level)) raise ValueError("Invalid level {!r}".format(level))
# We don't bother with set_register_stylesheet here as it's short-lived # We don't bother with set_register_stylesheet here as it's short-lived
# anyways. # anyways.
self.setStyleSheet(style.get_stylesheet(stylesheet)) config.set_register_stylesheet(self, stylesheet=stylesheet,
update=False)
class MessageView(QWidget): class MessageView(QWidget):
@ -76,6 +77,7 @@ class MessageView(QWidget):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self._messages = []
self._vbox = QVBoxLayout(self) self._vbox = QVBoxLayout(self)
self._vbox.setContentsMargins(0, 0, 0, 0) self._vbox.setContentsMargins(0, 0, 0, 0)
self._vbox.setSpacing(0) self._vbox.setSpacing(0)
@ -83,10 +85,9 @@ class MessageView(QWidget):
self._clear_timer = QTimer() self._clear_timer = QTimer()
self._clear_timer.timeout.connect(self.clear_messages) self._clear_timer.timeout.connect(self.clear_messages)
objreg.get('config').changed.connect(self._set_clear_timer_interval) config.instance.changed.connect(self._set_clear_timer_interval)
self._last_text = None self._last_text = None
self._messages = []
def sizeHint(self): def sizeHint(self):
"""Get the proposed height for the view.""" """Get the proposed height for the view."""
@ -94,10 +95,10 @@ class MessageView(QWidget):
# The width isn't really relevant as we're expanding anyways. # The width isn't really relevant as we're expanding anyways.
return QSize(-1, height) return QSize(-1, height)
@config.change_filter('ui', 'message-timeout') @config.change_filter('messages.timeout')
def _set_clear_timer_interval(self): def _set_clear_timer_interval(self):
"""Configure self._clear_timer according to the config.""" """Configure self._clear_timer according to the config."""
interval = config.get('ui', 'message-timeout') interval = config.val.messages.timeout
if interval > 0: if interval > 0:
interval *= min(5, len(self._messages)) interval *= min(5, len(self._messages))
self._clear_timer.setInterval(interval) self._clear_timer.setInterval(interval)
@ -131,7 +132,7 @@ class MessageView(QWidget):
self._last_text = text self._last_text = text
self.show() self.show()
self.update_geometry.emit() self.update_geometry.emit()
if config.get('ui', 'message-timeout') != 0: if config.val.messages.timeout != 0:
self._set_clear_timer_interval() self._set_clear_timer_interval()
self._clear_timer.start() self._clear_timer.start()

View File

@ -30,7 +30,7 @@ from PyQt5.QtWidgets import (QWidget, QGridLayout, QVBoxLayout, QLineEdit,
QLabel, QFileSystemModel, QTreeView, QSizePolicy) QLabel, QFileSystemModel, QTreeView, QSizePolicy)
from qutebrowser.browser import downloads from qutebrowser.browser import downloads
from qutebrowser.config import style, config from qutebrowser.config import config
from qutebrowser.utils import usertypes, log, utils, qtutils, objreg, message from qutebrowser.utils import usertypes, log, utils, qtutils, objreg, message
from qutebrowser.keyinput import modeman from qutebrowser.keyinput import modeman
from qutebrowser.commands import cmdutils, cmdexc from qutebrowser.commands import cmdutils, cmdexc
@ -233,29 +233,28 @@ class PromptContainer(QWidget):
""" """
STYLESHEET = """ STYLESHEET = """
{% set prompt_radius = config.get('ui', 'prompt-radius') %}
QWidget#PromptContainer { QWidget#PromptContainer {
{% if config.get('ui', 'status-position') == 'top' %} {% if conf.statusbar.position == 'top' %}
border-bottom-left-radius: {{ prompt_radius }}px; border-bottom-left-radius: {{ conf.prompt.radius }}px;
border-bottom-right-radius: {{ prompt_radius }}px; border-bottom-right-radius: {{ conf.prompt.radius }}px;
{% else %} {% else %}
border-top-left-radius: {{ prompt_radius }}px; border-top-left-radius: {{ conf.prompt.radius }}px;
border-top-right-radius: {{ prompt_radius }}px; border-top-right-radius: {{ conf.prompt.radius }}px;
{% endif %} {% endif %}
} }
QWidget { QWidget {
font: {{ font['prompts'] }}; font: {{ conf.fonts.prompts }};
color: {{ color['prompts.fg'] }}; color: {{ conf.colors.prompts.fg }};
background-color: {{ color['prompts.bg'] }}; background-color: {{ conf.colors.prompts.bg }};
} }
QTreeView { QTreeView {
selection-background-color: {{ color['prompts.selected.bg'] }}; selection-background-color: {{ conf.colors.prompts.selected.bg }};
} }
QTreeView::item:selected, QTreeView::item:selected:hover { QTreeView::item:selected, QTreeView::item:selected:hover {
background-color: {{ color['prompts.selected.bg'] }}; background-color: {{ conf.colors.prompts.selected.bg }};
} }
""" """
update_geometry = pyqtSignal() update_geometry = pyqtSignal()
@ -269,7 +268,7 @@ class PromptContainer(QWidget):
self.setObjectName('PromptContainer') self.setObjectName('PromptContainer')
self.setAttribute(Qt.WA_StyledBackground, True) self.setAttribute(Qt.WA_StyledBackground, True)
style.set_register_stylesheet(self) config.set_register_stylesheet(self)
message.global_bridge.prompt_done.connect(self._on_prompt_done) message.global_bridge.prompt_done.connect(self._on_prompt_done)
prompt_queue.show_prompts.connect(self._on_show_prompts) prompt_queue.show_prompts.connect(self._on_show_prompts)
@ -483,9 +482,8 @@ class _BasePrompt(QWidget):
self._key_grid = QGridLayout() self._key_grid = QGridLayout()
self._key_grid.setVerticalSpacing(0) self._key_grid.setVerticalSpacing(0)
key_config = objreg.get('key-config')
# The bindings are all in the 'prompt' mode, even for yesno prompts # The bindings are all in the 'prompt' mode, even for yesno prompts
all_bindings = key_config.get_reverse_bindings_for('prompt') all_bindings = config.key_instance.get_reverse_bindings_for('prompt')
labels = [] labels = []
for cmd, text in self._allowed_commands(): for cmd, text in self._allowed_commands():
@ -566,7 +564,7 @@ class FilenamePrompt(_BasePrompt):
self.setFocusProxy(self._lineedit) self.setFocusProxy(self._lineedit)
self._init_key_label() self._init_key_label()
if config.get('ui', 'prompt-filebrowser'): if config.val.prompt.filebrowser:
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
@pyqtSlot(str) @pyqtSlot(str)
@ -628,7 +626,7 @@ class FilenamePrompt(_BasePrompt):
self._file_view.setModel(self._file_model) self._file_view.setModel(self._file_model)
self._file_view.clicked.connect(self._insert_path) self._file_view.clicked.connect(self._insert_path)
if config.get('ui', 'prompt-filebrowser'): if config.val.prompt.filebrowser:
self._vbox.addWidget(self._file_view) self._vbox.addWidget(self._file_view)
else: else:
self._file_view.hide() self._file_view.hide()

View File

@ -23,7 +23,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, pyqtProperty, Qt, QSize, QTimer
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QStackedLayout, QSizePolicy from PyQt5.QtWidgets import QWidget, QHBoxLayout, QStackedLayout, QSizePolicy
from qutebrowser.browser import browsertab from qutebrowser.browser import browsertab
from qutebrowser.config import config, style from qutebrowser.config import config
from qutebrowser.utils import usertypes, log, objreg, utils from qutebrowser.utils import usertypes, log, objreg, utils
from qutebrowser.mainwindow.statusbar import (backforward, command, progress, from qutebrowser.mainwindow.statusbar import (backforward, command, progress,
keystring, percentage, url, keystring, percentage, url,
@ -82,21 +82,21 @@ class ColorFlags:
def _generate_stylesheet(): def _generate_stylesheet():
flags = [ flags = [
('private', 'statusbar.{}.private'), ('private', 'statusbar.private'),
('caret', 'statusbar.{}.caret'), ('caret', 'statusbar.caret'),
('caret-selection', 'statusbar.{}.caret-selection'), ('caret-selection', 'statusbar.caret.selection'),
('prompt', 'prompts.{}'), ('prompt', 'prompts'),
('insert', 'statusbar.{}.insert'), ('insert', 'statusbar.insert'),
('command', 'statusbar.{}.command'), ('command', 'statusbar.command'),
('private-command', 'statusbar.{}.command.private'), ('private-command', 'statusbar.command.private'),
] ]
stylesheet = """ stylesheet = """
QWidget#StatusBar, QWidget#StatusBar,
QWidget#StatusBar QLabel, QWidget#StatusBar QLabel,
QWidget#StatusBar QLineEdit { QWidget#StatusBar QLineEdit {
font: {{ font['statusbar'] }}; font: {{ conf.fonts.statusbar }};
background-color: {{ color['statusbar.bg'] }}; background-color: {{ conf.colors.statusbar.normal.bg }};
color: {{ color['statusbar.fg'] }}; color: {{ conf.colors.statusbar.normal.fg }};
} }
""" """
for flag, option in flags: for flag, option in flags:
@ -104,11 +104,11 @@ def _generate_stylesheet():
QWidget#StatusBar[color_flags~="%s"], QWidget#StatusBar[color_flags~="%s"],
QWidget#StatusBar[color_flags~="%s"] QLabel, QWidget#StatusBar[color_flags~="%s"] QLabel,
QWidget#StatusBar[color_flags~="%s"] QLineEdit { QWidget#StatusBar[color_flags~="%s"] QLineEdit {
color: {{ color['%s'] }}; color: {{ conf.colors.%s }};
background-color: {{ color['%s'] }}; background-color: {{ conf.colors.%s }};
} }
""" % (flag, flag, flag, # flake8: disable=S001 """ % (flag, flag, flag, # flake8: disable=S001
option.format('fg'), option.format('bg')) option + '.fg', option + '.bg')
return stylesheet return stylesheet
@ -148,7 +148,7 @@ class StatusBar(QWidget):
objreg.register('statusbar', self, scope='window', window=win_id) objreg.register('statusbar', self, scope='window', window=win_id)
self.setObjectName(self.__class__.__name__) self.setObjectName(self.__class__.__name__)
self.setAttribute(Qt.WA_StyledBackground) self.setAttribute(Qt.WA_StyledBackground)
style.set_register_stylesheet(self) config.set_register_stylesheet(self)
self.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed) self.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed)
@ -197,33 +197,31 @@ class StatusBar(QWidget):
self.prog = progress.Progress(self) self.prog = progress.Progress(self)
self._hbox.addWidget(self.prog) self._hbox.addWidget(self.prog)
objreg.get('config').changed.connect(self._on_config_changed) config.instance.changed.connect(self._on_config_changed)
QTimer.singleShot(0, self.maybe_hide) QTimer.singleShot(0, self.maybe_hide)
def __repr__(self): def __repr__(self):
return utils.get_repr(self) return utils.get_repr(self)
@pyqtSlot(str, str) @pyqtSlot(str)
def _on_config_changed(self, section, option): def _on_config_changed(self, option):
if section != 'ui': if option == 'statusbar.hide':
return
if option == 'hide-statusbar':
self.maybe_hide() self.maybe_hide()
elif option == 'statusbar-pdading': elif option == 'statusbar.padding':
self._set_hbox_padding() self._set_hbox_padding()
@pyqtSlot() @pyqtSlot()
def maybe_hide(self): def maybe_hide(self):
"""Hide the statusbar if it's configured to do so.""" """Hide the statusbar if it's configured to do so."""
hide = config.get('ui', 'hide-statusbar')
tab = self._current_tab() tab = self._current_tab()
hide = config.val.statusbar.hide
if hide or (tab is not None and tab.data.fullscreen): if hide or (tab is not None and tab.data.fullscreen):
self.hide() self.hide()
else: else:
self.show() self.show()
def _set_hbox_padding(self): def _set_hbox_padding(self):
padding = config.get('ui', 'statusbar-padding') padding = config.val.statusbar.padding
self._hbox.setContentsMargins(padding.left, 0, padding.right, 0) self._hbox.setContentsMargins(padding.left, 0, padding.right, 0)
@pyqtProperty('QStringList') @pyqtProperty('QStringList')
@ -265,11 +263,21 @@ class StatusBar(QWidget):
self._color_flags.caret = ColorFlags.CaretMode.on self._color_flags.caret = ColorFlags.CaretMode.on
else: else:
self._color_flags.caret = ColorFlags.CaretMode.off self._color_flags.caret = ColorFlags.CaretMode.off
self.setStyleSheet(style.get_stylesheet(self.STYLESHEET)) config.set_register_stylesheet(self, update=False)
def _set_mode_text(self, mode): def _set_mode_text(self, mode):
"""Set the mode text.""" """Set the mode text."""
text = "-- {} MODE --".format(mode.upper()) if mode == 'passthrough':
key_instance = config.key_instance
all_bindings = key_instance.get_reverse_bindings_for('passthrough')
bindings = all_bindings.get('leave-mode')
if bindings:
suffix = ' ({} to leave)'.format(bindings[0])
else:
suffix = ''
else:
suffix = ''
text = "-- {} MODE --{}".format(mode.upper(), suffix)
self.txt.set_text(self.txt.Text.normal, text) self.txt.set_text(self.txt.Text.normal, text)
def _show_cmd_widget(self): def _show_cmd_widget(self):
@ -349,7 +357,7 @@ class StatusBar(QWidget):
def minimumSizeHint(self): def minimumSizeHint(self):
"""Set the minimum height to the text height plus some padding.""" """Set the minimum height to the text height plus some padding."""
padding = config.get('ui', 'statusbar-padding') padding = config.val.statusbar.padding
width = super().minimumSizeHint().width() width = super().minimumSizeHint().width()
height = self.fontMetrics().height() + padding.top + padding.bottom height = self.fontMetrics().height() + padding.top + padding.bottom
return QSize(width, height) return QSize(width, height)

View File

@ -22,7 +22,7 @@
from PyQt5.QtCore import pyqtSlot, QSize from PyQt5.QtCore import pyqtSlot, QSize
from PyQt5.QtWidgets import QProgressBar, QSizePolicy from PyQt5.QtWidgets import QProgressBar, QSizePolicy
from qutebrowser.config import style from qutebrowser.config import config
from qutebrowser.utils import utils, usertypes from qutebrowser.utils import utils, usertypes
@ -35,17 +35,17 @@ class Progress(QProgressBar):
border-radius: 0px; border-radius: 0px;
border: 2px solid transparent; border: 2px solid transparent;
background-color: transparent; background-color: transparent;
font: {{ font['statusbar'] }}; font: {{ conf.fonts.statusbar }};
} }
QProgressBar::chunk { QProgressBar::chunk {
background-color: {{ color['statusbar.progress.bg'] }}; background-color: {{ conf.colors.statusbar.progress.bg }};
} }
""" """
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
style.set_register_stylesheet(self) config.set_register_stylesheet(self)
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
self.setTextVisible(False) self.setTextVisible(False)
self.hide() self.hide()

View File

@ -22,7 +22,7 @@
from PyQt5.QtCore import pyqtSlot, pyqtProperty, Qt, QUrl from PyQt5.QtCore import pyqtSlot, pyqtProperty, Qt, QUrl
from qutebrowser.mainwindow.statusbar import textbase from qutebrowser.mainwindow.statusbar import textbase
from qutebrowser.config import style from qutebrowser.config import config
from qutebrowser.utils import usertypes, urlutils from qutebrowser.utils import usertypes, urlutils
@ -53,27 +53,27 @@ class UrlText(textbase.TextBase):
STYLESHEET = """ STYLESHEET = """
QLabel#UrlText[urltype="normal"] { QLabel#UrlText[urltype="normal"] {
color: {{ color['statusbar.url.fg'] }}; color: {{ conf.colors.statusbar.url.fg }};
} }
QLabel#UrlText[urltype="success"] { QLabel#UrlText[urltype="success"] {
color: {{ color['statusbar.url.fg.success'] }}; color: {{ conf.colors.statusbar.url.success.http.fg }};
} }
QLabel#UrlText[urltype="success_https"] { QLabel#UrlText[urltype="success_https"] {
color: {{ color['statusbar.url.fg.success.https'] }}; color: {{ conf.colors.statusbar.url.success.https.fg }};
} }
QLabel#UrlText[urltype="error"] { QLabel#UrlText[urltype="error"] {
color: {{ color['statusbar.url.fg.error'] }}; color: {{ conf.colors.statusbar.url.error.fg }};
} }
QLabel#UrlText[urltype="warn"] { QLabel#UrlText[urltype="warn"] {
color: {{ color['statusbar.url.fg.warn'] }}; color: {{ conf.colors.statusbar.url.warn.fg }};
} }
QLabel#UrlText[urltype="hover"] { QLabel#UrlText[urltype="hover"] {
color: {{ color['statusbar.url.fg.hover'] }}; color: {{ conf.colors.statusbar.url.hover.fg }};
} }
""" """
@ -81,7 +81,7 @@ class UrlText(textbase.TextBase):
"""Override TextBase.__init__ to elide in the middle by default.""" """Override TextBase.__init__ to elide in the middle by default."""
super().__init__(parent, Qt.ElideMiddle) super().__init__(parent, Qt.ElideMiddle)
self.setObjectName(self.__class__.__name__) self.setObjectName(self.__class__.__name__)
style.set_register_stylesheet(self) config.set_register_stylesheet(self)
self._hover_url = None self._hover_url = None
self._normal_url = None self._normal_url = None
self._normal_url_type = UrlType.normal self._normal_url_type = UrlType.normal
@ -109,7 +109,7 @@ class UrlText(textbase.TextBase):
else: else:
self.setText('') self.setText('')
self._urltype = UrlType.normal self._urltype = UrlType.normal
self.setStyleSheet(style.get_stylesheet(self.STYLESHEET)) config.set_register_stylesheet(self, update=False)
@pyqtSlot(str) @pyqtSlot(str)
def on_load_status_changed(self, status_str): def on_load_status_changed(self, status_str):

View File

@ -62,7 +62,7 @@ class TabbedBrowser(tabwidget.TabWidget):
_filter: A SignalFilter instance. _filter: A SignalFilter instance.
_now_focused: The tab which is focused now. _now_focused: The tab which is focused now.
_tab_insert_idx_left: Where to insert a new tab with _tab_insert_idx_left: Where to insert a new tab with
tabbar -> new-tab-position set to 'prev'. tabs.new_tab_position set to 'prev'.
_tab_insert_idx_right: Same as above, for 'next'. _tab_insert_idx_right: Same as above, for 'next'.
_undo_stack: List of UndoEntry namedtuples of closed tabs. _undo_stack: List of UndoEntry namedtuples of closed tabs.
shutting_down: Whether we're currently shutting down. shutting_down: Whether we're currently shutting down.
@ -122,13 +122,20 @@ class TabbedBrowser(tabwidget.TabWidget):
self._global_marks = {} self._global_marks = {}
self.default_window_icon = self.window().windowIcon() self.default_window_icon = self.window().windowIcon()
self.private = private self.private = private
objreg.get('config').changed.connect(self.update_favicons) config.instance.changed.connect(self._on_config_changed)
objreg.get('config').changed.connect(self.update_window_title)
objreg.get('config').changed.connect(self.update_tab_titles)
def __repr__(self): def __repr__(self):
return utils.get_repr(self, count=self.count()) return utils.get_repr(self, count=self.count())
@pyqtSlot(str)
def _on_config_changed(self, option):
if option == 'tabs.favicons.show':
self._update_favicons()
elif option == 'window.title_format':
self._update_window_title()
elif option in ['tabs.title.format', 'tabs.title.format_pinned']:
self._update_tab_titles()
def _tab_index(self, tab): def _tab_index(self, tab):
"""Get the index of a given tab. """Get the index of a given tab.
@ -159,8 +166,7 @@ class TabbedBrowser(tabwidget.TabWidget):
widgets.append(widget) widgets.append(widget)
return widgets return widgets
@config.change_filter('ui', 'window-title-format') def _update_window_title(self):
def update_window_title(self):
"""Change the window title to match the current tab.""" """Change the window title to match the current tab."""
idx = self.currentIndex() idx = self.currentIndex()
if idx == -1: if idx == -1:
@ -170,8 +176,9 @@ class TabbedBrowser(tabwidget.TabWidget):
fields = self.get_tab_fields(idx) fields = self.get_tab_fields(idx)
fields['id'] = self._win_id fields['id'] = self._win_id
fmt = config.get('ui', 'window-title-format') title_format = config.val.window.title_format
self.window().setWindowTitle(fmt.format(**fields)) title = title_format.format(**fields)
self.window().setWindowTitle(title)
def _connect_tab_signals(self, tab): def _connect_tab_signals(self, tab):
"""Set up the needed signals for tab.""" """Set up the needed signals for tab."""
@ -252,7 +259,7 @@ class TabbedBrowser(tabwidget.TabWidget):
tab: The QWebView to be closed. tab: The QWebView to be closed.
add_undo: Whether the tab close can be undone. add_undo: Whether the tab close can be undone.
""" """
last_close = config.get('tabs', 'last-close') last_close = config.val.tabs.last_close
count = self.count() count = self.count()
if last_close == 'ignore' and count == 1: if last_close == 'ignore' and count == 1:
@ -270,11 +277,10 @@ class TabbedBrowser(tabwidget.TabWidget):
elif last_close == 'blank': elif last_close == 'blank':
self.openurl(QUrl('about:blank'), newtab=True) self.openurl(QUrl('about:blank'), newtab=True)
elif last_close == 'startpage': elif last_close == 'startpage':
url = QUrl(config.get('general', 'startpage')[0]) for url in config.val.url.start_pages:
self.openurl(url, newtab=True) self.openurl(url, newtab=True)
elif last_close == 'default-page': elif last_close == 'default-page':
url = config.get('general', 'default-page') self.openurl(config.val.url.default_page, newtab=True)
self.openurl(url, newtab=True)
def _remove_tab(self, tab, *, add_undo=True, crashed=False): def _remove_tab(self, tab, *, add_undo=True, crashed=False):
"""Remove a tab from the tab list and delete it properly. """Remove a tab from the tab list and delete it properly.
@ -329,15 +335,15 @@ class TabbedBrowser(tabwidget.TabWidget):
def undo(self): def undo(self):
"""Undo removing of a tab.""" """Undo removing of a tab."""
# Remove unused tab which may be created after the last tab is closed # Remove unused tab which may be created after the last tab is closed
last_close = config.get('tabs', 'last-close') last_close = config.val.tabs.last_close
use_current_tab = False use_current_tab = False
if last_close in ['blank', 'startpage', 'default-page']: if last_close in ['blank', 'startpage', 'default-page']:
only_one_tab_open = self.count() == 1 only_one_tab_open = self.count() == 1
no_history = len(self.widget(0).history) == 1 no_history = len(self.widget(0).history) == 1
urls = { urls = {
'blank': QUrl('about:blank'), 'blank': QUrl('about:blank'),
'startpage': QUrl(config.get('general', 'startpage')[0]), 'startpage': config.val.url.start_pages[0],
'default-page': config.get('general', 'default-page'), 'default-page': config.val.url.default_page,
} }
first_tab_url = self.widget(0).url() first_tab_url = self.widget(0).url()
last_close_urlstr = urls[last_close].toString().rstrip('/') last_close_urlstr = urls[last_close].toString().rstrip('/')
@ -393,7 +399,7 @@ class TabbedBrowser(tabwidget.TabWidget):
@pyqtSlot('QUrl') @pyqtSlot('QUrl')
@pyqtSlot('QUrl', bool) @pyqtSlot('QUrl', bool)
def tabopen(self, url=None, background=None, explicit=False, idx=None, *, def tabopen(self, url=None, background=None, related=True, idx=None, *,
ignore_tabs_are_windows=False): ignore_tabs_are_windows=False):
"""Open a new tab with a given URL. """Open a new tab with a given URL.
@ -403,16 +409,17 @@ class TabbedBrowser(tabwidget.TabWidget):
Args: Args:
url: The URL to open as QUrl or None for an empty tab. url: The URL to open as QUrl or None for an empty tab.
background: Whether to open the tab in the background. background: Whether to open the tab in the background.
if None, the background-tabs setting decides. if None, the `tabs.background_tabs`` setting decides.
explicit: Whether the tab was opened explicitly. related: Whether the tab was opened from another existing tab.
If this is set, the new position might be different. With If this is set, the new position might be different. With
the default settings we handle it like Chromium does: the default settings we handle it like Chromium does:
- Tabs from clicked links etc. are to the right of - Tabs from clicked links etc. are to the right of
the current. the current (related=True).
- Explicitly opened tabs are at the very right. - Explicitly opened tabs are at the very right
(related=False)
idx: The index where the new tab should be opened. idx: The index where the new tab should be opened.
ignore_tabs_are_windows: If given, never open a new window, even ignore_tabs_are_windows: If given, never open a new window, even
with tabs-are-windows set. with tabs.tabs_are_windows set.
Return: Return:
The opened WebView instance. The opened WebView instance.
@ -420,31 +427,32 @@ class TabbedBrowser(tabwidget.TabWidget):
if url is not None: if url is not None:
qtutils.ensure_valid(url) qtutils.ensure_valid(url)
log.webview.debug("Creating new tab with URL {}, background {}, " log.webview.debug("Creating new tab with URL {}, background {}, "
"explicit {}, idx {}".format( "related {}, idx {}".format(
url, background, explicit, idx)) url, background, related, idx))
if (config.get('tabs', 'tabs-are-windows') and self.count() > 0 and if (config.val.tabs.tabs_are_windows and self.count() > 0 and
not ignore_tabs_are_windows): not ignore_tabs_are_windows):
from qutebrowser.mainwindow import mainwindow from qutebrowser.mainwindow import mainwindow
window = mainwindow.MainWindow(private=self.private) window = mainwindow.MainWindow(private=self.private)
window.show() window.show()
tabbed_browser = objreg.get('tabbed-browser', scope='window', tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=window.win_id) window=window.win_id)
return tabbed_browser.tabopen(url, background, explicit) return tabbed_browser.tabopen(url=url, background=background,
related=related)
tab = browsertab.create(win_id=self._win_id, private=self.private, tab = browsertab.create(win_id=self._win_id, private=self.private,
parent=self) parent=self)
self._connect_tab_signals(tab) self._connect_tab_signals(tab)
if idx is None: if idx is None:
idx = self._get_new_tab_idx(explicit) idx = self._get_new_tab_idx(related)
self.insertTab(idx, tab, "") self.insertTab(idx, tab, "")
if url is not None: if url is not None:
tab.openurl(url) tab.openurl(url)
if background is None: if background is None:
background = config.get('tabs', 'background-tabs') background = config.val.tabs.background
if background: if background:
# Make sure the background tab has the correct initial size. # Make sure the background tab has the correct initial size.
# With a foreground tab, it's going to be resized correctly by the # With a foreground tab, it's going to be resized correctly by the
@ -458,19 +466,19 @@ class TabbedBrowser(tabwidget.TabWidget):
self.new_tab.emit(tab, idx) self.new_tab.emit(tab, idx)
return tab return tab
def _get_new_tab_idx(self, explicit): def _get_new_tab_idx(self, related):
"""Get the index of a tab to insert. """Get the index of a tab to insert.
Args: Args:
explicit: Whether the tab was opened explicitly. related: Whether the tab was opened from another tab (as a "child")
Return: Return:
The index of the new tab. The index of the new tab.
""" """
if explicit: if related:
pos = config.get('tabs', 'new-tab-position-explicit') pos = config.val.tabs.new_position.related
else: else:
pos = config.get('tabs', 'new-tab-position') pos = config.val.tabs.new_position.unrelated
if pos == 'prev': if pos == 'prev':
idx = self._tab_insert_idx_left idx = self._tab_insert_idx_left
# On first sight, we'd think we have to decrement # On first sight, we'd think we have to decrement
@ -486,26 +494,23 @@ class TabbedBrowser(tabwidget.TabWidget):
elif pos == 'last': elif pos == 'last':
idx = -1 idx = -1
else: else:
raise ValueError("Invalid new-tab-position '{}'.".format(pos)) raise ValueError("Invalid tabs.new_position '{}'.".format(pos))
log.webview.debug("new-tab-position {} -> opening new tab at {}, " log.webview.debug("tabs.new_position {} -> opening new tab at {}, "
"next left: {} / right: {}".format( "next left: {} / right: {}".format(
pos, idx, self._tab_insert_idx_left, pos, idx, self._tab_insert_idx_left,
self._tab_insert_idx_right)) self._tab_insert_idx_right))
return idx return idx
@config.change_filter('tabs', 'show-favicons') def _update_favicons(self):
def update_favicons(self):
"""Update favicons when config was changed.""" """Update favicons when config was changed."""
show = config.get('tabs', 'show-favicons')
tabs_are_wins = config.get('tabs', 'tabs-are-windows')
for i, tab in enumerate(self.widgets()): for i, tab in enumerate(self.widgets()):
if show: if config.val.tabs.favicons.show:
self.setTabIcon(i, tab.icon()) self.setTabIcon(i, tab.icon())
if tabs_are_wins: if config.val.tabs.tabs_are_windows:
self.window().setWindowIcon(tab.icon()) self.window().setWindowIcon(tab.icon())
else: else:
self.setTabIcon(i, QIcon()) self.setTabIcon(i, QIcon())
if tabs_are_wins: if config.val.tabs.tabs_are_windows:
self.window().setWindowIcon(self.default_window_icon) self.window().setWindowIcon(self.default_window_icon)
@pyqtSlot() @pyqtSlot()
@ -520,16 +525,16 @@ class TabbedBrowser(tabwidget.TabWidget):
except TabDeletedError: except TabDeletedError:
# We can get signals for tabs we already deleted... # We can get signals for tabs we already deleted...
return return
self.update_tab_title(idx) self._update_tab_title(idx)
if tab.data.keep_icon: if tab.data.keep_icon:
tab.data.keep_icon = False tab.data.keep_icon = False
else: else:
self.setTabIcon(idx, QIcon()) self.setTabIcon(idx, QIcon())
if (config.get('tabs', 'tabs-are-windows') and if (config.val.tabs.tabs_are_windows and
config.get('tabs', 'show-favicons')): config.val.tabs.favicons.show):
self.window().setWindowIcon(self.default_window_icon) self.window().setWindowIcon(self.default_window_icon)
if idx == self.currentIndex(): if idx == self.currentIndex():
self.update_window_title() self._update_window_title()
@pyqtSlot() @pyqtSlot()
def on_cur_load_started(self): def on_cur_load_started(self):
@ -561,7 +566,7 @@ class TabbedBrowser(tabwidget.TabWidget):
idx, text)) idx, text))
self.set_page_title(idx, text) self.set_page_title(idx, text)
if idx == self.currentIndex(): if idx == self.currentIndex():
self.update_window_title() self._update_window_title()
@pyqtSlot(browsertab.AbstractTab, QUrl) @pyqtSlot(browsertab.AbstractTab, QUrl)
def on_url_changed(self, tab, url): def on_url_changed(self, tab, url):
@ -590,7 +595,7 @@ class TabbedBrowser(tabwidget.TabWidget):
tab: The WebView where the title was changed. tab: The WebView where the title was changed.
icon: The new icon icon: The new icon
""" """
if not config.get('tabs', 'show-favicons'): if not config.val.tabs.favicons.show:
return return
try: try:
idx = self._tab_index(tab) idx = self._tab_index(tab)
@ -598,7 +603,7 @@ class TabbedBrowser(tabwidget.TabWidget):
# We can get signals for tabs we already deleted... # We can get signals for tabs we already deleted...
return return
self.setTabIcon(idx, icon) self.setTabIcon(idx, icon)
if config.get('tabs', 'tabs-are-windows'): if config.val.tabs.tabs_are_windows:
self.window().setWindowIcon(icon) self.window().setWindowIcon(icon)
@pyqtSlot(usertypes.KeyMode) @pyqtSlot(usertypes.KeyMode)
@ -635,7 +640,7 @@ class TabbedBrowser(tabwidget.TabWidget):
scope='window', window=self._win_id) scope='window', window=self._win_id)
self._now_focused = tab self._now_focused = tab
self.current_tab_changed.emit(tab) self.current_tab_changed.emit(tab)
QTimer.singleShot(0, self.update_window_title) QTimer.singleShot(0, self._update_window_title)
self._tab_insert_idx_left = self.currentIndex() self._tab_insert_idx_left = self.currentIndex()
self._tab_insert_idx_right = self.currentIndex() + 1 self._tab_insert_idx_right = self.currentIndex() + 1
@ -651,14 +656,14 @@ class TabbedBrowser(tabwidget.TabWidget):
except TabDeletedError: except TabDeletedError:
# We can get signals for tabs we already deleted... # We can get signals for tabs we already deleted...
return return
start = config.get('colors', 'tabs.indicator.start') start = config.val.colors.tabs.indicator.start
stop = config.get('colors', 'tabs.indicator.stop') stop = config.val.colors.tabs.indicator.stop
system = config.get('colors', 'tabs.indicator.system') system = config.val.colors.tabs.indicator.system
color = utils.interpolate_color(start, stop, perc, system) color = utils.interpolate_color(start, stop, perc, system)
self.set_tab_indicator_color(idx, color) self.set_tab_indicator_color(idx, color)
self.update_tab_title(idx) self._update_tab_title(idx)
if idx == self.currentIndex(): if idx == self.currentIndex():
self.update_window_title() self._update_window_title()
def on_load_finished(self, tab, ok): def on_load_finished(self, tab, ok):
"""Adjust tab indicator when loading finished.""" """Adjust tab indicator when loading finished."""
@ -668,16 +673,16 @@ class TabbedBrowser(tabwidget.TabWidget):
# We can get signals for tabs we already deleted... # We can get signals for tabs we already deleted...
return return
if ok: if ok:
start = config.get('colors', 'tabs.indicator.start') start = config.val.colors.tabs.indicator.start
stop = config.get('colors', 'tabs.indicator.stop') stop = config.val.colors.tabs.indicator.stop
system = config.get('colors', 'tabs.indicator.system') system = config.val.colors.tabs.indicator.system
color = utils.interpolate_color(start, stop, 100, system) color = utils.interpolate_color(start, stop, 100, system)
else: else:
color = config.get('colors', 'tabs.indicator.error') color = config.val.colors.tabs.indicator.error
self.set_tab_indicator_color(idx, color) self.set_tab_indicator_color(idx, color)
self.update_tab_title(idx) self._update_tab_title(idx)
if idx == self.currentIndex(): if idx == self.currentIndex():
self.update_window_title() self._update_window_title()
@pyqtSlot() @pyqtSlot()
def on_scroll_pos_changed(self): def on_scroll_pos_changed(self):
@ -688,8 +693,8 @@ class TabbedBrowser(tabwidget.TabWidget):
log.webview.debug("Not updating scroll position because index is " log.webview.debug("Not updating scroll position because index is "
"-1") "-1")
return return
self.update_window_title() self._update_window_title()
self.update_tab_title(idx) self._update_tab_title(idx)
def _on_renderer_process_terminated(self, tab, status, code): def _on_renderer_process_terminated(self, tab, status, code):
"""Show an error when a renderer process terminated.""" """Show an error when a renderer process terminated."""
@ -716,7 +721,7 @@ class TabbedBrowser(tabwidget.TabWidget):
url_string = tab.url(requested=True).toDisplayString() url_string = tab.url(requested=True).toDisplayString()
error_page = jinja.render( error_page = jinja.render(
'error.html', title="Error loading {}".format(url_string), 'error.html', title="Error loading {}".format(url_string),
url=url_string, error=msg, icon='') url=url_string, error=msg)
QTimer.singleShot(100, lambda: show_error_page(error_page)) QTimer.singleShot(100, lambda: show_error_page(error_page))
else: else:
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58698 # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58698

View File

@ -57,27 +57,27 @@ class TabWidget(QTabWidget):
self.setTabBar(bar) self.setTabBar(bar)
bar.tabCloseRequested.connect(self.tabCloseRequested) bar.tabCloseRequested.connect(self.tabCloseRequested)
bar.tabMoved.connect(functools.partial( bar.tabMoved.connect(functools.partial(
QTimer.singleShot, 0, self.update_tab_titles)) QTimer.singleShot, 0, self._update_tab_titles))
bar.currentChanged.connect(self._on_current_changed) bar.currentChanged.connect(self._on_current_changed)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.setDocumentMode(True) self.setDocumentMode(True)
self.setElideMode(Qt.ElideRight) self.setElideMode(Qt.ElideRight)
self.setUsesScrollButtons(True) self.setUsesScrollButtons(True)
bar.setDrawBase(False) bar.setDrawBase(False)
self.init_config() self._init_config()
objreg.get('config').changed.connect(self.init_config) config.instance.changed.connect(self._init_config)
@config.change_filter('tabs') @config.change_filter('tabs')
def init_config(self): def _init_config(self):
"""Initialize attributes based on the config.""" """Initialize attributes based on the config."""
if self is None: # pragma: no cover if self is None: # pragma: no cover
# WORKAROUND for PyQt 5.2 # WORKAROUND for PyQt 5.2
return return
tabbar = self.tabBar() tabbar = self.tabBar()
self.setMovable(config.get('tabs', 'movable')) self.setMovable(True)
self.setTabsClosable(False) self.setTabsClosable(False)
position = config.get('tabs', 'position') position = config.val.tabs.position
selection_behavior = config.get('tabs', 'select-on-remove') selection_behavior = config.val.tabs.select_on_remove
self.setTabPosition(position) self.setTabPosition(position)
tabbar.vertical = position in [QTabWidget.West, QTabWidget.East] tabbar.vertical = position in [QTabWidget.West, QTabWidget.East]
tabbar.setSelectionBehaviorOnRemove(selection_behavior) tabbar.setSelectionBehaviorOnRemove(selection_behavior)
@ -117,7 +117,7 @@ class TabWidget(QTabWidget):
bar.set_tab_data(idx, 'pinned', pinned) bar.set_tab_data(idx, 'pinned', pinned)
tab.data.pinned = pinned tab.data.pinned = pinned
self.update_tab_title(idx) self._update_tab_title(idx)
bar.refresh() bar.refresh()
@ -128,21 +128,21 @@ class TabWidget(QTabWidget):
def set_page_title(self, idx, title): def set_page_title(self, idx, title):
"""Set the tab title user data.""" """Set the tab title user data."""
self.tabBar().set_tab_data(idx, 'page-title', title) self.tabBar().set_tab_data(idx, 'page-title', title)
self.update_tab_title(idx) self._update_tab_title(idx)
def page_title(self, idx): def page_title(self, idx):
"""Get the tab title user data.""" """Get the tab title user data."""
return self.tabBar().page_title(idx) return self.tabBar().page_title(idx)
def update_tab_title(self, idx): def _update_tab_title(self, idx):
"""Update the tab text for the given tab.""" """Update the tab text for the given tab."""
tab = self.widget(idx) tab = self.widget(idx)
fields = self.get_tab_fields(idx) fields = self.get_tab_fields(idx)
fields['title'] = fields['title'].replace('&', '&&') fields['title'] = fields['title'].replace('&', '&&')
fields['index'] = idx + 1 fields['index'] = idx + 1
fmt = config.get('tabs', 'title-format') fmt = config.val.tabs.title.format
fmt_pinned = config.get('tabs', 'title-format-pinned') fmt_pinned = config.val.tabs.title.format_pinned
if tab.data.pinned: if tab.data.pinned:
title = '' if fmt_pinned is None else fmt_pinned.format(**fields) title = '' if fmt_pinned is None else fmt_pinned.format(**fields)
@ -190,22 +190,20 @@ class TabWidget(QTabWidget):
fields['scroll_pos'] = scroll_pos fields['scroll_pos'] = scroll_pos
return fields return fields
def update_tab_titles(self, section='tabs', option='title-format'): def _update_tab_titles(self):
"""Update all texts.""" """Update all texts."""
if section == 'tabs' and option in ['title-format', for idx in range(self.count()):
'title-format-pinned']: self._update_tab_title(idx)
for idx in range(self.count()):
self.update_tab_title(idx)
def tabInserted(self, idx): def tabInserted(self, idx):
"""Update titles when a tab was inserted.""" """Update titles when a tab was inserted."""
super().tabInserted(idx) super().tabInserted(idx)
self.update_tab_titles() self._update_tab_titles()
def tabRemoved(self, idx): def tabRemoved(self, idx):
"""Update titles when a tab was removed.""" """Update titles when a tab was removed."""
super().tabRemoved(idx) super().tabRemoved(idx)
self.update_tab_titles() self._update_tab_titles()
def addTab(self, page, icon_or_text, text_or_empty=None): def addTab(self, page, icon_or_text, text_or_empty=None):
"""Override addTab to use our own text setting logic. """Override addTab to use our own text setting logic.
@ -306,24 +304,17 @@ class TabBar(QTabBar):
super().__init__(parent) super().__init__(parent)
self._win_id = win_id self._win_id = win_id
self.setStyle(TabBarStyle()) self.setStyle(TabBarStyle())
self.set_font() self._set_font()
config_obj = objreg.get('config') config.instance.changed.connect(self._on_config_changed)
config_obj.changed.connect(self.set_font)
config_obj.changed.connect(self.set_icon_size)
self.vertical = False self.vertical = False
self._auto_hide_timer = QTimer() self._auto_hide_timer = QTimer()
self._auto_hide_timer.setSingleShot(True) self._auto_hide_timer.setSingleShot(True)
self._auto_hide_timer.setInterval(
config.get('tabs', 'show-switching-delay'))
self._auto_hide_timer.timeout.connect(self.maybe_hide) self._auto_hide_timer.timeout.connect(self.maybe_hide)
self._on_show_switching_delay_changed()
self.setAutoFillBackground(True) self.setAutoFillBackground(True)
self.set_colors() self._set_colors()
self.pinned_count = 0 self.pinned_count = 0
config_obj.changed.connect(self.set_colors)
QTimer.singleShot(0, self.maybe_hide) QTimer.singleShot(0, self.maybe_hide)
config_obj.changed.connect(self.on_tab_colors_changed)
config_obj.changed.connect(self.on_show_switching_delay_changed)
config_obj.changed.connect(self.tabs_show)
def __repr__(self): def __repr__(self):
return utils.get_repr(self, count=self.count()) return utils.get_repr(self, count=self.count())
@ -332,29 +323,37 @@ class TabBar(QTabBar):
"""Get the current tab object.""" """Get the current tab object."""
return self.parent().currentWidget() return self.parent().currentWidget()
@config.change_filter('tabs', 'show') @pyqtSlot(str)
def tabs_show(self): def _on_config_changed(self, option):
"""Hide or show tab bar if needed when tabs->show got changed.""" if option == 'fonts.tabs':
self.maybe_hide() self._set_font()
elif option == 'tabs.favicons.scale':
self._set_icon_size()
elif option == 'colors.tabs.bar.bg':
self._set_colors()
elif option == 'tabs.show_switching_delay':
self._on_show_switching_delay_changed()
elif option == 'tabs.show':
self.maybe_hide()
@config.change_filter('tabs', 'show-switching-delay') if option.startswith('colors.tabs.'):
def on_show_switching_delay_changed(self): self.update()
"""Set timer interval when tabs->show-switching-delay got changed."""
self._auto_hide_timer.setInterval( def _on_show_switching_delay_changed(self):
config.get('tabs', 'show-switching-delay')) """Set timer interval when tabs.show_switching_delay got changed."""
self._auto_hide_timer.setInterval(config.val.tabs.show_switching_delay)
def on_current_changed(self): def on_current_changed(self):
"""Show tab bar when current tab got changed.""" """Show tab bar when current tab got changed."""
self.maybe_hide() # for fullscreen tabs self.maybe_hide() # for fullscreen tabs
show = config.get('tabs', 'show') if config.val.tabs.show == 'switching':
if show == 'switching':
self.show() self.show()
self._auto_hide_timer.start() self._auto_hide_timer.start()
@pyqtSlot() @pyqtSlot()
def maybe_hide(self): def maybe_hide(self):
"""Hide the tab bar if needed.""" """Hide the tab bar if needed."""
show = config.get('tabs', 'show') show = config.val.tabs.show
tab = self._current_tab() tab = self._current_tab()
if (show in ['never', 'switching'] or if (show in ['never', 'switching'] or
(show == 'multiple' and self.count() == 1) or (show == 'multiple' and self.count() == 1) or
@ -409,35 +408,26 @@ class TabBar(QTabBar):
# code sets layoutDirty so it actually relayouts the tabs. # code sets layoutDirty so it actually relayouts the tabs.
self.setIconSize(self.iconSize()) self.setIconSize(self.iconSize())
@config.change_filter('fonts', 'tabbar') def _set_font(self):
def set_font(self):
"""Set the tab bar font.""" """Set the tab bar font."""
self.setFont(config.get('fonts', 'tabbar')) self.setFont(config.val.fonts.tabs)
self.set_icon_size() self._set_icon_size()
@config.change_filter('tabs', 'favicon-scale') def _set_icon_size(self):
def set_icon_size(self):
"""Set the tab bar favicon size.""" """Set the tab bar favicon size."""
size = self.fontMetrics().height() - 2 size = self.fontMetrics().height() - 2
size *= config.get('tabs', 'favicon-scale') size *= config.val.tabs.favicons.scale
self.setIconSize(QSize(size, size)) self.setIconSize(QSize(size, size))
@config.change_filter('colors', 'tabs.bg.bar') def _set_colors(self):
def set_colors(self):
"""Set the tab bar colors.""" """Set the tab bar colors."""
p = self.palette() p = self.palette()
p.setColor(QPalette.Window, config.get('colors', 'tabs.bg.bar')) p.setColor(QPalette.Window, config.val.colors.tabs.bar.bg)
self.setPalette(p) self.setPalette(p)
@pyqtSlot(str, str)
def on_tab_colors_changed(self, section, option):
"""Set the tab colors."""
if section == 'colors' and option.startswith('tabs.'):
self.update()
def mousePressEvent(self, e): def mousePressEvent(self, e):
"""Override mousePressEvent to close tabs if configured.""" """Override mousePressEvent to close tabs if configured."""
button = config.get('tabs', 'close-mouse-button') button = config.val.tabs.close_mouse_button
if (e.button() == Qt.RightButton and button == 'right' or if (e.button() == Qt.RightButton and button == 'right' or
e.button() == Qt.MiddleButton and button == 'middle'): e.button() == Qt.MiddleButton and button == 'middle'):
e.accept() e.accept()
@ -458,7 +448,7 @@ class TabBar(QTabBar):
A QSize. A QSize.
""" """
icon = self.tabIcon(index) icon = self.tabIcon(index)
padding = config.get('tabs', 'padding') padding = config.val.tabs.padding
padding_h = padding.left + padding.right padding_h = padding.left + padding.right
padding_v = padding.top + padding.bottom padding_v = padding.top + padding.bottom
if icon.isNull(): if icon.isNull():
@ -469,10 +459,9 @@ class TabBar(QTabBar):
icon_size = icon.actualSize(QSize(extent, extent)) icon_size = icon.actualSize(QSize(extent, extent))
padding_h += self.style().pixelMetric( padding_h += self.style().pixelMetric(
PixelMetrics.icon_padding, None, self) PixelMetrics.icon_padding, None, self)
indicator_width = config.get('tabs', 'indicator-width')
height = self.fontMetrics().height() + padding_v height = self.fontMetrics().height() + padding_v
width = (self.fontMetrics().width('\u2026') + icon_size.width() + width = (self.fontMetrics().width('\u2026') + icon_size.width() +
padding_h + indicator_width) padding_h + config.val.tabs.width.indicator)
return QSize(width, height) return QSize(width, height)
def tabSizeHint(self, index): def tabSizeHint(self, index):
@ -489,7 +478,7 @@ class TabBar(QTabBar):
minimum_size = self.minimumTabSizeHint(index) minimum_size = self.minimumTabSizeHint(index)
height = minimum_size.height() height = minimum_size.height()
if self.vertical: if self.vertical:
confwidth = str(config.get('tabs', 'width')) confwidth = str(config.val.tabs.width.bar)
if confwidth.endswith('%'): if confwidth.endswith('%'):
main_window = objreg.get('main-window', scope='window', main_window = objreg.get('main-window', scope='window',
window=self._win_id) window=self._win_id)
@ -504,19 +493,30 @@ class TabBar(QTabBar):
# want to ensure it's valid in this special case. # want to ensure it's valid in this special case.
return QSize() return QSize()
else: else:
tab_width_pinned_conf = config.get('tabs', 'pinned-width')
try: try:
pinned = self.tab_data(index, 'pinned') pinned = self.tab_data(index, 'pinned')
except KeyError: except KeyError:
pinned = False pinned = False
no_pinned_count = self.count() - self.pinned_count no_pinned_count = self.count() - self.pinned_count
pinned_width = tab_width_pinned_conf * self.pinned_count pinned_width = config.val.tabs.width.pinned * self.pinned_count
no_pinned_width = self.width() - pinned_width no_pinned_width = self.width() - pinned_width
if pinned: if pinned:
width = tab_width_pinned_conf size = QSize(config.val.tabs.width.pinned, height)
qtutils.ensure_valid(size)
return size
# If we *do* have enough space, tabs should occupy the whole window
# width. If there are pinned tabs their size will be subtracted
# from the total window width.
# During shutdown the self.count goes down,
# but the self.pinned_count not - this generates some odd behavior.
# To avoid this we compare self.count against self.pinned_count.
if self.pinned_count > 0 and self.count() > self.pinned_count:
pinned_width = config.val.tabs.width.pinned * self.pinned_count
no_pinned_width = self.width() - pinned_width
width = no_pinned_width / (self.count() - self.pinned_count)
else: else:
# Tabs should attempt to occupy the whole window width. If # Tabs should attempt to occupy the whole window width. If
@ -547,31 +547,21 @@ class TabBar(QTabBar):
def paintEvent(self, _e): def paintEvent(self, _e):
"""Override paintEvent to draw the tabs like we want to.""" """Override paintEvent to draw the tabs like we want to."""
# pylint: disable=bad-config-call
# WORKAROUND for https://bitbucket.org/logilab/astroid/issue/104
p = QStylePainter(self) p = QStylePainter(self)
selected = self.currentIndex() selected = self.currentIndex()
for idx in range(self.count()): for idx in range(self.count()):
tab = QStyleOptionTab() tab = QStyleOptionTab()
self.initStyleOption(tab, idx) self.initStyleOption(tab, idx)
bg_parts = ['tabs', 'bg'] # pylint: disable=bad-config-option
fg_parts = ['tabs', 'fg'] setting = config.val.colors.tabs
# pylint: enable=bad-config-option
if idx == selected: if idx == selected:
bg_parts.append('selected') setting = setting.selected
fg_parts.append('selected') setting = setting.odd if idx % 2 else setting.even
if idx % 2: tab.palette.setColor(QPalette.Window, setting.bg)
bg_parts.append('odd') tab.palette.setColor(QPalette.WindowText, setting.fg)
fg_parts.append('odd')
else:
bg_parts.append('even')
fg_parts.append('even')
bg_color = config.get('colors', '.'.join(bg_parts))
fg_color = config.get('colors', '.'.join(fg_parts))
tab.palette.setColor(QPalette.Window, bg_color)
tab.palette.setColor(QPalette.WindowText, fg_color)
indicator_color = self.tab_indicator_color(idx) indicator_color = self.tab_indicator_color(idx)
tab.palette.setColor(QPalette.Base, indicator_color) tab.palette.setColor(QPalette.Base, indicator_color)
@ -597,7 +587,7 @@ class TabBar(QTabBar):
Args: Args:
e: The QWheelEvent e: The QWheelEvent
""" """
if config.get('tabs', 'mousewheel-tab-switching'): if config.val.tabs.mousewheel_switching:
super().wheelEvent(e) super().wheelEvent(e)
else: else:
tabbed_browser = objreg.get('tabbed-browser', scope='window', tabbed_browser = objreg.get('tabbed-browser', scope='window',
@ -707,7 +697,7 @@ class TabBarStyle(QCommonStyle):
elif element == QStyle.CE_TabBarTabLabel: elif element == QStyle.CE_TabBarTabLabel:
if not opt.icon.isNull() and layouts.icon.isValid(): if not opt.icon.isNull() and layouts.icon.isValid():
self._draw_icon(layouts, opt, p) self._draw_icon(layouts, opt, p)
alignment = (config.get('tabs', 'title-alignment') | alignment = (config.val.tabs.title.alignment |
Qt.AlignVCenter | Qt.TextHideMnemonic) Qt.AlignVCenter | Qt.TextHideMnemonic)
self._style.drawItemText(p, layouts.text, alignment, opt.palette, self._style.drawItemText(p, layouts.text, alignment, opt.palette,
opt.state & QStyle.State_Enabled, opt.state & QStyle.State_Enabled,
@ -787,8 +777,8 @@ class TabBarStyle(QCommonStyle):
Return: Return:
A Layout namedtuple with two QRects. A Layout namedtuple with two QRects.
""" """
padding = config.get('tabs', 'padding') padding = config.val.tabs.padding
indicator_padding = config.get('tabs', 'indicator-padding') indicator_padding = config.val.tabs.indicator_padding
text_rect = QRect(opt.rect) text_rect = QRect(opt.rect)
if not text_rect.isValid(): if not text_rect.isValid():
@ -799,7 +789,7 @@ class TabBarStyle(QCommonStyle):
text_rect.adjust(padding.left, padding.top, -padding.right, text_rect.adjust(padding.left, padding.top, -padding.right,
-padding.bottom) -padding.bottom)
indicator_width = config.get('tabs', 'indicator-width') indicator_width = config.val.tabs.width.indicator
if indicator_width == 0: if indicator_width == 0:
indicator_rect = QRect() indicator_rect = QRect()
else: else:
@ -842,10 +832,10 @@ class TabBarStyle(QCommonStyle):
icon_state = (QIcon.On if opt.state & QStyle.State_Selected icon_state = (QIcon.On if opt.state & QStyle.State_Selected
else QIcon.Off) else QIcon.Off)
# reserve space for favicon when tab bar is vertical (issue #1968) # reserve space for favicon when tab bar is vertical (issue #1968)
position = config.get('tabs', 'position') position = config.val.tabs.position
if (opt.icon.isNull() and if (opt.icon.isNull() and
position in [QTabWidget.East, QTabWidget.West] and position in [QTabWidget.East, QTabWidget.West] and
config.get('tabs', 'show-favicons')): config.val.tabs.favicons.show):
tab_icon_size = icon_size tab_icon_size = icon_size
else: else:
actual_size = opt.icon.actualSize(icon_size, icon_mode, icon_state) actual_size = opt.icon.actualSize(icon_size, icon_mode, icon_state)

View File

@ -21,7 +21,8 @@
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject
from qutebrowser.utils import usertypes, log from qutebrowser.utils import usertypes, log, standarddir, objreg
from qutebrowser.misc import lineparser
class HistoryEmptyError(Exception): class HistoryEmptyError(Exception):
@ -129,3 +130,14 @@ class History(QObject):
if not self.history or text != self.history[-1]: if not self.history or text != self.history[-1]:
self.history.append(text) self.history.append(text)
self.changed.emit() self.changed.emit()
def init():
"""Initialize the LimitLineParser storing the history."""
save_manager = objreg.get('save-manager')
command_history = lineparser.LimitLineParser(
standarddir.data(), 'cmd-history',
limit='completion.cmd_history_max_items')
objreg.register('command-history', command_history)
save_manager.add_saveable('command-history', command_history.save,
command_history.changed)

View File

@ -51,8 +51,8 @@ class ConsoleLineEdit(miscwidgets.CommandLineEdit):
_namespace: The local namespace of the interpreter. _namespace: The local namespace of the interpreter.
""" """
super().__init__(parent=parent) super().__init__(parent=parent)
self.update_font() self._update_font()
objreg.get('config').changed.connect(self.update_font) config.instance.changed.connect(self._update_font)
self._history = cmdhistory.History(parent=self) self._history = cmdhistory.History(parent=self)
self.returnPressed.connect(self.on_return_pressed) self.returnPressed.connect(self.on_return_pressed)
@ -102,10 +102,10 @@ class ConsoleLineEdit(miscwidgets.CommandLineEdit):
else: else:
super().keyPressEvent(e) super().keyPressEvent(e)
@config.change_filter('fonts', 'debug-console') @config.change_filter('fonts.debug_console')
def update_font(self): def _update_font(self):
"""Set the correct font.""" """Set the correct font."""
self.setFont(config.get('fonts', 'debug-console')) self.setFont(config.val.fonts.debug_console)
class ConsoleTextEdit(QTextEdit): class ConsoleTextEdit(QTextEdit):
@ -116,17 +116,17 @@ class ConsoleTextEdit(QTextEdit):
super().__init__(parent) super().__init__(parent)
self.setAcceptRichText(False) self.setAcceptRichText(False)
self.setReadOnly(True) self.setReadOnly(True)
objreg.get('config').changed.connect(self.update_font) config.instance.changed.connect(self._update_font)
self.update_font() self._update_font()
self.setFocusPolicy(Qt.ClickFocus) self.setFocusPolicy(Qt.ClickFocus)
def __repr__(self): def __repr__(self):
return utils.get_repr(self) return utils.get_repr(self)
@config.change_filter('fonts', 'debug-console') @config.change_filter('fonts.debug_console')
def update_font(self): def _update_font(self):
"""Update font when config changed.""" """Update font when config changed."""
self.setFont(config.get('fonts', 'debug-console')) self.setFont(config.val.fonts.debug_console)
def append_text(self, text): def append_text(self, text):
"""Append new text and scroll output to bottom. """Append new text and scroll output to bottom.

View File

@ -255,8 +255,8 @@ class _CrashDialog(QDialog):
except Exception: except Exception:
self._crash_info.append(("Version info", traceback.format_exc())) self._crash_info.append(("Version info", traceback.format_exc()))
try: try:
conf = objreg.get('config') self._crash_info.append(("Config",
self._crash_info.append(("Config", conf.dump_userconfig())) config.instance.dump_userconfig()))
except Exception: except Exception:
self._crash_info.append(("Config", traceback.format_exc())) self._crash_info.append(("Config", traceback.format_exc()))
try: try:
@ -432,7 +432,7 @@ class ExceptionCrashDialog(_CrashDialog):
self._chk_log = QCheckBox("Include a debug log in the report", self._chk_log = QCheckBox("Include a debug log in the report",
checked=True) checked=True)
try: try:
if config.get('general', 'private-browsing'): if config.val.content.private_browsing:
self._chk_log.setChecked(False) self._chk_log.setChecked(False)
except Exception: except Exception:
log.misc.exception("Error while checking private browsing mode") log.misc.exception("Error while checking private browsing mode")
@ -524,7 +524,7 @@ class FatalCrashDialog(_CrashDialog):
"accessed pages in the report.", "accessed pages in the report.",
checked=True) checked=True)
try: try:
if config.get('general', 'private-browsing'): if config.val.content.private_browsing:
self._chk_history.setChecked(False) self._chk_history.setChecked(False)
except Exception: except Exception:
log.misc.exception("Error while checking private browsing mode") log.misc.exception("Error while checking private browsing mode")
@ -635,8 +635,7 @@ def dump_exception_info(exc, pages, cmdhist, qobjects):
traceback.print_exc() traceback.print_exc()
print("\n---- Config ----", file=sys.stderr) print("\n---- Config ----", file=sys.stderr)
try: try:
conf = objreg.get('config') print(config.instance.dump_userconfig(), file=sys.stderr)
print(conf.dump_userconfig(), file=sys.stderr)
except Exception: except Exception:
traceback.print_exc() traceback.print_exc()
print("\n---- Commandline args ----", file=sys.stderr) print("\n---- Commandline args ----", file=sys.stderr)

View File

@ -75,7 +75,7 @@ class ExternalEditor(QObject):
try: try:
if exitcode != 0: if exitcode != 0:
return return
encoding = config.get('general', 'editor-encoding') encoding = config.val.editor.encoding
try: try:
with open(self._file.name, 'r', encoding=encoding) as f: with open(self._file.name, 'r', encoding=encoding) as f:
text = f.read() text = f.read()
@ -102,14 +102,14 @@ class ExternalEditor(QObject):
if self._text is not None: if self._text is not None:
raise ValueError("Already editing a file!") raise ValueError("Already editing a file!")
self._text = text self._text = text
encoding = config.get('general', 'editor-encoding')
try: try:
# Close while the external process is running, as otherwise systems # Close while the external process is running, as otherwise systems
# with exclusive write access (e.g. Windows) may fail to update # with exclusive write access (e.g. Windows) may fail to update
# the file from the external editor, see # the file from the external editor, see
# https://github.com/qutebrowser/qutebrowser/issues/1767 # https://github.com/qutebrowser/qutebrowser/issues/1767
with tempfile.NamedTemporaryFile( with tempfile.NamedTemporaryFile(
mode='w', prefix='qutebrowser-editor-', encoding=encoding, mode='w', prefix='qutebrowser-editor-',
encoding=config.val.editor.encoding,
delete=False) as fobj: delete=False) as fobj:
if text: if text:
fobj.write(text) fobj.write(text)
@ -120,7 +120,7 @@ class ExternalEditor(QObject):
self._proc = guiprocess.GUIProcess(what='editor', parent=self) self._proc = guiprocess.GUIProcess(what='editor', parent=self)
self._proc.finished.connect(self.on_proc_closed) self._proc.finished.connect(self.on_proc_closed)
self._proc.error.connect(self.on_proc_error) self._proc.error.connect(self.on_proc_error)
editor = config.get('general', 'editor') editor = config.val.editor.command
executable = editor[0] executable = editor[0]
args = [arg.replace('{}', self._file.name) for arg in editor[1:]] args = [arg.replace('{}', self._file.name) for arg in editor[1:]]
log.procs.debug("Calling \"{}\" with args {}".format(executable, args)) log.procs.debug("Calling \"{}\" with args {}".format(executable, args))

View File

@ -30,8 +30,8 @@ import fnmatch
from PyQt5.QtWidgets import QLabel, QSizePolicy from PyQt5.QtWidgets import QLabel, QSizePolicy
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt
from qutebrowser.config import config, style from qutebrowser.config import config
from qutebrowser.utils import objreg, utils, usertypes from qutebrowser.utils import utils, usertypes
class KeyHintView(QLabel): class KeyHintView(QLabel):
@ -47,11 +47,11 @@ class KeyHintView(QLabel):
STYLESHEET = """ STYLESHEET = """
QLabel { QLabel {
font: {{ font['keyhint'] }}; font: {{ conf.fonts.keyhint }};
color: {{ color['keyhint.fg'] }}; color: {{ conf.colors.keyhint.fg }};
background-color: {{ color['keyhint.bg'] }}; background-color: {{ conf.colors.keyhint.bg }};
padding: 6px; padding: 6px;
{% if config.get('ui', 'status-position') == 'top' %} {% if conf.statusbar.position == 'top' %}
border-bottom-right-radius: 6px; border-bottom-right-radius: 6px;
{% else %} {% else %}
border-top-right-radius: 6px; border-top-right-radius: 6px;
@ -68,7 +68,7 @@ class KeyHintView(QLabel):
self.hide() self.hide()
self._show_timer = usertypes.Timer(self, 'keyhint_show') self._show_timer = usertypes.Timer(self, 'keyhint_show')
self._show_timer.timeout.connect(self.show) self._show_timer.timeout.connect(self.show)
style.set_register_stylesheet(self) config.set_register_stylesheet(self)
def __repr__(self): def __repr__(self):
return utils.get_repr(self, win_id=self._win_id) return utils.get_repr(self, win_id=self._win_id)
@ -90,16 +90,14 @@ class KeyHintView(QLabel):
self.hide() self.hide()
return return
blacklist = config.get('ui', 'keyhint-blacklist') or []
keyconf = objreg.get('key-config')
def blacklisted(keychain): def blacklisted(keychain):
return any(fnmatch.fnmatchcase(keychain, glob) return any(fnmatch.fnmatchcase(keychain, glob)
for glob in blacklist) for glob in config.val.keyhint.blacklist)
bindings = [(k, v) for (k, v) bindings_dict = config.key_instance.get_bindings_for(modename)
in keyconf.get_bindings_for(modename).items() bindings = [(k, v) for (k, v) in sorted(bindings_dict.items())
if k.startswith(prefix) and not utils.is_special_key(k) and if k.startswith(prefix) and
not utils.is_special_key(k) and
not blacklisted(k)] not blacklisted(k)]
if not bindings: if not bindings:
@ -107,9 +105,9 @@ class KeyHintView(QLabel):
return return
# delay so a quickly typed keychain doesn't display hints # delay so a quickly typed keychain doesn't display hints
self._show_timer.setInterval(config.get('ui', 'keyhint-delay')) self._show_timer.setInterval(config.val.keyhint.delay)
self._show_timer.start() self._show_timer.start()
suffix_color = html.escape(config.get('colors', 'keyhint.fg.suffix')) suffix_color = html.escape(config.val.colors.keyhint.suffix.fg)
text = '' text = ''
for key, cmd in bindings: for key, cmd in bindings:

View File

@ -25,7 +25,7 @@ import contextlib
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject
from qutebrowser.utils import log, utils, objreg, qtutils from qutebrowser.utils import log, utils, qtutils
from qutebrowser.config import config from qutebrowser.config import config
@ -195,8 +195,7 @@ class LimitLineParser(LineParser):
"""A LineParser with a limited count of lines. """A LineParser with a limited count of lines.
Attributes: Attributes:
_limit: The config section/option used to limit the maximum number of _limit: The config option used to limit the maximum number of lines.
lines.
""" """
def __init__(self, configdir, fname, *, limit, binary=False, parent=None): def __init__(self, configdir, fname, *, limit, binary=False, parent=None):
@ -205,33 +204,33 @@ class LimitLineParser(LineParser):
Args: Args:
configdir: Directory to read the config from, or None. configdir: Directory to read the config from, or None.
fname: Filename of the config file. fname: Filename of the config file.
limit: Config tuple (section, option) which contains a limit. limit: Config option which contains a limit.
binary: Whether to open the file in binary mode. binary: Whether to open the file in binary mode.
""" """
super().__init__(configdir, fname, binary=binary, parent=parent) super().__init__(configdir, fname, binary=binary, parent=parent)
self._limit = limit self._limit = limit
if limit is not None and configdir is not None: if limit is not None and configdir is not None:
objreg.get('config').changed.connect(self.cleanup_file) config.instance.changed.connect(self._cleanup_file)
def __repr__(self): def __repr__(self):
return utils.get_repr(self, constructor=True, return utils.get_repr(self, constructor=True,
configdir=self._configdir, fname=self._fname, configdir=self._configdir, fname=self._fname,
limit=self._limit, binary=self._binary) limit=self._limit, binary=self._binary)
@pyqtSlot(str, str) @pyqtSlot(str)
def cleanup_file(self, section, option): def _cleanup_file(self, option):
"""Delete the file if the limit was changed to 0.""" """Delete the file if the limit was changed to 0."""
assert self._configfile is not None assert self._configfile is not None
if (section, option) != self._limit: if option != self._limit:
return return
value = config.get(section, option) value = config.instance.get(option)
if value == 0: if value == 0:
if os.path.exists(self._configfile): if os.path.exists(self._configfile):
os.remove(self._configfile) os.remove(self._configfile)
def save(self): def save(self):
"""Save the config file.""" """Save the config file."""
limit = config.get(*self._limit) limit = config.instance.get(self._limit)
if limit == 0: if limit == 0:
return return
do_save = self._prepare_save() do_save = self._prepare_save()

View File

@ -24,7 +24,8 @@ from PyQt5.QtWidgets import (QLineEdit, QWidget, QHBoxLayout, QLabel,
QStyleOption, QStyle, QLayout, QApplication) QStyleOption, QStyle, QLayout, QApplication)
from PyQt5.QtGui import QValidator, QPainter from PyQt5.QtGui import QValidator, QPainter
from qutebrowser.utils import utils, objreg, qtutils, log, usertypes from qutebrowser.config import config
from qutebrowser.utils import utils, qtutils, log, usertypes
from qutebrowser.misc import cmdhistory, objects from qutebrowser.misc import cmdhistory, objects
@ -288,8 +289,7 @@ class FullscreenNotification(QLabel):
padding: 30px; padding: 30px;
""") """)
key_config = objreg.get('key-config') all_bindings = config.key_instance.get_reverse_bindings_for('normal')
all_bindings = key_config.get_reverse_bindings_for('normal')
bindings = all_bindings.get('fullscreen --leave') bindings = all_bindings.get('fullscreen --leave')
if bindings: if bindings:
key = bindings[0] key = bindings[0]

View File

@ -41,6 +41,7 @@ def msgbox(parent, title, text, *, icon, buttons=QMessageBox.Ok,
A new QMessageBox. A new QMessageBox.
""" """
box = QMessageBox(parent) box = QMessageBox(parent)
box.setAttribute(Qt.WA_DeleteOnClose)
box.setIcon(icon) box.setIcon(icon)
box.setStandardButtons(buttons) box.setStandardButtons(buttons)
if on_finished is not None: if on_finished is not None:

Some files were not shown because too many files have changed in this diff Show More