Merge https://github.com/The-Compiler/qutebrowser into insert-text
This commit is contained in:
commit
db0f8fffcd
@ -1 +0,0 @@
|
||||
qutebrowser/3rdparty/pdfjs/*
|
49
.eslintrc
49
.eslintrc
@ -1,49 +0,0 @@
|
||||
# vim: ft=yaml
|
||||
|
||||
env:
|
||||
browser: true
|
||||
|
||||
rules:
|
||||
block-scoped-var: 2
|
||||
dot-location: 2
|
||||
default-case: 2
|
||||
guard-for-in: 2
|
||||
no-div-regex: 2
|
||||
no-param-reassign: 2
|
||||
no-eq-null: 2
|
||||
no-floating-decimal: 2
|
||||
no-self-compare: 2
|
||||
no-throw-literal: 2
|
||||
no-void: 2
|
||||
radix: 2
|
||||
wrap-iife: [2, "inside"]
|
||||
brace-style: [2, "1tbs", {"allowSingleLine": true}]
|
||||
comma-style: [2, "last"]
|
||||
consistent-this: [2, "self"]
|
||||
func-style: [2, "declaration"]
|
||||
indent: [2, 4, {"SwitchCase": 1}]
|
||||
linebreak-style: [2, "unix"]
|
||||
max-nested-callbacks: [2, 3]
|
||||
no-lonely-if: 2
|
||||
no-multiple-empty-lines: [2, {"max": 2}]
|
||||
no-nested-ternary: 2
|
||||
no-unneeded-ternary: 2
|
||||
operator-assignment: [2, "always"]
|
||||
operator-linebreak: [2, "after"]
|
||||
keyword-spacing: 2
|
||||
space-before-blocks: [2, "always"]
|
||||
space-before-function-paren: [2, {"anonymous": "never", "named": "never"}]
|
||||
object-curly-spacing: [2, "never"]
|
||||
array-bracket-spacing: [2, "never"]
|
||||
computed-property-spacing: [2, "never"]
|
||||
space-in-parens: [2, "never"]
|
||||
space-unary-ops: [2, {"words": true, "nonwords": false}]
|
||||
spaced-comment: [2, "always"]
|
||||
max-depth: [2, 5]
|
||||
max-len: [2, 79, 4]
|
||||
max-params: [2, 5]
|
||||
max-statements: [2, 30]
|
||||
no-bitwise: 2
|
||||
quote-props: [2, "always"]
|
||||
global-strict: 0
|
||||
quotes: 0
|
@ -28,6 +28,10 @@ Added
|
||||
completion category headers.
|
||||
- New `:debug-log-capacity` command to adjust how many lines are logged into RAM
|
||||
(to report bugs which are difficult to reproduce).
|
||||
- New `hide-unmatched-rapid-hints` option to not hide hint unmatched hint labels
|
||||
in rapid mode.
|
||||
- New `{clipboard}` and `{primary}` replacements for the commandline which
|
||||
replace the `:paste` command.
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
@ -44,6 +48,47 @@ Changed
|
||||
(i.e. to open it at the position it would be opened if it was a clicked link)
|
||||
- `:download-open` and `:prompt-open-download` now have an optional `cmdline`
|
||||
argument to pass a commandline to open the download with.
|
||||
- `:yank` now has a position argument to select what to yank instead of using
|
||||
flags.
|
||||
- Replacements like `{url}` can now also be used in the middle of an argument.
|
||||
Consequently, commands taking another command (`:later`, `:repeat` and
|
||||
`:bind`) now don't immediately evaluate variables.
|
||||
- Tab titles in the `:buffer` completion now update correctly when a page's
|
||||
title is changed via javascript.
|
||||
- `:hint` now has a `--mode <mode>` flag to override the hint mode configured
|
||||
using the `hints -> mode` setting.
|
||||
- With `new-instance-open-target` set to a tab option, the tab is now opened in
|
||||
the most recently focused (instead of the last opened) window. This can be
|
||||
configured with the new `new-instance-open-target.window` setting.
|
||||
It can also be set to `last-visible` to show the pages in the most recently
|
||||
visible window.
|
||||
- Word hints now are more clever about getting the element text from some elements.
|
||||
- Completions for `:help` and `:bind` now also show hidden commands
|
||||
- The `:buffer` completion now also filters using the first column (id).
|
||||
- `:undo` has been improved to reopen tabs at the position they were closed.
|
||||
|
||||
Deprecated
|
||||
~~~~~~~~~~
|
||||
|
||||
- The `:paste` command got deprecated as `:open` with `{clipboard}` and
|
||||
`{primary}` can be used instead.
|
||||
|
||||
Removed
|
||||
~~~~~~~
|
||||
|
||||
- The `:yank-selected` command got merged into `:yank` as `:yank selection`
|
||||
and thus removed.
|
||||
- The `:completion-item-prev` and `:completion-item-next` commands got merged
|
||||
into a new `:completion-focus {prev,next}` command and thus removed.
|
||||
- The `ui -> hide-mouse-cursor` setting since it was completely broken and
|
||||
nobody seemed to care.
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- `:bind` can now be used to bind to an alias (binding by editing `keys.conf`
|
||||
already worked before)
|
||||
- The command completion now updates correctly when changing aliases
|
||||
|
||||
v0.8.3 (unreleased)
|
||||
-------------------
|
||||
@ -52,6 +97,8 @@ Fixed
|
||||
~~~~~
|
||||
|
||||
- Fixed crash when doing `:<space><enter>`, another corner-case introduced in v0.8.0
|
||||
- Fixed `:open-editor` (`<Ctrl-e>`) on Windows
|
||||
- Fixed crash when setting `general -> auto-save-interval` to a too big value.
|
||||
|
||||
v0.8.2
|
||||
------
|
||||
|
@ -35,8 +35,8 @@ exclude pytest.ini
|
||||
exclude qutebrowser.rcc
|
||||
exclude .coveragerc
|
||||
exclude .pylintrc
|
||||
exclude .eslintrc
|
||||
exclude .eslintignore
|
||||
exclude qutebrowser/javascript/.eslintrc.yaml
|
||||
exclude qutebrowser/javascript/.eslintignore
|
||||
exclude doc/help
|
||||
exclude .appveyor.yml
|
||||
exclude .travis.yml
|
||||
|
@ -145,14 +145,14 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* Antoni Boucher
|
||||
* Lamar Pavel
|
||||
* Bruno Oliveira
|
||||
* Jan Verbeek
|
||||
* Alexander Cogneau
|
||||
* Felix Van der Jeugt
|
||||
* Jakub Klinkovský
|
||||
* Martin Tournoij
|
||||
* Marshall Lochbaum
|
||||
* Jakub Klinkovský
|
||||
* Felix Van der Jeugt
|
||||
* Martin Tournoij
|
||||
* Raphael Pierzina
|
||||
* Joel Torstensson
|
||||
* Jan Verbeek
|
||||
* Patric Schmitz
|
||||
* Tarcisio Fedrizzi
|
||||
* Claude
|
||||
@ -175,6 +175,7 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* Clayton Craft
|
||||
* nanjekyejoannah
|
||||
* Oliver Caldwell
|
||||
* Niklas Haas
|
||||
* Jonas Schürmann
|
||||
* error800
|
||||
* Liam BEGUIN
|
||||
@ -212,6 +213,7 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* adam
|
||||
* Samir Benmendil
|
||||
* Regina Hug
|
||||
* Michael Hoang
|
||||
* Mathias Fussenegger
|
||||
* Marcelo Santos
|
||||
* Jean-Louis Fuchs
|
||||
@ -225,6 +227,7 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* haxwithaxe
|
||||
* evan
|
||||
* dylan araps
|
||||
* addictedtoflames
|
||||
* Xitian9
|
||||
* Tomas Orsava
|
||||
* Tom Janson
|
||||
@ -233,6 +236,7 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* Thiago Barroso Perrotta
|
||||
* Sorokin Alexei
|
||||
* Noah Huesser
|
||||
* Moez Bouhlel
|
||||
* Matthias Lisin
|
||||
* Marcel Schilling
|
||||
* Julie Engel
|
||||
|
@ -1,3 +1,7 @@
|
||||
// DO NOT EDIT THIS FILE DIRECTLY!
|
||||
// It is autogenerated from docstrings by running:
|
||||
// $ python3 scripts/dev/src2asciidoc.py
|
||||
|
||||
= Commands
|
||||
|
||||
== Normal commands
|
||||
@ -34,7 +38,6 @@
|
||||
|<<messages,messages>>|Show a log of past messages.
|
||||
|<<navigate,navigate>>|Open typical prev/next links or navigate using the URL path.
|
||||
|<<open,open>>|Open a URL in the current/[count]th tab.
|
||||
|<<paste,paste>>|Open a page from the clipboard.
|
||||
|<<print,print>>|Print the current/[count]th tab.
|
||||
|<<quickmark-add,quickmark-add>>|Add a new quickmark.
|
||||
|<<quickmark-del,quickmark-del>>|Delete a quickmark.
|
||||
@ -66,8 +69,7 @@
|
||||
|<<undo,undo>>|Re-open a closed tab (optionally skipping [count] closed tabs).
|
||||
|<<view-source,view-source>>|Show the source of the current page.
|
||||
|<<wq,wq>>|Save open pages and quit.
|
||||
|<<yank,yank>>|Yank the current URL/title to the clipboard or primary selection.
|
||||
|<<yank-selected,yank-selected>>|Yank the selected text 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-in,zoom-in>>|Increase the zoom level for the current tab.
|
||||
|<<zoom-out,zoom-out>>|Decrease the zoom level for the current tab.
|
||||
@ -111,6 +113,7 @@ Bind a key to a command.
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
|
||||
* This command does not replace variables like +\{url\}+.
|
||||
|
||||
[[bookmark-add]]
|
||||
=== bookmark-add
|
||||
@ -320,12 +323,12 @@ Show help about a command or setting.
|
||||
|
||||
[[hint]]
|
||||
=== hint
|
||||
Syntax: +:hint [*--rapid*] ['group'] ['target'] ['args' ['args' ...]]+
|
||||
Syntax: +:hint [*--rapid*] [*--mode* 'mode'] ['group'] ['target'] ['args' ['args' ...]]+
|
||||
|
||||
Start hinting.
|
||||
|
||||
==== positional arguments
|
||||
* +'group'+: The hinting mode to use.
|
||||
* +'group'+: The element types to hint.
|
||||
|
||||
- `all`: All clickable elements.
|
||||
- `links`: Only links.
|
||||
@ -376,6 +379,15 @@ Start hinting.
|
||||
* +*-r*+, +*--rapid*+: Whether to do rapid hinting. This is only possible with targets `tab` (with background-tabs=true), `tab-bg`,
|
||||
`window`, `run`, `hover`, `userscript` and `spawn`.
|
||||
|
||||
* +*-m*+, +*--mode*+: The hinting mode to use.
|
||||
|
||||
- `number`: Use numeric hints.
|
||||
- `letter`: Use the chars in the hints->chars settings.
|
||||
- `word`: Use hint words based on the html elements and the
|
||||
extra words.
|
||||
|
||||
|
||||
|
||||
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
@ -425,6 +437,7 @@ Execute a command after some time.
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
|
||||
* This command does not replace variables like +\{url\}+.
|
||||
|
||||
[[messages]]
|
||||
=== messages
|
||||
@ -473,6 +486,8 @@ Syntax: +:open [*--implicit*] [*--bg*] [*--tab*] [*--window*] ['url']+
|
||||
|
||||
Open a URL in the current/[count]th tab.
|
||||
|
||||
If the URL contains newlines, each line gets opened in its own tab.
|
||||
|
||||
==== positional arguments
|
||||
* +'url'+: The URL to open.
|
||||
|
||||
@ -489,20 +504,6 @@ The tab index to open the URL in.
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
|
||||
[[paste]]
|
||||
=== paste
|
||||
Syntax: +:paste [*--sel*] [*--tab*] [*--bg*] [*--window*]+
|
||||
|
||||
Open a page from the clipboard.
|
||||
|
||||
If the pasted text contains newlines, each line gets opened in its own tab.
|
||||
|
||||
==== optional arguments
|
||||
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
|
||||
* +*-t*+, +*--tab*+: Open in a new tab.
|
||||
* +*-b*+, +*--bg*+: Open in a background tab.
|
||||
* +*-w*+, +*--window*+: Open in new window.
|
||||
|
||||
[[print]]
|
||||
=== print
|
||||
Syntax: +:print [*--preview*] [*--pdf* 'file']+
|
||||
@ -592,6 +593,7 @@ Repeat a given command.
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
|
||||
* This command does not replace variables like +\{url\}+.
|
||||
|
||||
[[report]]
|
||||
=== report
|
||||
@ -853,25 +855,25 @@ Save open pages and quit.
|
||||
|
||||
[[yank]]
|
||||
=== yank
|
||||
Syntax: +:yank [*--title*] [*--sel*] [*--domain*] [*--pretty*]+
|
||||
Syntax: +:yank [*--sel*] [*--keep*] ['what']+
|
||||
|
||||
Yank the current URL/title to the clipboard or primary selection.
|
||||
Yank something to the clipboard or primary selection.
|
||||
|
||||
==== optional arguments
|
||||
* +*-t*+, +*--title*+: Yank the title instead of the URL.
|
||||
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
|
||||
* +*-d*+, +*--domain*+: Yank only the scheme, domain, and port number.
|
||||
* +*-p*+, +*--pretty*+: Yank the URL in pretty decoded form.
|
||||
==== positional arguments
|
||||
* +'what'+: What to yank.
|
||||
|
||||
- `url`: The current URL.
|
||||
- `pretty-url`: The URL in pretty decoded form.
|
||||
- `title`: The current page's title.
|
||||
- `domain`: The current scheme, domain, and port number.
|
||||
- `selection`: The selection under the cursor.
|
||||
|
||||
|
||||
[[yank-selected]]
|
||||
=== yank-selected
|
||||
Syntax: +:yank-selected [*--sel*] [*--keep*]+
|
||||
|
||||
Yank the selected text to the clipboard or primary selection.
|
||||
|
||||
==== optional arguments
|
||||
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
|
||||
* +*-k*+, +*--keep*+: If given, stay in visual mode after yanking.
|
||||
* +*-k*+, +*--keep*+: Stay in visual mode after yanking the selection.
|
||||
|
||||
[[zoom]]
|
||||
=== zoom
|
||||
@ -912,8 +914,7 @@ How many steps to zoom out.
|
||||
|<<command-history-next,command-history-next>>|Go forward in the commandline history.
|
||||
|<<command-history-prev,command-history-prev>>|Go back in the commandline history.
|
||||
|<<completion-item-del,completion-item-del>>|Delete the current completion item.
|
||||
|<<completion-item-next,completion-item-next>>|Select the next completion item.
|
||||
|<<completion-item-prev,completion-item-prev>>|Select the previous completion item.
|
||||
|<<completion-item-focus,completion-item-focus>>|Shift the focus of the completion menu to another item.
|
||||
|<<drop-selection,drop-selection>>|Drop selection and keep selection mode enabled.
|
||||
|<<enter-mode,enter-mode>>|Enter a key mode.
|
||||
|<<follow-hint,follow-hint>>|Follow a hint.
|
||||
@ -990,13 +991,14 @@ Go back in the commandline history.
|
||||
=== completion-item-del
|
||||
Delete the current completion item.
|
||||
|
||||
[[completion-item-next]]
|
||||
=== completion-item-next
|
||||
Select the next completion item.
|
||||
[[completion-item-focus]]
|
||||
=== completion-item-focus
|
||||
Syntax: +:completion-item-focus 'which'+
|
||||
|
||||
[[completion-item-prev]]
|
||||
=== completion-item-prev
|
||||
Select the previous completion item.
|
||||
Shift the focus of the completion menu to another item.
|
||||
|
||||
==== positional arguments
|
||||
* +'which'+: 'next' or 'prev'
|
||||
|
||||
[[drop-selection]]
|
||||
=== drop-selection
|
||||
|
@ -1,3 +1,7 @@
|
||||
// DO NOT EDIT THIS FILE DIRECTLY!
|
||||
// It is autogenerated from docstrings by running:
|
||||
// $ python3 scripts/dev/src2asciidoc.py
|
||||
|
||||
= Settings
|
||||
|
||||
.Quick reference for section ``general''
|
||||
@ -19,6 +23,7 @@
|
||||
|<<general-site-specific-quirks,site-specific-quirks>>|Enable workarounds for broken sites.
|
||||
|<<general-default-encoding,default-encoding>>|Default encoding to use for websites.
|
||||
|<<general-new-instance-open-target,new-instance-open-target>>|How to open links in an existing instance if a new one is launched.
|
||||
|<<general-new-instance-open-target.window,new-instance-open-target.window>>|Which window to choose when opening links as new tabs.
|
||||
|<<general-log-javascript-console,log-javascript-console>>|How to log javascript console messages.
|
||||
|<<general-save-session,save-session>>|Whether to always save the open pages.
|
||||
|<<general-session-default-name,session-default-name>>|The name of the session to save by default, or empty for the last loaded session.
|
||||
@ -45,7 +50,6 @@
|
||||
|<<ui-hide-statusbar,hide-statusbar>>|Whether to hide the statusbar unless a message is shown.
|
||||
|<<ui-statusbar-padding,statusbar-padding>>|Padding for statusbar (top, bottom, left, right).
|
||||
|<<ui-window-title-format,window-title-format>>|The format to use for the window title. The following placeholders are defined:
|
||||
|<<ui-hide-mouse-cursor,hide-mouse-cursor>>|Whether to hide the mouse cursor.
|
||||
|<<ui-modal-js-dialog,modal-js-dialog>>|Use standard JavaScript modal dialog for alert() and confirm()
|
||||
|<<ui-hide-wayland-decoration,hide-wayland-decoration>>|Hide the window decoration when using wayland (requires restart)
|
||||
|<<ui-keyhint-blacklist,keyhint-blacklist>>|Keychains that shouldn't be shown in the keyhint dialog
|
||||
@ -87,8 +91,8 @@
|
||||
[options="header",width="75%",cols="25%,75%"]
|
||||
|==============
|
||||
|Setting|Description
|
||||
|<<input-timeout,timeout>>|Timeout for ambiguous key bindings.
|
||||
|<<input-partial-timeout,partial-timeout>>|Timeout for partially typed key bindings.
|
||||
|<<input-timeout,timeout>>|Timeout (in milliseconds) for ambiguous key bindings.
|
||||
|<<input-partial-timeout,partial-timeout>>|Timeout (in milliseconds) for partially typed key bindings.
|
||||
|<<input-insert-mode-on-plugins,insert-mode-on-plugins>>|Whether to switch to insert mode when clicking flash and other plugins.
|
||||
|<<input-auto-leave-insert-mode,auto-leave-insert-mode>>|Whether to leave insert mode if a non-editable element is clicked.
|
||||
|<<input-auto-insert-mode,auto-insert-mode>>|Whether to automatically enter insert mode if an editable element is focused after page load.
|
||||
@ -182,10 +186,11 @@
|
||||
|<<hints-uppercase,uppercase>>|Make chars in hint strings uppercase.
|
||||
|<<hints-dictionary,dictionary>>|The dictionary file to be used by the word hints.
|
||||
|<<hints-auto-follow,auto-follow>>|Follow a hint immediately when the hint text is completely matched.
|
||||
|<<hints-auto-follow-timeout,auto-follow-timeout>>|A timeout to inhibit normal-mode key bindings after a successfulauto-follow.
|
||||
|<<hints-auto-follow-timeout,auto-follow-timeout>>|A timeout (in milliseconds) to inhibit normal-mode key bindings after a successful auto-follow.
|
||||
|<<hints-next-regexes,next-regexes>>|A comma-separated list of regexes to use for 'next' links.
|
||||
|<<hints-prev-regexes,prev-regexes>>|A comma-separated list of regexes to use for 'prev' links.
|
||||
|<<hints-find-implementation,find-implementation>>|Which implementation to use to find elements to hint.
|
||||
|<<hints-hide-unmatched-rapid-hints,hide-unmatched-rapid-hints>>|Controls hiding unmatched hints in rapid mode.
|
||||
|==============
|
||||
|
||||
.Quick reference for section ``colors''
|
||||
@ -443,6 +448,18 @@ Valid values:
|
||||
|
||||
Default: +pass:[tab]+
|
||||
|
||||
[[general-new-instance-open-target.window]]
|
||||
=== new-instance-open-target.window
|
||||
Which window to choose when opening links as new tabs.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +last-opened+: Open new tabs in the last opened window.
|
||||
* +last-focused+: Open new tabs in the most recently focused window.
|
||||
* +last-visible+: Open new tabs in the most recently visible window.
|
||||
|
||||
Default: +pass:[last-focused]+
|
||||
|
||||
[[general-log-javascript-console]]
|
||||
=== log-javascript-console
|
||||
How to log javascript console messages.
|
||||
@ -455,8 +472,6 @@ Valid values:
|
||||
|
||||
Default: +pass:[debug]+
|
||||
|
||||
This setting is only available with the QtWebKit backend.
|
||||
|
||||
[[general-save-session]]
|
||||
=== save-session
|
||||
Whether to always save the open pages.
|
||||
@ -646,17 +661,6 @@ The format to use for the window title. The following placeholders are defined:
|
||||
|
||||
Default: +pass:[{perc}{title}{title_sep}qutebrowser]+
|
||||
|
||||
[[ui-hide-mouse-cursor]]
|
||||
=== hide-mouse-cursor
|
||||
Whether to hide the mouse cursor.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[ui-modal-js-dialog]]
|
||||
=== modal-js-dialog
|
||||
Use standard JavaScript modal dialog for alert() and confirm()
|
||||
@ -900,7 +904,7 @@ Options related to input modes.
|
||||
|
||||
[[input-timeout]]
|
||||
=== timeout
|
||||
Timeout for ambiguous key bindings.
|
||||
Timeout (in milliseconds) for ambiguous key bindings.
|
||||
|
||||
If the current input forms both a complete match and a partial match, the complete match will be executed after this time.
|
||||
|
||||
@ -908,7 +912,7 @@ Default: +pass:[500]+
|
||||
|
||||
[[input-partial-timeout]]
|
||||
=== partial-timeout
|
||||
Timeout for partially typed key bindings.
|
||||
Timeout (in milliseconds) for partially typed key bindings.
|
||||
|
||||
If the current input forms only partial matches, the keystring will be cleared after this time.
|
||||
|
||||
@ -1667,7 +1671,7 @@ Default: +pass:[true]+
|
||||
|
||||
[[hints-auto-follow-timeout]]
|
||||
=== auto-follow-timeout
|
||||
A timeout to inhibit normal-mode key bindings after a successfulauto-follow.
|
||||
A timeout (in milliseconds) to inhibit normal-mode key bindings after a successful auto-follow.
|
||||
|
||||
Default: +pass:[0]+
|
||||
|
||||
@ -1694,6 +1698,17 @@ Valid values:
|
||||
|
||||
Default: +pass:[python]+
|
||||
|
||||
[[hints-hide-unmatched-rapid-hints]]
|
||||
=== hide-unmatched-rapid-hints
|
||||
Controls hiding unmatched hints in rapid mode.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: +pass:[true]+
|
||||
|
||||
== searchengines
|
||||
Definitions of search engines which can be used via the address bar.
|
||||
The searchengine named `DEFAULT` is used when `general -> auto-search` is true and something else than a URL was entered to be opened. Other search engines can be used by prepending the search engine name to the search term, e.g. `:open google qutebrowser`. The string `{}` will be replaced by the search term, use `{{` and `}}` for literal `{`/`}` signs.
|
||||
|
@ -2,4 +2,4 @@
|
||||
|
||||
codecov==2.0.5
|
||||
coverage==4.2
|
||||
requests==2.10.0
|
||||
requests==2.11.0
|
||||
|
@ -21,5 +21,5 @@ pep8-naming==0.4.1
|
||||
pycodestyle==2.0.0
|
||||
pydocstyle==1.0.0
|
||||
pyflakes==1.2.3
|
||||
pyparsing==2.1.5
|
||||
pyparsing==2.1.6
|
||||
six==1.10.0
|
||||
|
@ -1,2 +1,2 @@
|
||||
pip==8.1.2
|
||||
setuptools==25.1.4
|
||||
setuptools==25.1.6
|
||||
|
@ -6,6 +6,6 @@ lazy-object-proxy==1.2.2
|
||||
mccabe==0.5.2
|
||||
-e git+https://github.com/PyCQA/pylint.git#egg=pylint
|
||||
./scripts/dev/pylint_checkers
|
||||
requests==2.10.0
|
||||
requests==2.11.0
|
||||
six==1.10.0
|
||||
wrapt==1.10.8
|
||||
|
@ -7,7 +7,7 @@ lazy-object-proxy==1.2.2
|
||||
mccabe==0.5.2
|
||||
pylint==1.6.4
|
||||
./scripts/dev/pylint_checkers
|
||||
requests==2.10.0
|
||||
requests==2.11.0
|
||||
six==1.10.0
|
||||
uritemplate.py==0.3.0
|
||||
wrapt==1.10.8
|
||||
|
@ -2,7 +2,12 @@ bzr+lp:beautifulsoup
|
||||
git+https://github.com/cherrypy/cherrypy.git
|
||||
hg+https://bitbucket.org/ned/coveragepy
|
||||
git+https://github.com/micheles/decorator.git
|
||||
git+https://github.com/pallets/flask.git
|
||||
|
||||
# We need to use flask < 0.11 because of
|
||||
# https://github.com/Runscope/httpbin/issues/290
|
||||
# git+https://github.com/pallets/flask.git
|
||||
Flask==0.10.1 # rq.filter: < 0.11.0
|
||||
|
||||
git+https://github.com/miracle2k/python-glob2.git
|
||||
git+https://github.com/Runscope/httpbin.git
|
||||
git+https://github.com/HypothesisWorks/hypothesis-python.git
|
||||
|
@ -18,16 +18,16 @@ py==1.4.31
|
||||
pytest==2.9.2
|
||||
pytest-bdd==2.17.0
|
||||
pytest-catchlog==1.2.2
|
||||
pytest-cov==2.3.0
|
||||
pytest-cov==2.3.1
|
||||
pytest-faulthandler==1.3.0
|
||||
pytest-instafail==0.3.0
|
||||
pytest-mock==1.2
|
||||
pytest-qt==2.0.0
|
||||
pytest-repeat==0.3.0
|
||||
pytest-rerunfailures==2.0.0
|
||||
pytest-repeat==0.4.0
|
||||
pytest-rerunfailures==2.0.1
|
||||
pytest-travis-fold==1.2.0
|
||||
pytest-warnings==0.1.0
|
||||
pytest-xvfb==0.2.0
|
||||
pytest-xvfb==0.2.1
|
||||
six==1.10.0
|
||||
vulture==0.10
|
||||
Werkzeug==0.11.10
|
||||
|
@ -3,4 +3,4 @@
|
||||
pluggy==0.3.1
|
||||
py==1.4.31
|
||||
tox==2.3.1
|
||||
virtualenv==15.0.2
|
||||
virtualenv==15.0.3
|
||||
|
@ -33,9 +33,9 @@ import tokenize
|
||||
|
||||
from PyQt5.QtWidgets import QApplication, QWidget
|
||||
from PyQt5.QtWebKit import QWebSettings
|
||||
from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon, QCursor, QWindow
|
||||
from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon, QWindow
|
||||
from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QTimer, QUrl,
|
||||
QObject, Qt, QEvent, pyqtSignal)
|
||||
QObject, QEvent, pyqtSignal)
|
||||
try:
|
||||
import hunter
|
||||
except ImportError:
|
||||
@ -46,8 +46,8 @@ import qutebrowser.resources
|
||||
from qutebrowser.completion.models import instances as completionmodels
|
||||
from qutebrowser.commands import cmdutils, runners, cmdexc
|
||||
from qutebrowser.config import style, config, websettings, configexc
|
||||
from qutebrowser.browser import urlmarks, adblock
|
||||
from qutebrowser.browser.webkit import cookies, cache, history, downloads
|
||||
from qutebrowser.browser import urlmarks, adblock, history
|
||||
from qutebrowser.browser.webkit import cookies, cache, downloads
|
||||
from qutebrowser.browser.webkit.network import (qutescheme, proxy,
|
||||
networkmanager)
|
||||
from qutebrowser.mainwindow import mainwindow
|
||||
@ -339,19 +339,20 @@ def _save_version():
|
||||
|
||||
def on_focus_changed(_old, new):
|
||||
"""Register currently focused main window in the object registry."""
|
||||
if not isinstance(new, QWidget) and new is not None:
|
||||
if new is None:
|
||||
return
|
||||
|
||||
if not isinstance(new, QWidget):
|
||||
log.misc.debug("on_focus_changed called with non-QWidget {!r}".format(
|
||||
new))
|
||||
return
|
||||
|
||||
if new is None or not isinstance(new, mainwindow.MainWindow):
|
||||
try:
|
||||
objreg.delete('last-focused-main-window')
|
||||
except KeyError:
|
||||
pass
|
||||
qApp.restoreOverrideCursor()
|
||||
else:
|
||||
objreg.register('last-focused-main-window', new.window(), update=True)
|
||||
_maybe_hide_mouse_cursor()
|
||||
window = new.window()
|
||||
if isinstance(window, mainwindow.MainWindow):
|
||||
objreg.register('last-focused-main-window', window, update=True)
|
||||
# A focused window must also be visible, and in this case we should
|
||||
# consider it as the most recently looked-at window
|
||||
objreg.register('last-visible-main-window', window, update=True)
|
||||
|
||||
|
||||
def open_desktopservices_url(url):
|
||||
@ -362,17 +363,6 @@ def open_desktopservices_url(url):
|
||||
tabbed_browser.tabopen(url)
|
||||
|
||||
|
||||
@config.change_filter('ui', 'hide-mouse-cursor', function=True)
|
||||
def _maybe_hide_mouse_cursor():
|
||||
"""Hide the mouse cursor if it isn't yet and it's configured."""
|
||||
if config.get('ui', 'hide-mouse-cursor'):
|
||||
if qApp.overrideCursor() is not None:
|
||||
return
|
||||
qApp.setOverrideCursor(QCursor(Qt.BlankCursor))
|
||||
else:
|
||||
qApp.restoreOverrideCursor()
|
||||
|
||||
|
||||
def _init_modules(args, crash_handler):
|
||||
"""Initialize all 'modules' which need to be initialized.
|
||||
|
||||
@ -434,8 +424,6 @@ def _init_modules(args, crash_handler):
|
||||
os.environ['QT_WAYLAND_DISABLE_WINDOWDECORATION'] = '1'
|
||||
else:
|
||||
os.environ.pop('QT_WAYLAND_DISABLE_WINDOWDECORATION', None)
|
||||
_maybe_hide_mouse_cursor()
|
||||
objreg.get('config').changed.connect(_maybe_hide_mouse_cursor)
|
||||
temp_downloads = downloads.TempDownloadManager(qApp)
|
||||
objreg.register('temporary-downloads', temp_downloads)
|
||||
|
||||
|
@ -29,6 +29,7 @@ from qutebrowser.keyinput import modeman
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import utils, objreg, usertypes, message, log, qtutils
|
||||
from qutebrowser.misc import miscwidgets
|
||||
from qutebrowser.browser import mouse
|
||||
|
||||
|
||||
tab_id_gen = itertools.count(0)
|
||||
@ -455,6 +456,7 @@ class AbstractTab(QWidget):
|
||||
url_changed = pyqtSignal(QUrl)
|
||||
shutting_down = pyqtSignal()
|
||||
contents_size_changed = pyqtSignal(QSizeF)
|
||||
add_history_item = pyqtSignal(QUrl, QUrl, str) # url, requested url, title
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
self.win_id = win_id
|
||||
@ -480,6 +482,7 @@ class AbstractTab(QWidget):
|
||||
self._progress = 0
|
||||
self._has_ssl_errors = False
|
||||
self._load_status = usertypes.LoadStatus.none
|
||||
self._mouse_event_filter = mouse.MouseEventFilter(self, parent=self)
|
||||
self.backend = None
|
||||
|
||||
def _set_widget(self, widget):
|
||||
@ -493,6 +496,10 @@ class AbstractTab(QWidget):
|
||||
self.search._widget = widget
|
||||
self.printing._widget = widget
|
||||
widget.mouse_wheel_zoom.connect(self.zoom._on_mouse_wheel_zoom)
|
||||
self._install_event_filter()
|
||||
|
||||
def _install_event_filter(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def _set_load_status(self, val):
|
||||
"""Setter for load_status."""
|
||||
@ -533,6 +540,13 @@ class AbstractTab(QWidget):
|
||||
if not self.title():
|
||||
self.title_changed.emit(self.url().toDisplayString())
|
||||
|
||||
@pyqtSlot()
|
||||
def _on_history_trigger(self):
|
||||
"""Emit add_history_item when triggered by backend-specific signal."""
|
||||
url = self.url()
|
||||
requested_url = self.url(requested=True)
|
||||
self.add_history_item.emit(url, requested_url, self.title())
|
||||
|
||||
@pyqtSlot(int)
|
||||
def _on_load_progress(self, perc):
|
||||
self._progress = perc
|
||||
@ -542,7 +556,7 @@ class AbstractTab(QWidget):
|
||||
def _on_ssl_errors(self):
|
||||
self._has_ssl_errors = True
|
||||
|
||||
def url(self):
|
||||
def url(self, requested=False):
|
||||
raise NotImplementedError
|
||||
|
||||
def progress(self):
|
||||
@ -613,6 +627,15 @@ class AbstractTab(QWidget):
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def find_focus_element(self, callback):
|
||||
"""Find the focused element on the page async.
|
||||
|
||||
Args:
|
||||
callback: The callback to be called when the search finished.
|
||||
Called with a WebEngineElement or None.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def __repr__(self):
|
||||
try:
|
||||
url = utils.elide(self.url().toDisplayString(QUrl.EncodeUnicode),
|
||||
|
@ -40,7 +40,7 @@ import pygments.formatters
|
||||
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
|
||||
from qutebrowser.config import config, configexc
|
||||
from qutebrowser.browser import urlmarks, browsertab, inspector, navigate
|
||||
from qutebrowser.browser.webkit import webelem, downloads, mhtml
|
||||
from qutebrowser.browser.webkit import webkitelem, downloads, mhtml
|
||||
from qutebrowser.keyinput import modeman
|
||||
from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils,
|
||||
objreg, utils, typing, javascript)
|
||||
@ -236,6 +236,8 @@ class CommandDispatcher:
|
||||
bg=False, tab=False, window=False, count=None):
|
||||
"""Open a URL in the current/[count]th tab.
|
||||
|
||||
If the URL contains newlines, each line gets opened in its own tab.
|
||||
|
||||
Args:
|
||||
url: The URL to open.
|
||||
bg: Open in a new background tab.
|
||||
@ -247,35 +249,73 @@ class CommandDispatcher:
|
||||
"""
|
||||
if url is None:
|
||||
if tab or bg or window:
|
||||
url = config.get('general', 'default-page')
|
||||
urls = [config.get('general', 'default-page')]
|
||||
else:
|
||||
raise cmdexc.CommandError("No URL given, but -t/-b/-w is not "
|
||||
"set!")
|
||||
else:
|
||||
try:
|
||||
url = objreg.get('quickmark-manager').get(url)
|
||||
except urlmarks.Error:
|
||||
try:
|
||||
url = urlutils.fuzzy_url(url)
|
||||
except urlutils.InvalidUrlError as e:
|
||||
# We don't use cmdexc.CommandError here as this can be
|
||||
# called async from edit_url
|
||||
message.error(self._win_id, str(e))
|
||||
return
|
||||
if tab or bg or window:
|
||||
self._open(url, tab, bg, window, not implicit)
|
||||
else:
|
||||
curtab = self._cntwidget(count)
|
||||
if curtab is None:
|
||||
if count is None:
|
||||
# We want to open a URL in the current tab, but none exists
|
||||
# yet.
|
||||
self._tabbed_browser.tabopen(url)
|
||||
else:
|
||||
# Explicit count with a tab that doesn't exist.
|
||||
return
|
||||
urls = self._parse_url_input(url)
|
||||
for i, cur_url in enumerate(urls):
|
||||
if not window and i > 0:
|
||||
tab = False
|
||||
bg = True
|
||||
if tab or bg or window:
|
||||
self._open(cur_url, tab, bg, window, not implicit)
|
||||
else:
|
||||
curtab.openurl(url)
|
||||
curtab = self._cntwidget(count)
|
||||
if curtab is None:
|
||||
if count is None:
|
||||
# We want to open a URL in the current tab, but none
|
||||
# exists yet.
|
||||
self._tabbed_browser.tabopen(cur_url)
|
||||
else:
|
||||
# Explicit count with a tab that doesn't exist.
|
||||
return
|
||||
else:
|
||||
curtab.openurl(cur_url)
|
||||
|
||||
def _parse_url(self, url, *, force_search=False):
|
||||
"""Parse a URL or quickmark or search query.
|
||||
|
||||
Args:
|
||||
url: The URL to parse.
|
||||
force_search: Whether to force a search even if the content can be
|
||||
interpreted as a URL or a path.
|
||||
|
||||
Return:
|
||||
A URL that can be opened.
|
||||
"""
|
||||
try:
|
||||
return objreg.get('quickmark-manager').get(url)
|
||||
except urlmarks.Error:
|
||||
try:
|
||||
return urlutils.fuzzy_url(url, force_search=force_search)
|
||||
except urlutils.InvalidUrlError as e:
|
||||
# We don't use cmdexc.CommandError here as this can be
|
||||
# called async from edit_url
|
||||
message.error(self._win_id, str(e))
|
||||
return None
|
||||
|
||||
def _parse_url_input(self, url):
|
||||
"""Parse a URL or newline-separated list of URLs.
|
||||
|
||||
Args:
|
||||
url: The URL or list to parse.
|
||||
|
||||
Return:
|
||||
A list of URLs that can be opened.
|
||||
"""
|
||||
force_search = False
|
||||
urllist = [u for u in url.split('\n') if u.strip()]
|
||||
if (len(urllist) > 1 and not urlutils.is_url(urllist[0]) and
|
||||
urlutils.get_path_if_valid(urllist[0], check_exists=True)
|
||||
is None):
|
||||
urllist = [url]
|
||||
force_search = True
|
||||
for cur_url in urllist:
|
||||
parsed = self._parse_url(cur_url, force_search=force_search)
|
||||
if parsed is not None:
|
||||
yield parsed
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', name='reload',
|
||||
scope='window')
|
||||
@ -439,8 +479,7 @@ class CommandDispatcher:
|
||||
"""
|
||||
self._back_forward(tab, bg, window, count, forward=True)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||
backend=usertypes.Backend.QtWebKit)
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
@cmdutils.argument('where', choices=['prev', 'next', 'up', 'increment',
|
||||
'decrement'])
|
||||
def navigate(self, where: str, tab=False, bg=False, window=False):
|
||||
@ -620,30 +659,44 @@ class CommandDispatcher:
|
||||
"representation.")
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
def yank(self, title=False, sel=False, domain=False, pretty=False):
|
||||
"""Yank the current URL/title to the clipboard or primary selection.
|
||||
@cmdutils.argument('what', choices=['selection', 'url', 'pretty-url',
|
||||
'title', 'domain'])
|
||||
def yank(self, what='url', sel=False, keep=False):
|
||||
"""Yank something to the clipboard or primary selection.
|
||||
|
||||
Args:
|
||||
what: What to yank.
|
||||
|
||||
- `url`: The current URL.
|
||||
- `pretty-url`: The URL in pretty decoded form.
|
||||
- `title`: The current page's title.
|
||||
- `domain`: The current scheme, domain, and port number.
|
||||
- `selection`: The selection under the cursor.
|
||||
|
||||
sel: Use the primary selection instead of the clipboard.
|
||||
title: Yank the title instead of the URL.
|
||||
domain: Yank only the scheme, domain, and port number.
|
||||
pretty: Yank the URL in pretty decoded form.
|
||||
keep: Stay in visual mode after yanking the selection.
|
||||
"""
|
||||
if title:
|
||||
if what == 'title':
|
||||
s = self._tabbed_browser.page_title(self._current_index())
|
||||
what = 'title'
|
||||
elif domain:
|
||||
elif what == 'domain':
|
||||
port = self._current_url().port()
|
||||
s = '{}://{}{}'.format(self._current_url().scheme(),
|
||||
self._current_url().host(),
|
||||
':' + str(port) if port > -1 else '')
|
||||
what = 'domain'
|
||||
else:
|
||||
elif what in ['url', 'pretty-url']:
|
||||
flags = QUrl.RemovePassword
|
||||
if not pretty:
|
||||
if what != 'pretty-url':
|
||||
flags |= QUrl.FullyEncoded
|
||||
s = self._current_url().toString(flags)
|
||||
what = 'URL'
|
||||
what = 'URL' # For printing
|
||||
elif what == 'selection':
|
||||
caret = self._current_widget().caret
|
||||
s = caret.selection()
|
||||
if not caret.has_selection() or not s:
|
||||
message.info(self._win_id, "Nothing to yank")
|
||||
return
|
||||
else: # pragma: no cover
|
||||
raise ValueError("Invalid value {!r} for `what'.".format(what))
|
||||
|
||||
if sel and utils.supports_selection():
|
||||
target = "primary selection"
|
||||
@ -652,8 +705,15 @@ class CommandDispatcher:
|
||||
target = "clipboard"
|
||||
|
||||
utils.set_clipboard(s, selection=sel)
|
||||
message.info(self._win_id, "Yanked {} to {}: {}".format(
|
||||
what, target, s))
|
||||
if what != 'selection':
|
||||
message.info(self._win_id, "Yanked {} to {}: {}".format(
|
||||
what, target, s))
|
||||
else:
|
||||
message.info(self._win_id, "{} {} yanked to {}".format(
|
||||
len(s), "char" if len(s) == 1 else "chars", target))
|
||||
if not keep:
|
||||
modeman.maybe_leave(self._win_id, KeyMode.caret,
|
||||
"yank selected")
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
@cmdutils.argument('count', count=True)
|
||||
@ -776,7 +836,8 @@ class CommandDispatcher:
|
||||
else:
|
||||
raise cmdexc.CommandError("Last tab")
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||
deprecated="Use :open {clipboard}")
|
||||
def paste(self, sel=False, tab=False, bg=False, window=False):
|
||||
"""Open a page from the clipboard.
|
||||
|
||||
@ -790,15 +851,12 @@ class CommandDispatcher:
|
||||
window: Open in new window.
|
||||
"""
|
||||
force_search = False
|
||||
if sel and utils.supports_selection():
|
||||
target = "Primary selection"
|
||||
else:
|
||||
if not utils.supports_selection():
|
||||
sel = False
|
||||
target = "Clipboard"
|
||||
text = utils.get_clipboard(selection=sel)
|
||||
if not text.strip():
|
||||
raise cmdexc.CommandError("{} is empty.".format(target))
|
||||
log.misc.debug("{} contained: {!r}".format(target, text))
|
||||
try:
|
||||
text = utils.get_clipboard(selection=sel)
|
||||
except utils.ClipboardError as e:
|
||||
raise cmdexc.CommandError(e)
|
||||
text_urls = [u for u in text.split('\n') if u.strip()]
|
||||
if (len(text_urls) > 1 and not urlutils.is_url(text_urls[0]) and
|
||||
urlutils.get_path_if_valid(
|
||||
@ -959,7 +1017,7 @@ class CommandDispatcher:
|
||||
self._tabbed_browser.setUpdatesEnabled(True)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||
maxsplit=0)
|
||||
maxsplit=0, no_replace_variables=True)
|
||||
def spawn(self, cmdline, userscript=False, verbose=False, detach=False):
|
||||
"""Spawn a command in a shell.
|
||||
|
||||
@ -1165,14 +1223,13 @@ class CommandDispatcher:
|
||||
current page's url.
|
||||
"""
|
||||
if url is None:
|
||||
url = self._current_url().toString(QUrl.RemovePassword
|
||||
| QUrl.FullyEncoded)
|
||||
url = self._current_url().toString(QUrl.RemovePassword |
|
||||
QUrl.FullyEncoded)
|
||||
try:
|
||||
objreg.get('bookmark-manager').delete(url)
|
||||
except KeyError:
|
||||
raise cmdexc.CommandError("Bookmark '{}' not found!".format(url))
|
||||
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||
scope='window')
|
||||
def follow_selected(self, *, tab=False):
|
||||
@ -1388,29 +1445,31 @@ class CommandDispatcher:
|
||||
url = QUrl('qute://log?level={}'.format(level))
|
||||
self._open(url, tab, bg, window)
|
||||
|
||||
def _open_editor_cb(self, elem):
|
||||
"""Open editor after the focus elem was found in open_editor."""
|
||||
if elem is None:
|
||||
message.error(self._win_id, "No element focused!")
|
||||
return
|
||||
if not elem.is_editable(strict=True):
|
||||
message.error(self._win_id, "Focused element is not editable!")
|
||||
return
|
||||
|
||||
text = elem.text(use_js=True)
|
||||
ed = editor.ExternalEditor(self._win_id, self._tabbed_browser)
|
||||
ed.editing_finished.connect(functools.partial(
|
||||
self.on_editing_finished, elem))
|
||||
ed.edit(text)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher',
|
||||
modes=[KeyMode.insert], hide=True, scope='window',
|
||||
backend=usertypes.Backend.QtWebKit)
|
||||
modes=[KeyMode.insert], hide=True, scope='window')
|
||||
def open_editor(self):
|
||||
"""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.
|
||||
"""
|
||||
# FIXME:qtwebengine have a proper API for this
|
||||
tab = self._current_widget()
|
||||
page = tab._widget.page() # pylint: disable=protected-access
|
||||
try:
|
||||
elem = webelem.focus_elem(page.currentFrame())
|
||||
except webelem.IsNullError:
|
||||
raise cmdexc.CommandError("No element focused!")
|
||||
if not elem.is_editable(strict=True):
|
||||
raise cmdexc.CommandError("Focused element is not editable!")
|
||||
text = elem.text(use_js=True)
|
||||
ed = editor.ExternalEditor(self._win_id, self._tabbed_browser)
|
||||
ed.editing_finished.connect(functools.partial(
|
||||
self.on_editing_finished, elem))
|
||||
ed.edit(text)
|
||||
tab.find_focus_element(self._open_editor_cb)
|
||||
|
||||
def on_editing_finished(self, elem, text):
|
||||
"""Write the editor text into the form field and clean up tempfile.
|
||||
@ -1423,7 +1482,7 @@ class CommandDispatcher:
|
||||
"""
|
||||
try:
|
||||
elem.set_text(text, use_js=True)
|
||||
except webelem.IsNullError:
|
||||
except webkitelem.IsNullError:
|
||||
raise cmdexc.CommandError("Element vanished while editing!")
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher',
|
||||
@ -1448,8 +1507,8 @@ class CommandDispatcher:
|
||||
tab = self._current_widget()
|
||||
page = tab._widget.page() # pylint: disable=protected-access
|
||||
try:
|
||||
elem = webelem.focus_elem(page.currentFrame())
|
||||
except webelem.IsNullError:
|
||||
elem = webkitelem.focus_elem(page.currentFrame())
|
||||
except webkitelem.IsNullError:
|
||||
raise cmdexc.CommandError("No element focused!")
|
||||
if not elem.is_editable(strict=True):
|
||||
raise cmdexc.CommandError("Focused element is not editable!")
|
||||
@ -1736,31 +1795,6 @@ class CommandDispatcher:
|
||||
"""Move the cursor or selection to the end of the document."""
|
||||
self._current_widget().caret.move_to_end_of_document()
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
def yank_selected(self, sel=False, keep=False):
|
||||
"""Yank the selected text to the clipboard or primary selection.
|
||||
|
||||
Args:
|
||||
sel: Use the primary selection instead of the clipboard.
|
||||
keep: If given, stay in visual mode after yanking.
|
||||
"""
|
||||
caret = self._current_widget().caret
|
||||
s = caret.selection()
|
||||
if not caret.has_selection() or len(s) == 0:
|
||||
message.info(self._win_id, "Nothing to yank")
|
||||
return
|
||||
|
||||
if sel and utils.supports_selection():
|
||||
target = "primary selection"
|
||||
else:
|
||||
sel = False
|
||||
target = "clipboard"
|
||||
utils.set_clipboard(s, sel)
|
||||
message.info(self._win_id, "{} {} yanked to {}".format(
|
||||
len(s), "char" if len(s) == 1 else "chars", target))
|
||||
if not keep:
|
||||
modeman.maybe_leave(self._win_id, KeyMode.caret, "yank selected")
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||
modes=[KeyMode.caret], scope='window')
|
||||
def toggle_selection(self):
|
||||
|
@ -28,12 +28,11 @@ from string import ascii_lowercase
|
||||
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl,
|
||||
QTimer)
|
||||
from PyQt5.QtGui import QMouseEvent
|
||||
from PyQt5.QtWebKit import QWebElement
|
||||
from PyQt5.QtWebKitWidgets import QWebPage
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.keyinput import modeman, modeparsers
|
||||
from qutebrowser.browser.webkit import webelem
|
||||
from qutebrowser.browser import webelem
|
||||
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
|
||||
from qutebrowser.utils import usertypes, log, qtutils, message, objreg, utils
|
||||
|
||||
@ -95,6 +94,7 @@ class HintContext:
|
||||
self.args = []
|
||||
self.tab = None
|
||||
self.group = None
|
||||
self.hint_mode = None
|
||||
|
||||
def get_args(self, urlstr):
|
||||
"""Get the arguments, with {hint-url} replaced by the given URL."""
|
||||
@ -374,7 +374,7 @@ class HintManager(QObject):
|
||||
for elem in self._context.all_elems:
|
||||
try:
|
||||
elem.label.remove_from_document()
|
||||
except webelem.IsNullError:
|
||||
except webelem.Error:
|
||||
pass
|
||||
text = self._get_text()
|
||||
message_bridge = objreg.get('message-bridge', scope='window',
|
||||
@ -394,7 +394,7 @@ class HintManager(QObject):
|
||||
Return:
|
||||
A list of hint strings, in the same order as the elements.
|
||||
"""
|
||||
hint_mode = config.get('hints', 'mode')
|
||||
hint_mode = self._context.hint_mode
|
||||
if hint_mode == 'word':
|
||||
try:
|
||||
return self._word_hinter.hint(elems)
|
||||
@ -516,7 +516,7 @@ class HintManager(QObject):
|
||||
|
||||
def _is_hidden(self, elem):
|
||||
"""Check if the element is hidden via display=none."""
|
||||
display = elem.style_property('display', QWebElement.InlineStyle)
|
||||
display = elem.style_property('display', strategy='inline')
|
||||
return display == 'none'
|
||||
|
||||
def _show_elem(self, elem):
|
||||
@ -548,7 +548,7 @@ class HintManager(QObject):
|
||||
|
||||
# Make text uppercase if set in config
|
||||
if (config.get('hints', 'uppercase') and
|
||||
config.get('hints', 'mode') == 'letter'):
|
||||
self._context.hint_mode == 'letter'):
|
||||
attrs.append(('text-transform', 'uppercase !important'))
|
||||
else:
|
||||
attrs.append(('text-transform', 'none !important'))
|
||||
@ -661,14 +661,14 @@ class HintManager(QObject):
|
||||
backend=usertypes.Backend.QtWebKit)
|
||||
@cmdutils.argument('win_id', win_id=True)
|
||||
def start(self, rapid=False, group=webelem.Group.all, target=Target.normal,
|
||||
*args, win_id):
|
||||
*args, win_id, mode=None):
|
||||
"""Start hinting.
|
||||
|
||||
Args:
|
||||
rapid: Whether to do rapid hinting. This is only possible with
|
||||
targets `tab` (with background-tabs=true), `tab-bg`,
|
||||
`window`, `run`, `hover`, `userscript` and `spawn`.
|
||||
group: The hinting mode to use.
|
||||
group: The element types to hint.
|
||||
|
||||
- `all`: All clickable elements.
|
||||
- `links`: Only links.
|
||||
@ -695,6 +695,13 @@ class HintManager(QObject):
|
||||
link.
|
||||
- `spawn`: Spawn a command.
|
||||
|
||||
mode: The hinting mode to use.
|
||||
|
||||
- `number`: Use numeric hints.
|
||||
- `letter`: Use the chars in the hints->chars settings.
|
||||
- `word`: Use hint words based on the html elements and the
|
||||
extra words.
|
||||
|
||||
*args: Arguments for spawn/userscript/run/fill.
|
||||
|
||||
- With `spawn`: The executable and arguments to spawn.
|
||||
@ -733,11 +740,15 @@ class HintManager(QObject):
|
||||
raise cmdexc.CommandError("Rapid hinting makes no sense with "
|
||||
"target {}!".format(name))
|
||||
|
||||
if mode is None:
|
||||
mode = config.get('hints', 'mode')
|
||||
|
||||
self._check_args(target, *args)
|
||||
self._context = HintContext()
|
||||
self._context.tab = tab
|
||||
self._context.target = target
|
||||
self._context.rapid = rapid
|
||||
self._context.hint_mode = mode
|
||||
try:
|
||||
self._context.baseurl = tabbed_browser.current_url()
|
||||
except qtutils.QtValueError:
|
||||
@ -749,6 +760,13 @@ class HintManager(QObject):
|
||||
self._context.tab.find_all_elements(selector, self._start_cb,
|
||||
only_visible=True)
|
||||
|
||||
def current_mode(self):
|
||||
"""Return the currently active hinting mode (or None otherwise)."""
|
||||
if self._context is None:
|
||||
return None
|
||||
|
||||
return self._context.hint_mode
|
||||
|
||||
def handle_partial_key(self, keystr):
|
||||
"""Handle a new partial keypress."""
|
||||
log.hints.debug("Handling new keystring: '{}'".format(keystr))
|
||||
@ -765,9 +783,12 @@ class HintManager(QObject):
|
||||
# hidden element which matches again -> show it
|
||||
self._show_elem(elem.label)
|
||||
else:
|
||||
# element doesn't match anymore -> hide it
|
||||
self._hide_elem(elem.label)
|
||||
except webelem.IsNullError:
|
||||
# element doesn't match anymore -> hide it, unless in rapid
|
||||
# mode and hide-unmatched-rapid-hints is false (see #1799)
|
||||
if (not self._context.rapid or
|
||||
config.get('hints', 'hide-unmatched-rapid-hints')):
|
||||
self._hide_elem(elem.label)
|
||||
except webelem.Error:
|
||||
pass
|
||||
|
||||
def _filter_number_hints(self):
|
||||
@ -782,7 +803,7 @@ class HintManager(QObject):
|
||||
try:
|
||||
if not self._is_hidden(e.label):
|
||||
elems.append(e)
|
||||
except webelem.IsNullError:
|
||||
except webelem.Error:
|
||||
pass
|
||||
if not elems:
|
||||
# Whoops, filtered all hints
|
||||
@ -813,7 +834,7 @@ class HintManager(QObject):
|
||||
try:
|
||||
if not self._is_hidden(elem.label):
|
||||
visible[string] = elem
|
||||
except webelem.IsNullError:
|
||||
except webelem.Error:
|
||||
pass
|
||||
if not visible:
|
||||
# Whoops, filtered all hints
|
||||
@ -844,10 +865,10 @@ class HintManager(QObject):
|
||||
else:
|
||||
# element doesn't match anymore -> hide it
|
||||
self._hide_elem(elem.label)
|
||||
except webelem.IsNullError:
|
||||
except webelem.Error:
|
||||
pass
|
||||
|
||||
if config.get('hints', 'mode') == 'number':
|
||||
if self._context.hint_mode == 'number':
|
||||
visible = self._filter_number_hints()
|
||||
else:
|
||||
visible = self._filter_non_number_hints()
|
||||
@ -961,7 +982,7 @@ class HintManager(QObject):
|
||||
e.label.remove_from_document()
|
||||
continue
|
||||
self._set_style_position(e.elem, e.label)
|
||||
except webelem.IsNullError:
|
||||
except webelem.Error:
|
||||
pass
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
@ -1022,15 +1043,18 @@ class WordHinter:
|
||||
"alt": lambda elem: elem["alt"],
|
||||
"name": lambda elem: elem["name"],
|
||||
"title": lambda elem: elem["title"],
|
||||
"placeholder": lambda elem: elem["placeholder"],
|
||||
"src": lambda elem: elem["src"].split('/')[-1],
|
||||
"href": lambda elem: elem["href"].split('/')[-1],
|
||||
"text": str,
|
||||
}
|
||||
|
||||
extractable_attrs = collections.defaultdict(list, {
|
||||
"IMG": ["alt", "title", "src"],
|
||||
"A": ["title", "href", "text"],
|
||||
"INPUT": ["name"]
|
||||
"img": ["alt", "title", "src"],
|
||||
"a": ["title", "href", "text"],
|
||||
"input": ["name", "placeholder"],
|
||||
"textarea": ["name", "placeholder"],
|
||||
"button": ["text"]
|
||||
})
|
||||
|
||||
return (attr_extractors[attr](elem)
|
||||
|
@ -22,11 +22,11 @@
|
||||
import time
|
||||
import collections
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, QUrl, QObject
|
||||
from PyQt5.QtWebKit import QWebHistoryInterface
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, QObject
|
||||
|
||||
from qutebrowser.commands import cmdutils
|
||||
from qutebrowser.utils import utils, objreg, standarddir, log, qtutils
|
||||
from qutebrowser.utils import (utils, objreg, standarddir, log, qtutils,
|
||||
usertypes)
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.misc import lineparser
|
||||
|
||||
@ -108,34 +108,6 @@ class Entry:
|
||||
return cls(atime, url, title, redirect=redirect)
|
||||
|
||||
|
||||
class WebHistoryInterface(QWebHistoryInterface):
|
||||
|
||||
"""Glue code between WebHistory and Qt's QWebHistoryInterface.
|
||||
|
||||
Attributes:
|
||||
_history: The WebHistory object.
|
||||
"""
|
||||
|
||||
def __init__(self, webhistory, parent=None):
|
||||
super().__init__(parent)
|
||||
self._history = webhistory
|
||||
|
||||
def addHistoryEntry(self, url_string):
|
||||
"""Required for a QWebHistoryInterface impl, obsoleted by add_url."""
|
||||
pass
|
||||
|
||||
def historyContains(self, url_string):
|
||||
"""Called by WebKit to determine if a URL is contained in the history.
|
||||
|
||||
Args:
|
||||
url_string: The URL (as string) to check for.
|
||||
|
||||
Return:
|
||||
True if the url is in the history, False otherwise.
|
||||
"""
|
||||
return url_string in self._history.history_dict
|
||||
|
||||
|
||||
class WebHistory(QObject):
|
||||
|
||||
"""The global history of visited pages.
|
||||
@ -284,8 +256,19 @@ class WebHistory(QObject):
|
||||
self._saved_count = 0
|
||||
self.cleared.emit()
|
||||
|
||||
@pyqtSlot(QUrl, QUrl, str)
|
||||
def add_from_tab(self, url, requested_url, title):
|
||||
"""Add a new history entry as slot, called from a BrowserTab."""
|
||||
no_formatting = QUrl.UrlFormattingOption(0)
|
||||
if (requested_url.isValid() and
|
||||
not requested_url.matches(url, no_formatting)):
|
||||
# If the url of the page is different than the url of the link
|
||||
# originally clicked, save them both.
|
||||
self.add_url(requested_url, title, redirect=True)
|
||||
self.add_url(url, title)
|
||||
|
||||
def add_url(self, url, title="", *, redirect=False, atime=None):
|
||||
"""Called by WebKit when a URL should be added to the history.
|
||||
"""Called via add_from_tab when a URL should be added to the history.
|
||||
|
||||
Args:
|
||||
url: A url (as QUrl) to add to the history.
|
||||
@ -322,5 +305,7 @@ def init(parent=None):
|
||||
parent=parent)
|
||||
objreg.register('web-history', history)
|
||||
|
||||
interface = WebHistoryInterface(history, parent=history)
|
||||
QWebHistoryInterface.setDefaultInterface(interface)
|
||||
used_backend = usertypes.arg2backend[objreg.get('args').backend]
|
||||
if used_backend == usertypes.Backend.QtWebKit:
|
||||
from qutebrowser.browser.webkit import webkithistory
|
||||
webkithistory.init(history)
|
@ -22,7 +22,7 @@
|
||||
import base64
|
||||
import binascii
|
||||
|
||||
from PyQt5.QtWebKitWidgets import QWebInspector
|
||||
from PyQt5.QtWidgets import QWidget
|
||||
|
||||
from qutebrowser.utils import log, objreg
|
||||
from qutebrowser.misc import miscwidgets
|
||||
@ -52,7 +52,7 @@ class WebInspectorError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AbstractWebInspector(QWebInspector):
|
||||
class AbstractWebInspector(QWidget):
|
||||
|
||||
"""A customized WebInspector which stores its geometry."""
|
||||
|
||||
|
104
qutebrowser/browser/mouse.py
Normal file
104
qutebrowser/browser/mouse.py
Normal file
@ -0,0 +1,104 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2016 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/>.
|
||||
|
||||
"""Mouse handling for a browser tab."""
|
||||
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import message, log
|
||||
|
||||
|
||||
from PyQt5.QtCore import QObject, QEvent, Qt
|
||||
|
||||
|
||||
class ChildEventFilter(QObject):
|
||||
|
||||
"""An event filter re-adding MouseEventFilter on ChildEvent.
|
||||
|
||||
This is needed because QtWebEngine likes to randomly change its
|
||||
focusProxy...
|
||||
|
||||
FIXME:qtwebengine Add a test for this happening
|
||||
"""
|
||||
|
||||
def __init__(self, eventfilter, widget, parent=None):
|
||||
super().__init__(parent)
|
||||
self._filter = eventfilter
|
||||
assert widget is not None
|
||||
self._widget = widget
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
"""Act on ChildAdded events."""
|
||||
if event.type() == QEvent.ChildAdded:
|
||||
child = event.child()
|
||||
log.mouse.debug("{} got new child {}, installing filter".format(
|
||||
obj, child))
|
||||
assert obj is self._widget
|
||||
child.installEventFilter(self._filter)
|
||||
return False
|
||||
|
||||
|
||||
class MouseEventFilter(QObject):
|
||||
|
||||
"""Handle mouse events on a tab."""
|
||||
|
||||
def __init__(self, tab, parent=None):
|
||||
super().__init__(parent)
|
||||
self._tab = tab
|
||||
self._handlers = {
|
||||
QEvent.MouseButtonPress: self._handle_mouse_press,
|
||||
}
|
||||
|
||||
def _handle_mouse_press(self, _obj, e):
|
||||
"""Handle pressing of a mouse button."""
|
||||
is_rocker_gesture = (config.get('input', 'rocker-gestures') and
|
||||
e.buttons() == Qt.LeftButton | Qt.RightButton)
|
||||
|
||||
if e.button() in [Qt.XButton1, Qt.XButton2] or is_rocker_gesture:
|
||||
self._mousepress_backforward(e)
|
||||
return True
|
||||
return False
|
||||
|
||||
def _mousepress_backforward(self, e):
|
||||
"""Handle back/forward mouse button presses.
|
||||
|
||||
Args:
|
||||
e: The QMouseEvent.
|
||||
"""
|
||||
if e.button() in [Qt.XButton1, Qt.LeftButton]:
|
||||
# Back button on mice which have it, or rocker gesture
|
||||
if self._tab.history.can_go_back():
|
||||
self._tab.history.back()
|
||||
else:
|
||||
message.error(self._tab.win_id, "At beginning of history.",
|
||||
immediately=True)
|
||||
elif e.button() in [Qt.XButton2, Qt.RightButton]:
|
||||
# Forward button on mice which have it, or rocker gesture
|
||||
if self._tab.history.can_go_forward():
|
||||
self._tab.history.forward()
|
||||
else:
|
||||
message.error(self._tab.win_id, "At end of history.",
|
||||
immediately=True)
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
"""Filter events going to a QWeb(Engine)View."""
|
||||
evtype = event.type()
|
||||
if evtype not in self._handlers:
|
||||
return False
|
||||
return self._handlers[evtype](obj, event)
|
@ -17,14 +17,13 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Implementation of :navigate"""
|
||||
"""Implementation of :navigate."""
|
||||
|
||||
import posixpath
|
||||
|
||||
from qutebrowser.browser.webkit import webelem
|
||||
from qutebrowser.browser import webelem
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import (usertypes, objreg, urlutils, log, message,
|
||||
qtutils)
|
||||
from qutebrowser.utils import objreg, urlutils, log, message, qtutils
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
@ -109,11 +108,6 @@ def prevnext(*, browsertab, win_id, baseurl, prev=False,
|
||||
background: True to open in a background tab.
|
||||
window: True to open in a new window, False for the current one.
|
||||
"""
|
||||
# FIXME:qtwebengine have a proper API for this
|
||||
if browsertab.backend == usertypes.Backend.QtWebEngine:
|
||||
raise Error(":navigate prev/next is not supported yet with "
|
||||
"QtWebEngine")
|
||||
|
||||
def _prevnext_cb(elems):
|
||||
elem = _find_prevnext(prev, elems)
|
||||
word = 'prev' if prev else 'forward'
|
||||
|
369
qutebrowser/browser/webelem.py
Normal file
369
qutebrowser/browser/webelem.py
Normal file
@ -0,0 +1,369 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014-2016 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/>.
|
||||
|
||||
"""Generic web element related code.
|
||||
|
||||
Module attributes:
|
||||
Group: Enum for different kinds of groups.
|
||||
SELECTORS: CSS selectors for different groups of elements.
|
||||
FILTERS: A dictionary of filter functions for the modes.
|
||||
The filter for "links" filters javascript:-links and a-tags
|
||||
without "href".
|
||||
"""
|
||||
|
||||
import collections.abc
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import log, usertypes, utils, qtutils
|
||||
|
||||
|
||||
Group = usertypes.enum('Group', ['all', 'links', 'images', 'url', 'prevnext',
|
||||
'inputs'])
|
||||
|
||||
|
||||
SELECTORS = {
|
||||
Group.all: ('a, area, textarea, select, input:not([type=hidden]), button, '
|
||||
'frame, iframe, link, [onclick], [onmousedown], [role=link], '
|
||||
'[role=option], [role=button], img'),
|
||||
Group.links: 'a, area, link, [role=link]',
|
||||
Group.images: 'img',
|
||||
Group.url: '[src], [href]',
|
||||
Group.prevnext: 'a, area, button, link, [role=button]',
|
||||
Group.inputs: ('input[type=text], input[type=email], input[type=url], '
|
||||
'input[type=tel], input[type=number], '
|
||||
'input[type=password], input[type=search], '
|
||||
'input:not([type]), textarea'),
|
||||
}
|
||||
|
||||
|
||||
def filter_links(elem):
|
||||
return 'href' in elem and QUrl(elem['href']).scheme() != 'javascript'
|
||||
|
||||
|
||||
FILTERS = {
|
||||
Group.links: filter_links,
|
||||
Group.prevnext: filter_links,
|
||||
}
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
|
||||
"""Base class for WebElement errors."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AbstractWebElement(collections.abc.MutableMapping):
|
||||
|
||||
"""A wrapper around QtWebKit/QtWebEngine web element."""
|
||||
|
||||
def __eq__(self, other):
|
||||
raise NotImplementedError
|
||||
|
||||
def __str__(self):
|
||||
return self.text()
|
||||
|
||||
def __getitem__(self, key):
|
||||
raise NotImplementedError
|
||||
|
||||
def __setitem__(self, key, val):
|
||||
raise NotImplementedError
|
||||
|
||||
def __delitem__(self, key):
|
||||
raise NotImplementedError
|
||||
|
||||
def __iter__(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def __len__(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def __repr__(self):
|
||||
try:
|
||||
html = self.debug_text()
|
||||
except Error:
|
||||
html = None
|
||||
return utils.get_repr(self, html=html)
|
||||
|
||||
def frame(self):
|
||||
"""Get the main frame of this element."""
|
||||
# FIXME:qtwebengine get rid of this?
|
||||
raise NotImplementedError
|
||||
|
||||
def geometry(self):
|
||||
"""Get the geometry for this element."""
|
||||
raise NotImplementedError
|
||||
|
||||
def document_element(self):
|
||||
"""Get the document element of this element."""
|
||||
# FIXME:qtwebengine get rid of this?
|
||||
raise NotImplementedError
|
||||
|
||||
def create_inside(self, tagname):
|
||||
"""Append the given element inside the current one."""
|
||||
# FIXME:qtwebengine get rid of this?
|
||||
raise NotImplementedError
|
||||
|
||||
def find_first(self, selector):
|
||||
"""Find the first child based on the given CSS selector."""
|
||||
# FIXME:qtwebengine get rid of this?
|
||||
raise NotImplementedError
|
||||
|
||||
def style_property(self, name, *, strategy):
|
||||
"""Get the element style resolved with the given strategy."""
|
||||
raise NotImplementedError
|
||||
|
||||
def classes(self):
|
||||
"""Get a list of classes assigned to this element."""
|
||||
raise NotImplementedError
|
||||
|
||||
def tag_name(self):
|
||||
"""Get the tag name of this element.
|
||||
|
||||
The returned name will always be lower-case.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def outer_xml(self):
|
||||
"""Get the full HTML representation of this element."""
|
||||
raise NotImplementedError
|
||||
|
||||
def text(self, *, use_js=False):
|
||||
"""Get the plain text content for this element.
|
||||
|
||||
Args:
|
||||
use_js: Whether to use javascript if the element isn't
|
||||
content-editable.
|
||||
"""
|
||||
# FIXME:qtwebengine what to do about use_js with WebEngine?
|
||||
raise NotImplementedError
|
||||
|
||||
def set_text(self, text, *, use_js=False):
|
||||
"""Set the given plain text.
|
||||
|
||||
Args:
|
||||
use_js: Whether to use javascript if the element isn't
|
||||
content-editable.
|
||||
"""
|
||||
# FIXME:qtwebengine what to do about use_js with WebEngine?
|
||||
raise NotImplementedError
|
||||
|
||||
def set_inner_xml(self, xml):
|
||||
"""Set the given inner XML."""
|
||||
# FIXME:qtwebengine get rid of this?
|
||||
raise NotImplementedError
|
||||
|
||||
def remove_from_document(self):
|
||||
"""Remove the node from the document."""
|
||||
# FIXME:qtwebengine get rid of this?
|
||||
raise NotImplementedError
|
||||
|
||||
def set_style_property(self, name, value):
|
||||
"""Set the element style."""
|
||||
# FIXME:qtwebengine get rid of this?
|
||||
raise NotImplementedError
|
||||
|
||||
def run_js_async(self, code, callback=None):
|
||||
"""Run the given JS snippet async on the element."""
|
||||
# FIXME:qtwebengine get rid of this?
|
||||
raise NotImplementedError
|
||||
|
||||
def parent(self):
|
||||
"""Get the parent element of this element."""
|
||||
# FIXME:qtwebengine get rid of this?
|
||||
raise NotImplementedError
|
||||
|
||||
def rect_on_view(self, *, elem_geometry=None, adjust_zoom=True,
|
||||
no_js=False):
|
||||
"""Get the geometry of the element relative to the webview.
|
||||
|
||||
Uses the getClientRects() JavaScript method to obtain the collection of
|
||||
rectangles containing the element and returns the first rectangle which
|
||||
is large enough (larger than 1px times 1px). If all rectangles returned
|
||||
by getClientRects() are too small, falls back to elem.rect_on_view().
|
||||
|
||||
Skipping of small rectangles is due to <a> elements containing other
|
||||
elements with "display:block" style, see
|
||||
https://github.com/The-Compiler/qutebrowser/issues/1298
|
||||
|
||||
Args:
|
||||
elem_geometry: The geometry of the element, or None.
|
||||
Calling QWebElement::geometry is rather expensive so
|
||||
we want to avoid doing it twice.
|
||||
adjust_zoom: Whether to adjust the element position based on the
|
||||
current zoom level.
|
||||
no_js: Fall back to the Python implementation
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def is_visible(self, mainframe):
|
||||
"""Check if the given element is visible in the given frame."""
|
||||
# FIXME:qtwebengine get rid of this?
|
||||
raise NotImplementedError
|
||||
|
||||
def is_writable(self):
|
||||
"""Check whether an element is writable."""
|
||||
return not ('disabled' in self or 'readonly' in self)
|
||||
|
||||
def is_content_editable(self):
|
||||
"""Check if an element has a contenteditable attribute.
|
||||
|
||||
Args:
|
||||
elem: The QWebElement to check.
|
||||
|
||||
Return:
|
||||
True if the element has a contenteditable attribute,
|
||||
False otherwise.
|
||||
"""
|
||||
try:
|
||||
return self['contenteditable'].lower() not in ['false', 'inherit']
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
def _is_editable_object(self):
|
||||
"""Check if an object-element is editable."""
|
||||
if 'type' not in self:
|
||||
log.webview.debug("<object> without type clicked...")
|
||||
return False
|
||||
objtype = self['type'].lower()
|
||||
if objtype.startswith('application/') or 'classid' in self:
|
||||
# Let's hope flash/java stuff has an application/* mimetype OR
|
||||
# at least a classid attribute. Oh, and let's hope images/...
|
||||
# DON'T have a classid attribute. HTML sucks.
|
||||
log.webview.debug("<object type='{}'> clicked.".format(objtype))
|
||||
return config.get('input', 'insert-mode-on-plugins')
|
||||
else:
|
||||
# Image/Audio/...
|
||||
return False
|
||||
|
||||
def _is_editable_input(self):
|
||||
"""Check if an input-element is editable.
|
||||
|
||||
Return:
|
||||
True if the element is editable, False otherwise.
|
||||
"""
|
||||
try:
|
||||
objtype = self['type'].lower()
|
||||
except KeyError:
|
||||
return self.is_writable()
|
||||
else:
|
||||
if objtype in ['text', 'email', 'url', 'tel', 'number', 'password',
|
||||
'search']:
|
||||
return self.is_writable()
|
||||
else:
|
||||
return False
|
||||
|
||||
def _is_editable_div(self):
|
||||
"""Check if a div-element is editable.
|
||||
|
||||
Return:
|
||||
True if the element is editable, False otherwise.
|
||||
"""
|
||||
# Beginnings of div-classes which are actually some kind of editor.
|
||||
div_classes = ('CodeMirror', # Javascript editor over a textarea
|
||||
'kix-', # Google Docs editor
|
||||
'ace_') # http://ace.c9.io/
|
||||
for klass in self.classes():
|
||||
if any([klass.startswith(e) for e in div_classes]):
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_editable(self, strict=False):
|
||||
"""Check whether we should switch to insert mode for this element.
|
||||
|
||||
Args:
|
||||
strict: Whether to do stricter checking so only fields where we can
|
||||
get the value match, for use with the :editor command.
|
||||
|
||||
Return:
|
||||
True if we should switch to insert mode, False otherwise.
|
||||
"""
|
||||
roles = ('combobox', 'textbox')
|
||||
log.misc.debug("Checking if element is editable: {}".format(
|
||||
repr(self)))
|
||||
tag = self.tag_name()
|
||||
if self.is_content_editable() and self.is_writable():
|
||||
return True
|
||||
elif self.get('role', None) in roles and self.is_writable():
|
||||
return True
|
||||
elif tag == 'input':
|
||||
return self._is_editable_input()
|
||||
elif tag == 'textarea':
|
||||
return self.is_writable()
|
||||
elif tag in ['embed', 'applet']:
|
||||
# Flash/Java/...
|
||||
return config.get('input', 'insert-mode-on-plugins') and not strict
|
||||
elif tag == 'object':
|
||||
return self._is_editable_object() and not strict
|
||||
elif tag == 'div':
|
||||
return self._is_editable_div() and not strict
|
||||
else:
|
||||
return False
|
||||
|
||||
def is_text_input(self):
|
||||
"""Check if this element is some kind of text box."""
|
||||
roles = ('combobox', 'textbox')
|
||||
tag = self.tag_name()
|
||||
return self.get('role', None) in roles or tag in ['input', 'textarea']
|
||||
|
||||
def remove_blank_target(self):
|
||||
"""Remove target from link."""
|
||||
elem = self
|
||||
for _ in range(5):
|
||||
if elem is None:
|
||||
break
|
||||
tag = elem.tag_name()
|
||||
if tag == 'a' or tag == 'area':
|
||||
if elem.get('target', None) == '_blank':
|
||||
elem['target'] = '_top'
|
||||
break
|
||||
elem = elem.parent()
|
||||
|
||||
def debug_text(self):
|
||||
"""Get a text based on an element suitable for debug output."""
|
||||
return utils.compact_text(self.outer_xml(), 500)
|
||||
|
||||
def resolve_url(self, baseurl):
|
||||
"""Resolve the URL in the element's src/href attribute.
|
||||
|
||||
Args:
|
||||
baseurl: The URL to base relative URLs on as QUrl.
|
||||
|
||||
Return:
|
||||
A QUrl with the absolute URL, or None.
|
||||
"""
|
||||
if baseurl.isRelative():
|
||||
raise ValueError("Need an absolute base URL!")
|
||||
|
||||
for attr in ['href', 'src']:
|
||||
if attr in self:
|
||||
text = self[attr].strip()
|
||||
break
|
||||
else:
|
||||
return None
|
||||
|
||||
url = QUrl(text)
|
||||
if not url.isValid():
|
||||
return None
|
||||
if url.isRelative():
|
||||
url = baseurl.resolved(url)
|
||||
qtutils.ensure_valid(url)
|
||||
return url
|
178
qutebrowser/browser/webengine/webengineelem.py
Normal file
178
qutebrowser/browser/webengine/webengineelem.py
Normal file
@ -0,0 +1,178 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2016 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/>.
|
||||
|
||||
# FIXME:qtwebengine remove this once the stubs are gone
|
||||
# pylint: disable=unused-variable
|
||||
|
||||
"""QtWebEngine specific part of the web element API."""
|
||||
|
||||
from PyQt5.QtCore import QRect
|
||||
|
||||
from qutebrowser.utils import log, javascript
|
||||
from qutebrowser.browser import webelem
|
||||
|
||||
|
||||
class WebEngineElement(webelem.AbstractWebElement):
|
||||
|
||||
"""A web element for QtWebEngine, using JS under the hood."""
|
||||
|
||||
def __init__(self, js_dict, run_js_callable):
|
||||
self._id = js_dict['id']
|
||||
self._js_dict = js_dict
|
||||
self._run_js = run_js_callable
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, WebEngineElement):
|
||||
return NotImplemented
|
||||
return self._id == other._id # pylint: disable=protected-access
|
||||
|
||||
def __getitem__(self, key):
|
||||
attrs = self._js_dict['attributes']
|
||||
return attrs[key]
|
||||
|
||||
def __setitem__(self, key, val):
|
||||
log.stub()
|
||||
|
||||
def __delitem__(self, key):
|
||||
log.stub()
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._js_dict['attributes'])
|
||||
|
||||
def __len__(self):
|
||||
return len(self._js_dict['attributes'])
|
||||
|
||||
def frame(self):
|
||||
log.stub()
|
||||
return None
|
||||
|
||||
def geometry(self):
|
||||
log.stub()
|
||||
return QRect()
|
||||
|
||||
def document_element(self):
|
||||
log.stub()
|
||||
return None
|
||||
|
||||
def create_inside(self, tagname):
|
||||
log.stub()
|
||||
return None
|
||||
|
||||
def find_first(self, selector):
|
||||
log.stub()
|
||||
return None
|
||||
|
||||
def style_property(self, name, *, strategy):
|
||||
log.stub()
|
||||
return ''
|
||||
|
||||
def classes(self):
|
||||
"""Get a list of classes assigned to this element."""
|
||||
log.stub()
|
||||
return []
|
||||
|
||||
def tag_name(self):
|
||||
"""Get the tag name of this element.
|
||||
|
||||
The returned name will always be lower-case.
|
||||
"""
|
||||
return self._js_dict['tag_name'].lower()
|
||||
|
||||
def outer_xml(self):
|
||||
"""Get the full HTML representation of this element."""
|
||||
return self._js_dict['outer_xml']
|
||||
|
||||
def text(self, *, use_js=False):
|
||||
"""Get the plain text content for this element.
|
||||
|
||||
Args:
|
||||
use_js: Whether to use javascript if the element isn't
|
||||
content-editable.
|
||||
"""
|
||||
if use_js:
|
||||
# FIXME:qtwebengine what to do about use_js with WebEngine?
|
||||
log.stub('with use_js=True')
|
||||
return self._js_dict.get('text', '')
|
||||
|
||||
def set_text(self, text, *, use_js=False):
|
||||
"""Set the given plain text.
|
||||
|
||||
Args:
|
||||
use_js: Whether to use javascript if the element isn't
|
||||
content-editable.
|
||||
"""
|
||||
# FIXME:qtwebengine what to do about use_js with WebEngine?
|
||||
js_code = javascript.assemble('webelem', 'set_text', self._id, text)
|
||||
self._run_js(js_code)
|
||||
|
||||
def set_inner_xml(self, xml):
|
||||
"""Set the given inner XML."""
|
||||
# FIXME:qtwebengine get rid of this?
|
||||
log.stub()
|
||||
|
||||
def remove_from_document(self):
|
||||
"""Remove the node from the document."""
|
||||
# FIXME:qtwebengine get rid of this?
|
||||
log.stub()
|
||||
|
||||
def set_style_property(self, name, value):
|
||||
"""Set the element style."""
|
||||
# FIXME:qtwebengine get rid of this?
|
||||
log.stub()
|
||||
|
||||
def run_js_async(self, code, callback=None):
|
||||
"""Run the given JS snippet async on the element."""
|
||||
# FIXME:qtwebengine get rid of this?
|
||||
log.stub()
|
||||
|
||||
def parent(self):
|
||||
"""Get the parent element of this element."""
|
||||
# FIXME:qtwebengine get rid of this?
|
||||
log.stub()
|
||||
return None
|
||||
|
||||
def rect_on_view(self, *, elem_geometry=None, adjust_zoom=True,
|
||||
no_js=False):
|
||||
"""Get the geometry of the element relative to the webview.
|
||||
|
||||
Uses the getClientRects() JavaScript method to obtain the collection of
|
||||
rectangles containing the element and returns the first rectangle which
|
||||
is large enough (larger than 1px times 1px). If all rectangles returned
|
||||
by getClientRects() are too small, falls back to elem.rect_on_view().
|
||||
|
||||
Skipping of small rectangles is due to <a> elements containing other
|
||||
elements with "display:block" style, see
|
||||
https://github.com/The-Compiler/qutebrowser/issues/1298
|
||||
|
||||
Args:
|
||||
elem_geometry: The geometry of the element, or None.
|
||||
Calling QWebElement::geometry is rather expensive so
|
||||
we want to avoid doing it twice.
|
||||
adjust_zoom: Whether to adjust the element position based on the
|
||||
current zoom level.
|
||||
no_js: Fall back to the Python implementation
|
||||
"""
|
||||
log.stub()
|
||||
return QRect()
|
||||
|
||||
def is_visible(self, mainframe):
|
||||
"""Check if the given element is visible in the given frame."""
|
||||
# FIXME:qtwebengine get rid of this?
|
||||
log.stub()
|
||||
return True
|
@ -36,22 +36,30 @@ from qutebrowser.utils import objreg, utils
|
||||
|
||||
class Attribute(websettings.Attribute):
|
||||
|
||||
"""A setting set via QWebEngineSettings::setAttribute."""
|
||||
|
||||
GLOBAL_SETTINGS = QWebEngineSettings.globalSettings
|
||||
ENUM_BASE = QWebEngineSettings
|
||||
|
||||
|
||||
class Setter(websettings.Setter):
|
||||
|
||||
"""A setting set via QWebEngineSettings getter/setter methods."""
|
||||
|
||||
GLOBAL_SETTINGS = QWebEngineSettings.globalSettings
|
||||
|
||||
|
||||
class NullStringSetter(websettings.NullStringSetter):
|
||||
|
||||
"""A setter for settings requiring a null QString as default."""
|
||||
|
||||
GLOBAL_SETTINGS = QWebEngineSettings.globalSettings
|
||||
|
||||
|
||||
class StaticSetter(websettings.StaticSetter):
|
||||
|
||||
"""A setting set via static QWebEngineSettings getter/setter methods."""
|
||||
|
||||
GLOBAL_SETTINGS = QWebEngineSettings.globalSettings
|
||||
|
||||
|
||||
|
@ -22,6 +22,8 @@
|
||||
|
||||
"""Wrapper over a QWebEngineView."""
|
||||
|
||||
import functools
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, Qt, QEvent, QPoint
|
||||
from PyQt5.QtGui import QKeyEvent, QIcon
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
@ -29,9 +31,9 @@ from PyQt5.QtWidgets import QApplication
|
||||
from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript
|
||||
# pylint: enable=no-name-in-module,import-error,useless-suppression
|
||||
|
||||
from qutebrowser.browser import browsertab
|
||||
from qutebrowser.browser.webengine import webview
|
||||
from qutebrowser.utils import usertypes, qtutils, log, javascript
|
||||
from qutebrowser.browser import browsertab, mouse
|
||||
from qutebrowser.browser.webengine import webview, webengineelem
|
||||
from qutebrowser.utils import usertypes, qtutils, log, javascript, utils
|
||||
|
||||
|
||||
class WebEnginePrinting(browsertab.AbstractPrinting):
|
||||
@ -95,7 +97,7 @@ class WebEngineSearch(browsertab.AbstractSearch):
|
||||
flags &= ~QWebEnginePage.FindBackward
|
||||
else:
|
||||
flags |= QWebEnginePage.FindBackward
|
||||
self._find(self.text, self._flags, result_cb)
|
||||
self._find(self.text, flags, result_cb)
|
||||
|
||||
def next_result(self, *, result_cb=None):
|
||||
self._find(self.text, self._flags, result_cb)
|
||||
@ -182,18 +184,17 @@ class WebEngineScroller(browsertab.AbstractScroller):
|
||||
|
||||
def __init__(self, tab, parent=None):
|
||||
super().__init__(tab, parent)
|
||||
self._pos_perc = (None, None)
|
||||
self._pos_perc = (0, 0)
|
||||
self._pos_px = QPoint()
|
||||
|
||||
def _init_widget(self, widget):
|
||||
super()._init_widget(widget)
|
||||
page = widget.page()
|
||||
try:
|
||||
page.scrollPositionChanged.connect(
|
||||
self._on_scroll_pos_changed)
|
||||
page.scrollPositionChanged.connect(self._update_pos)
|
||||
except AttributeError:
|
||||
log.stub('scrollPositionChanged, on Qt < 5.7')
|
||||
self._on_scroll_pos_changed()
|
||||
self._pos_perc = (None, None)
|
||||
|
||||
def _key_press(self, key, count=1):
|
||||
# FIXME:qtwebengine Abort scrolling if the minimum/maximum was reached.
|
||||
@ -207,9 +208,9 @@ class WebEngineScroller(browsertab.AbstractScroller):
|
||||
QApplication.postEvent(recipient, release_evt)
|
||||
|
||||
@pyqtSlot()
|
||||
def _on_scroll_pos_changed(self):
|
||||
def _update_pos(self):
|
||||
"""Update the scroll position attributes when it changed."""
|
||||
def update_scroll_pos(jsret):
|
||||
def update_pos_cb(jsret):
|
||||
"""Callback after getting scroll position via JS."""
|
||||
if jsret is None:
|
||||
# This can happen when the callback would get called after
|
||||
@ -220,8 +221,8 @@ class WebEngineScroller(browsertab.AbstractScroller):
|
||||
self._pos_px = QPoint(jsret['px']['x'], jsret['px']['y'])
|
||||
self.perc_changed.emit(*self._pos_perc)
|
||||
|
||||
js_code = javascript.assemble('scroll', 'scroll_pos')
|
||||
self._tab.run_js_async(js_code, update_scroll_pos)
|
||||
js_code = javascript.assemble('scroll', 'pos')
|
||||
self._tab.run_js_async(js_code, update_pos_cb)
|
||||
|
||||
def pos_px(self):
|
||||
return self._pos_px
|
||||
@ -230,18 +231,18 @@ class WebEngineScroller(browsertab.AbstractScroller):
|
||||
return self._pos_perc
|
||||
|
||||
def to_perc(self, x=None, y=None):
|
||||
js_code = javascript.assemble('scroll', 'scroll_to_perc', x, y)
|
||||
js_code = javascript.assemble('scroll', 'to_perc', x, y)
|
||||
self._tab.run_js_async(js_code)
|
||||
|
||||
def to_point(self, point):
|
||||
self._tab.run_js_async("window.scroll({x}, {y});".format(
|
||||
x=point.x(), y=point.y()))
|
||||
js_code = javascript.assemble('window', 'scroll', point.x(), point.y())
|
||||
self._tab.run_js_async(js_code)
|
||||
|
||||
def delta(self, x=0, y=0):
|
||||
self._tab.run_js_async("window.scrollBy({x}, {y});".format(x=x, y=y))
|
||||
self._tab.run_js_async(javascript.assemble('window', 'scrollBy', x, y))
|
||||
|
||||
def delta_page(self, x=0, y=0):
|
||||
js_code = javascript.assemble('scroll', 'scroll_delta_page', x, y)
|
||||
js_code = javascript.assemble('scroll', 'delta_page', x, y)
|
||||
self._tab.run_js_async(js_code)
|
||||
|
||||
def up(self, count=1):
|
||||
@ -332,13 +333,50 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
self._set_widget(widget)
|
||||
self._connect_signals()
|
||||
self.backend = usertypes.Backend.QtWebEngine
|
||||
# init js stuff
|
||||
self._init_js()
|
||||
self._child_event_filter = None
|
||||
|
||||
def _init_js(self):
|
||||
js_code = '\n'.join([
|
||||
'"use strict";',
|
||||
'window._qutebrowser = {};',
|
||||
utils.read_file('javascript/scroll.js'),
|
||||
utils.read_file('javascript/webelem.js'),
|
||||
])
|
||||
script = QWebEngineScript()
|
||||
script.setInjectionPoint(QWebEngineScript.DocumentCreation)
|
||||
page = self._widget.page()
|
||||
script.setSourceCode(js_code)
|
||||
|
||||
try:
|
||||
page.runJavaScript("", QWebEngineScript.ApplicationWorld)
|
||||
except TypeError:
|
||||
# We're unable to pass a world to runJavaScript
|
||||
script.setWorldId(QWebEngineScript.MainWorld)
|
||||
else:
|
||||
script.setWorldId(QWebEngineScript.ApplicationWorld)
|
||||
|
||||
# FIXME:qtwebengine what about runsOnSubFrames?
|
||||
page.scripts().insert(script)
|
||||
|
||||
def _install_event_filter(self):
|
||||
self._widget.focusProxy().installEventFilter(self._mouse_event_filter)
|
||||
self._child_event_filter = mouse.ChildEventFilter(
|
||||
eventfilter=self._mouse_event_filter, widget=self._widget,
|
||||
parent=self)
|
||||
self._widget.installEventFilter(self._child_event_filter)
|
||||
|
||||
def openurl(self, url):
|
||||
self._openurl_prepare(url)
|
||||
self._widget.load(url)
|
||||
|
||||
def url(self):
|
||||
return self._widget.url()
|
||||
def url(self, requested=False):
|
||||
page = self._widget.page()
|
||||
if requested:
|
||||
return page.requestedUrl()
|
||||
else:
|
||||
return page.url()
|
||||
|
||||
def dump_async(self, callback, *, plain=False):
|
||||
if plain:
|
||||
@ -411,9 +449,43 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
def clear_ssl_errors(self):
|
||||
log.stub()
|
||||
|
||||
def _find_all_elements_js_cb(self, callback, js_elems):
|
||||
"""Handle found elements coming from JS and call the real callback.
|
||||
|
||||
Args:
|
||||
callback: The callback originally passed to find_all_elements.
|
||||
js_elems: The elements serialized from javascript.
|
||||
"""
|
||||
elems = []
|
||||
for js_elem in js_elems:
|
||||
elem = webengineelem.WebEngineElement(js_elem, self.run_js_async)
|
||||
elems.append(elem)
|
||||
callback(elems)
|
||||
|
||||
def find_all_elements(self, selector, callback, *, only_visible=False):
|
||||
log.stub()
|
||||
callback([])
|
||||
js_code = javascript.assemble('webelem', 'find_all', selector)
|
||||
js_cb = functools.partial(self._find_all_elements_js_cb, callback)
|
||||
self.run_js_async(js_code, js_cb)
|
||||
|
||||
def _find_focus_element_js_cb(self, callback, js_elem):
|
||||
"""Handle a found focus elem coming from JS and call the real callback.
|
||||
|
||||
Args:
|
||||
callback: The callback originally passed to find_focus_element.
|
||||
Called with a WebEngineElement or None.
|
||||
js_elem: The element serialized from javascript.
|
||||
"""
|
||||
log.webview.debug("Got focus element from JS: {!r}".format(js_elem))
|
||||
if js_elem is None:
|
||||
callback(None)
|
||||
else:
|
||||
elem = webengineelem.WebEngineElement(js_elem, self.run_js_async)
|
||||
callback(elem)
|
||||
|
||||
def find_focus_element(self, callback):
|
||||
js_code = javascript.assemble('webelem', 'focus_element')
|
||||
js_cb = functools.partial(self._find_focus_element_js_cb, callback)
|
||||
self.run_js_async(js_code, js_cb)
|
||||
|
||||
def _connect_signals(self):
|
||||
view = self._widget
|
||||
@ -422,6 +494,7 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
page.linkHovered.connect(self.link_hovered)
|
||||
page.loadProgress.connect(self._on_load_progress)
|
||||
page.loadStarted.connect(self._on_load_started)
|
||||
page.loadStarted.connect(self._on_history_trigger)
|
||||
view.titleChanged.connect(self.title_changed)
|
||||
view.urlChanged.connect(self._on_url_changed)
|
||||
page.loadFinished.connect(self._on_load_finished)
|
||||
|
@ -34,7 +34,7 @@ import email.message
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
|
||||
from qutebrowser.browser.webkit import webelem, downloads
|
||||
from qutebrowser.browser.webkit import webkitelem, downloads
|
||||
from qutebrowser.utils import log, objreg, message, usertypes, utils, urlutils
|
||||
|
||||
try:
|
||||
@ -271,7 +271,7 @@ class _Downloader:
|
||||
elements = web_frame.findAllElements('link, script, img')
|
||||
|
||||
for element in elements:
|
||||
element = webelem.WebElementWrapper(element)
|
||||
element = webkitelem.WebKitElement(element)
|
||||
# Websites are free to set whatever rel=... attribute they want.
|
||||
# We just care about stylesheets and icons.
|
||||
if not _check_rel(element):
|
||||
@ -288,7 +288,7 @@ class _Downloader:
|
||||
|
||||
styles = web_frame.findAllElements('style')
|
||||
for style in styles:
|
||||
style = webelem.WebElementWrapper(style)
|
||||
style = webkitelem.WebKitElement(style)
|
||||
# The Mozilla Developer Network says:
|
||||
# type: This attribute defines the styling language as a MIME type
|
||||
# (charset should not be specified). This attribute is optional and
|
||||
@ -301,7 +301,7 @@ class _Downloader:
|
||||
|
||||
# Search for references in inline styles
|
||||
for element in web_frame.findAllElements('[style]'):
|
||||
element = webelem.WebElementWrapper(element)
|
||||
element = webkitelem.WebKitElement(element)
|
||||
style = element['style']
|
||||
for element_url in _get_css_imports(style, inline=True):
|
||||
self._fetch_url(web_url.resolved(QUrl(element_url)))
|
||||
|
@ -17,65 +17,26 @@
|
||||
# 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 QWebElements.
|
||||
"""QtWebKit specific part of the web element API."""
|
||||
|
||||
Module attributes:
|
||||
Group: Enum for different kinds of groups.
|
||||
SELECTORS: CSS selectors for different groups of elements.
|
||||
FILTERS: A dictionary of filter functions for the modes.
|
||||
The filter for "links" filters javascript:-links and a-tags
|
||||
without "href".
|
||||
"""
|
||||
|
||||
import collections.abc
|
||||
|
||||
from PyQt5.QtCore import QRect, QUrl
|
||||
from PyQt5.QtCore import QRect
|
||||
from PyQt5.QtWebKit import QWebElement
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import log, usertypes, utils, javascript, qtutils
|
||||
from qutebrowser.utils import log, utils, javascript
|
||||
from qutebrowser.browser import webelem
|
||||
|
||||
|
||||
Group = usertypes.enum('Group', ['all', 'links', 'images', 'url', 'prevnext',
|
||||
'focus', 'inputs'])
|
||||
class IsNullError(webelem.Error):
|
||||
|
||||
|
||||
SELECTORS = {
|
||||
Group.all: ('a, area, textarea, select, input:not([type=hidden]), button, '
|
||||
'frame, iframe, link, [onclick], [onmousedown], [role=link], '
|
||||
'[role=option], [role=button], img'),
|
||||
Group.links: 'a, area, link, [role=link]',
|
||||
Group.images: 'img',
|
||||
Group.url: '[src], [href]',
|
||||
Group.prevnext: 'a, area, button, link, [role=button]',
|
||||
Group.focus: '*:focus',
|
||||
Group.inputs: ('input[type=text], input[type=email], input[type=url], '
|
||||
'input[type=tel], input[type=number], '
|
||||
'input[type=password], input[type=search], '
|
||||
'input:not([type]), textarea'),
|
||||
}
|
||||
|
||||
|
||||
def filter_links(elem):
|
||||
return 'href' in elem and QUrl(elem['href']).scheme() != 'javascript'
|
||||
|
||||
|
||||
FILTERS = {
|
||||
Group.links: filter_links,
|
||||
Group.prevnext: filter_links,
|
||||
}
|
||||
|
||||
|
||||
class IsNullError(Exception):
|
||||
|
||||
"""Gets raised by WebElementWrapper if an element is null."""
|
||||
"""Gets raised by WebKitElement if an element is null."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class WebElementWrapper(collections.abc.MutableMapping):
|
||||
class WebKitElement(webelem.AbstractWebElement):
|
||||
|
||||
"""A wrapper around QWebElement to make it more intelligent."""
|
||||
"""A wrapper around a QWebElement."""
|
||||
|
||||
def __init__(self, elem):
|
||||
if isinstance(elem, self.__class__):
|
||||
@ -85,21 +46,10 @@ class WebElementWrapper(collections.abc.MutableMapping):
|
||||
self._elem = elem
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, WebElementWrapper):
|
||||
if not isinstance(other, WebKitElement):
|
||||
return NotImplemented
|
||||
return self._elem == other._elem # pylint: disable=protected-access
|
||||
|
||||
def __str__(self):
|
||||
self._check_vanished()
|
||||
return self._elem.toPlainText()
|
||||
|
||||
def __repr__(self):
|
||||
try:
|
||||
html = self.debug_text()
|
||||
except IsNullError:
|
||||
html = None
|
||||
return utils.get_repr(self, html=html)
|
||||
|
||||
def __getitem__(self, key):
|
||||
self._check_vanished()
|
||||
if key not in self:
|
||||
@ -134,24 +84,19 @@ class WebElementWrapper(collections.abc.MutableMapping):
|
||||
raise IsNullError('Element {} vanished!'.format(self._elem))
|
||||
|
||||
def frame(self):
|
||||
"""Get the main frame of this element."""
|
||||
# FIXME:qtwebengine how to get rid of this?
|
||||
self._check_vanished()
|
||||
return self._elem.webFrame()
|
||||
|
||||
def geometry(self):
|
||||
"""Get the geometry for this element."""
|
||||
self._check_vanished()
|
||||
return self._elem.geometry()
|
||||
|
||||
def document_element(self):
|
||||
"""Get the document element of this element."""
|
||||
self._check_vanished()
|
||||
elem = self._elem.webFrame().documentElement()
|
||||
return WebElementWrapper(elem)
|
||||
return WebKitElement(elem)
|
||||
|
||||
def create_inside(self, tagname):
|
||||
"""Append the given element inside the current one."""
|
||||
# It seems impossible to create an empty QWebElement for which isNull()
|
||||
# is false so we can work with it.
|
||||
# As a workaround, we use appendInside() with markup as argument, and
|
||||
@ -159,28 +104,40 @@ class WebElementWrapper(collections.abc.MutableMapping):
|
||||
# See: http://stackoverflow.com/q/7364852/2085149
|
||||
self._check_vanished()
|
||||
self._elem.appendInside('<{}></{}>'.format(tagname, tagname))
|
||||
return WebElementWrapper(self._elem.lastChild())
|
||||
return WebKitElement(self._elem.lastChild())
|
||||
|
||||
def find_first(self, selector):
|
||||
"""Find the first child based on the given CSS selector."""
|
||||
self._check_vanished()
|
||||
elem = self._elem.findFirst(selector)
|
||||
if elem.isNull():
|
||||
return None
|
||||
return WebElementWrapper(elem)
|
||||
return WebKitElement(elem)
|
||||
|
||||
def style_property(self, name, strategy):
|
||||
"""Get the element style resolved with the given strategy."""
|
||||
def style_property(self, name, *, strategy):
|
||||
self._check_vanished()
|
||||
return self._elem.styleProperty(name, strategy)
|
||||
strategies = {
|
||||
# FIXME:qtwebengine which ones do we actually need?
|
||||
'inline': QWebElement.InlineStyle,
|
||||
'computed': QWebElement.ComputedStyle,
|
||||
}
|
||||
qt_strategy = strategies[strategy]
|
||||
return self._elem.styleProperty(name, qt_strategy)
|
||||
|
||||
def classes(self):
|
||||
self._check_vanished()
|
||||
return self._elem.classes()
|
||||
|
||||
def tag_name(self):
|
||||
"""Get the tag name for the current element."""
|
||||
self._check_vanished()
|
||||
return self._elem.tagName().lower()
|
||||
|
||||
def outer_xml(self):
|
||||
"""Get the full HTML representation of this element."""
|
||||
self._check_vanished()
|
||||
return self._elem.toOuterXml()
|
||||
|
||||
def text(self, *, use_js=False):
|
||||
"""Get the plain text content for this element.
|
||||
|
||||
Args:
|
||||
use_js: Whether to use javascript if the element isn't
|
||||
content-editable.
|
||||
"""
|
||||
self._check_vanished()
|
||||
if self.is_content_editable() or not use_js:
|
||||
return self._elem.toPlainText()
|
||||
@ -188,12 +145,6 @@ class WebElementWrapper(collections.abc.MutableMapping):
|
||||
return self._elem.evaluateJavaScript('this.value')
|
||||
|
||||
def set_text(self, text, *, use_js=False):
|
||||
"""Set the given plain text.
|
||||
|
||||
Args:
|
||||
use_js: Whether to use javascript if the element isn't
|
||||
content-editable.
|
||||
"""
|
||||
self._check_vanished()
|
||||
if self.is_content_editable() or not use_js:
|
||||
log.misc.debug("Filling element {} via set_text.".format(
|
||||
@ -206,158 +157,17 @@ class WebElementWrapper(collections.abc.MutableMapping):
|
||||
self._elem.evaluateJavaScript("this.value='{}'".format(text))
|
||||
|
||||
def set_inner_xml(self, xml):
|
||||
"""Set the given inner XML."""
|
||||
self._check_vanished()
|
||||
self._elem.setInnerXml(xml)
|
||||
|
||||
def remove_from_document(self):
|
||||
"""Remove the node from the document."""
|
||||
self._check_vanished()
|
||||
self._elem.removeFromDocument()
|
||||
|
||||
def set_style_property(self, name, value):
|
||||
"""Set the element style."""
|
||||
self._check_vanished()
|
||||
return self._elem.setStyleProperty(name, value)
|
||||
|
||||
def is_writable(self):
|
||||
"""Check whether an element is writable."""
|
||||
self._check_vanished()
|
||||
return not ('disabled' in self or 'readonly' in self)
|
||||
|
||||
def is_content_editable(self):
|
||||
"""Check if an element has a contenteditable attribute.
|
||||
|
||||
Args:
|
||||
elem: The QWebElement to check.
|
||||
|
||||
Return:
|
||||
True if the element has a contenteditable attribute,
|
||||
False otherwise.
|
||||
"""
|
||||
self._check_vanished()
|
||||
try:
|
||||
return self['contenteditable'].lower() not in ['false', 'inherit']
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
def _is_editable_object(self):
|
||||
"""Check if an object-element is editable."""
|
||||
if 'type' not in self:
|
||||
log.webview.debug("<object> without type clicked...")
|
||||
return False
|
||||
objtype = self['type'].lower()
|
||||
if objtype.startswith('application/') or 'classid' in self:
|
||||
# Let's hope flash/java stuff has an application/* mimetype OR
|
||||
# at least a classid attribute. Oh, and let's hope images/...
|
||||
# DON'T have a classid attribute. HTML sucks.
|
||||
log.webview.debug("<object type='{}'> clicked.".format(objtype))
|
||||
return config.get('input', 'insert-mode-on-plugins')
|
||||
else:
|
||||
# Image/Audio/...
|
||||
return False
|
||||
|
||||
def _is_editable_input(self):
|
||||
"""Check if an input-element is editable.
|
||||
|
||||
Return:
|
||||
True if the element is editable, False otherwise.
|
||||
"""
|
||||
try:
|
||||
objtype = self['type'].lower()
|
||||
except KeyError:
|
||||
return self.is_writable()
|
||||
else:
|
||||
if objtype in ['text', 'email', 'url', 'tel', 'number', 'password',
|
||||
'search']:
|
||||
return self.is_writable()
|
||||
else:
|
||||
return False
|
||||
|
||||
def _is_editable_div(self):
|
||||
"""Check if a div-element is editable.
|
||||
|
||||
Return:
|
||||
True if the element is editable, False otherwise.
|
||||
"""
|
||||
# Beginnings of div-classes which are actually some kind of editor.
|
||||
div_classes = ('CodeMirror', # Javascript editor over a textarea
|
||||
'kix-', # Google Docs editor
|
||||
'ace_') # http://ace.c9.io/
|
||||
for klass in self._elem.classes():
|
||||
if any([klass.startswith(e) for e in div_classes]):
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_editable(self, strict=False):
|
||||
"""Check whether we should switch to insert mode for this element.
|
||||
|
||||
Args:
|
||||
strict: Whether to do stricter checking so only fields where we can
|
||||
get the value match, for use with the :editor command.
|
||||
|
||||
Return:
|
||||
True if we should switch to insert mode, False otherwise.
|
||||
"""
|
||||
self._check_vanished()
|
||||
roles = ('combobox', 'textbox')
|
||||
log.misc.debug("Checking if element is editable: {}".format(
|
||||
repr(self)))
|
||||
tag = self._elem.tagName().lower()
|
||||
if self.is_content_editable() and self.is_writable():
|
||||
return True
|
||||
elif self.get('role', None) in roles and self.is_writable():
|
||||
return True
|
||||
elif tag == 'input':
|
||||
return self._is_editable_input()
|
||||
elif tag == 'textarea':
|
||||
return self.is_writable()
|
||||
elif tag in ['embed', 'applet']:
|
||||
# Flash/Java/...
|
||||
return config.get('input', 'insert-mode-on-plugins') and not strict
|
||||
elif tag == 'object':
|
||||
return self._is_editable_object() and not strict
|
||||
elif tag == 'div':
|
||||
return self._is_editable_div() and not strict
|
||||
else:
|
||||
return False
|
||||
|
||||
def is_text_input(self):
|
||||
"""Check if this element is some kind of text box."""
|
||||
self._check_vanished()
|
||||
roles = ('combobox', 'textbox')
|
||||
tag = self._elem.tagName().lower()
|
||||
return self.get('role', None) in roles or tag in ['input', 'textarea']
|
||||
|
||||
def remove_blank_target(self):
|
||||
"""Remove target from link."""
|
||||
self._check_vanished()
|
||||
elem = self._elem
|
||||
for _ in range(5):
|
||||
if elem is None:
|
||||
break
|
||||
tag = elem.tagName().lower()
|
||||
if tag == 'a' or tag == 'area':
|
||||
if elem.attribute('target') == '_blank':
|
||||
elem.setAttribute('target', '_top')
|
||||
break
|
||||
elem = elem.parent()
|
||||
|
||||
def debug_text(self):
|
||||
"""Get a text based on an element suitable for debug output."""
|
||||
self._check_vanished()
|
||||
return utils.compact_text(self._elem.toOuterXml(), 500)
|
||||
|
||||
def outer_xml(self):
|
||||
"""Get the full HTML representation of this element."""
|
||||
self._check_vanished()
|
||||
return self._elem.toOuterXml()
|
||||
|
||||
def tag_name(self):
|
||||
"""Get the tag name for the current element."""
|
||||
self._check_vanished()
|
||||
return self._elem.tagName()
|
||||
|
||||
def run_js_async(self, code, callback=None):
|
||||
"""Run the given JS snippet async on the element."""
|
||||
self._check_vanished()
|
||||
@ -365,8 +175,16 @@ class WebElementWrapper(collections.abc.MutableMapping):
|
||||
if callback is not None:
|
||||
callback(result)
|
||||
|
||||
def parent(self):
|
||||
self._check_vanished()
|
||||
elem = self._elem.parent()
|
||||
if elem is None:
|
||||
return None
|
||||
return WebKitElement(elem)
|
||||
|
||||
def _rect_on_view_js(self, adjust_zoom):
|
||||
"""Javascript implementation for rect_on_view."""
|
||||
# FIXME:qtwebengine maybe we can reuse this?
|
||||
rects = self._elem.evaluateJavaScript("this.getClientRects()")
|
||||
if rects is None: # pragma: no cover
|
||||
# Depending on unknown circumstances, this might not work with JS
|
||||
@ -444,6 +262,8 @@ class WebElementWrapper(collections.abc.MutableMapping):
|
||||
current zoom level.
|
||||
no_js: Fall back to the Python implementation
|
||||
"""
|
||||
# FIXME:qtwebengine can we get rid of this with
|
||||
# find_all_elements(only_visible=True)?
|
||||
self._check_vanished()
|
||||
|
||||
# First try getting the element rect via JS, as that's usually more
|
||||
@ -500,33 +320,6 @@ class WebElementWrapper(collections.abc.MutableMapping):
|
||||
visible_in_frame = visible_on_screen
|
||||
return all([visible_on_screen, visible_in_frame])
|
||||
|
||||
def resolve_url(self, baseurl):
|
||||
"""Resolve the URL in the element's src/href attribute.
|
||||
|
||||
Args:
|
||||
baseurl: The URL to base relative URLs on as QUrl.
|
||||
|
||||
Return:
|
||||
A QUrl with the absolute URL, or None.
|
||||
"""
|
||||
if baseurl.isRelative():
|
||||
raise ValueError("Need an absolute base URL!")
|
||||
|
||||
for attr in ['href', 'src']:
|
||||
if attr in self:
|
||||
text = self[attr].strip()
|
||||
break
|
||||
else:
|
||||
return None
|
||||
|
||||
url = QUrl(text)
|
||||
if not url.isValid():
|
||||
return None
|
||||
if url.isRelative():
|
||||
url = baseurl.resolved(url)
|
||||
qtutils.ensure_valid(url)
|
||||
return url
|
||||
|
||||
|
||||
def get_child_frames(startframe):
|
||||
"""Get all children recursively of a given QWebFrame.
|
||||
@ -556,5 +349,5 @@ def focus_elem(frame):
|
||||
Args:
|
||||
frame: The QWebFrame to search in.
|
||||
"""
|
||||
elem = frame.findFirstElement(SELECTORS[Group.focus])
|
||||
return WebElementWrapper(elem)
|
||||
elem = frame.findFirstElement('*:focus')
|
||||
return WebKitElement(elem)
|
61
qutebrowser/browser/webkit/webkithistory.py
Normal file
61
qutebrowser/browser/webkit/webkithistory.py
Normal file
@ -0,0 +1,61 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2015-2016 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/>.
|
||||
|
||||
"""QtWebKit specific part of history."""
|
||||
|
||||
|
||||
from PyQt5.QtWebKit import QWebHistoryInterface
|
||||
|
||||
|
||||
class WebHistoryInterface(QWebHistoryInterface):
|
||||
|
||||
"""Glue code between WebHistory and Qt's QWebHistoryInterface.
|
||||
|
||||
Attributes:
|
||||
_history: The WebHistory object.
|
||||
"""
|
||||
|
||||
def __init__(self, webhistory, parent=None):
|
||||
super().__init__(parent)
|
||||
self._history = webhistory
|
||||
|
||||
def addHistoryEntry(self, url_string):
|
||||
"""Required for a QWebHistoryInterface impl, obsoleted by add_url."""
|
||||
pass
|
||||
|
||||
def historyContains(self, url_string):
|
||||
"""Called by WebKit to determine if a URL is contained in the history.
|
||||
|
||||
Args:
|
||||
url_string: The URL (as string) to check for.
|
||||
|
||||
Return:
|
||||
True if the url is in the history, False otherwise.
|
||||
"""
|
||||
return url_string in self._history.history_dict
|
||||
|
||||
|
||||
def init(history):
|
||||
"""Initialize the QWebHistoryInterface.
|
||||
|
||||
Args:
|
||||
history: The WebHistory object.
|
||||
"""
|
||||
interface = WebHistoryInterface(history, parent=history)
|
||||
QWebHistoryInterface.setDefaultInterface(interface)
|
@ -34,22 +34,30 @@ from qutebrowser.utils import standarddir, objreg
|
||||
|
||||
class Attribute(websettings.Attribute):
|
||||
|
||||
"""A setting set via QWebSettings::setAttribute."""
|
||||
|
||||
GLOBAL_SETTINGS = QWebSettings.globalSettings
|
||||
ENUM_BASE = QWebSettings
|
||||
|
||||
|
||||
class Setter(websettings.Setter):
|
||||
|
||||
"""A setting set via QWebSettings getter/setter methods."""
|
||||
|
||||
GLOBAL_SETTINGS = QWebSettings.globalSettings
|
||||
|
||||
|
||||
class NullStringSetter(websettings.NullStringSetter):
|
||||
|
||||
"""A setter for settings requiring a null QString as default."""
|
||||
|
||||
GLOBAL_SETTINGS = QWebSettings.globalSettings
|
||||
|
||||
|
||||
class StaticSetter(websettings.StaticSetter):
|
||||
|
||||
"""A setting set via static QWebSettings getter/setter methods."""
|
||||
|
||||
GLOBAL_SETTINGS = QWebSettings.globalSettings
|
||||
|
||||
|
||||
|
@ -31,7 +31,7 @@ from PyQt5.QtWebKit import QWebSettings
|
||||
from PyQt5.QtPrintSupport import QPrinter
|
||||
|
||||
from qutebrowser.browser import browsertab
|
||||
from qutebrowser.browser.webkit import webview, tabhistory, webelem
|
||||
from qutebrowser.browser.webkit import webview, tabhistory, webkitelem
|
||||
from qutebrowser.utils import qtutils, objreg, usertypes, utils
|
||||
|
||||
|
||||
@ -510,12 +510,19 @@ class WebKitTab(browsertab.AbstractTab):
|
||||
self.zoom.set_default()
|
||||
self.backend = usertypes.Backend.QtWebKit
|
||||
|
||||
def _install_event_filter(self):
|
||||
self._widget.installEventFilter(self._mouse_event_filter)
|
||||
|
||||
def openurl(self, url):
|
||||
self._openurl_prepare(url)
|
||||
self._widget.openurl(url)
|
||||
|
||||
def url(self):
|
||||
return self._widget.url()
|
||||
def url(self, requested=False):
|
||||
frame = self._widget.page().mainFrame()
|
||||
if requested:
|
||||
return frame.requestedUrl()
|
||||
else:
|
||||
return frame.url()
|
||||
|
||||
def dump_async(self, callback, *, plain=False):
|
||||
frame = self._widget.page().mainFrame()
|
||||
@ -564,16 +571,28 @@ class WebKitTab(browsertab.AbstractTab):
|
||||
raise browsertab.WebTabError("No frame focused!")
|
||||
|
||||
elems = []
|
||||
frames = webelem.get_child_frames(mainframe)
|
||||
frames = webkitelem.get_child_frames(mainframe)
|
||||
for f in frames:
|
||||
for elem in f.findAllElements(selector):
|
||||
elems.append(webelem.WebElementWrapper(elem))
|
||||
elems.append(webkitelem.WebKitElement(elem))
|
||||
|
||||
if only_visible:
|
||||
elems = [e for e in elems if e.is_visible(mainframe)]
|
||||
|
||||
callback(elems)
|
||||
|
||||
def find_focus_element(self, callback):
|
||||
frame = self._widget.page().currentFrame()
|
||||
if frame is None:
|
||||
callback(None)
|
||||
return
|
||||
|
||||
elem = frame.findFirstElement('*:focus')
|
||||
if elem.isNull():
|
||||
callback(None)
|
||||
else:
|
||||
callback(webkitelem.WebKitElement(elem))
|
||||
|
||||
@pyqtSlot()
|
||||
def _on_frame_load_finished(self):
|
||||
"""Make sure we emit an appropriate status when loading finished.
|
||||
@ -618,3 +637,4 @@ class WebKitTab(browsertab.AbstractTab):
|
||||
view.iconChanged.connect(self._on_webkit_icon_changed)
|
||||
page.frameCreated.connect(self._on_frame_created)
|
||||
frame.contentsSizeChanged.connect(self._on_contents_size_changed)
|
||||
frame.initialLayoutCompleted.connect(self._on_history_trigger)
|
||||
|
@ -29,9 +29,9 @@ from PyQt5.QtWebKitWidgets import QWebView, QWebPage, QWebFrame
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.keyinput import modeman
|
||||
from qutebrowser.utils import message, log, usertypes, utils, qtutils, objreg
|
||||
from qutebrowser.utils import log, usertypes, utils, qtutils, objreg
|
||||
from qutebrowser.browser import hints
|
||||
from qutebrowser.browser.webkit import webpage, webelem
|
||||
from qutebrowser.browser.webkit import webpage, webkitelem
|
||||
|
||||
|
||||
class WebView(QWebView):
|
||||
@ -94,26 +94,11 @@ class WebView(QWebView):
|
||||
self.setContextMenuPolicy(Qt.PreventContextMenu)
|
||||
objreg.get('config').changed.connect(self.on_config_changed)
|
||||
|
||||
@pyqtSlot()
|
||||
def on_initial_layout_completed(self):
|
||||
"""Add url to history now that we have displayed something."""
|
||||
history = objreg.get('web-history')
|
||||
no_formatting = QUrl.UrlFormattingOption(0)
|
||||
orig_url = self.page().mainFrame().requestedUrl()
|
||||
if (orig_url.isValid() and
|
||||
not orig_url.matches(self.url(), no_formatting)):
|
||||
# If the url of the page is different than the url of the link
|
||||
# originally clicked, save them both.
|
||||
history.add_url(orig_url, self.title(), redirect=True)
|
||||
history.add_url(self.url(), self.title())
|
||||
|
||||
def _init_page(self):
|
||||
"""Initialize the QWebPage used by this view."""
|
||||
page = webpage.BrowserPage(self.win_id, self._tab_id, self)
|
||||
self.setPage(page)
|
||||
page.mainFrame().loadFinished.connect(self.on_load_finished)
|
||||
page.mainFrame().initialLayoutCompleted.connect(
|
||||
self.on_initial_layout_completed)
|
||||
return page
|
||||
|
||||
def __repr__(self):
|
||||
@ -153,27 +138,6 @@ class WebView(QWebView):
|
||||
elif section == 'colors' and option == 'webpage.bg':
|
||||
self._set_bg_color()
|
||||
|
||||
def _mousepress_backforward(self, e):
|
||||
"""Handle back/forward mouse button presses.
|
||||
|
||||
Args:
|
||||
e: The QMouseEvent.
|
||||
"""
|
||||
if e.button() in [Qt.XButton1, Qt.LeftButton]:
|
||||
# Back button on mice which have it, or rocker gesture
|
||||
if self.page().history().canGoBack():
|
||||
self.back()
|
||||
else:
|
||||
message.error(self.win_id, "At beginning of history.",
|
||||
immediately=True)
|
||||
elif e.button() in [Qt.XButton2, Qt.RightButton]:
|
||||
# Forward button on mice which have it, or rocker gesture
|
||||
if self.page().history().canGoForward():
|
||||
self.forward()
|
||||
else:
|
||||
message.error(self.win_id, "At end of history.",
|
||||
immediately=True)
|
||||
|
||||
def _mousepress_insertmode(self, e):
|
||||
"""Switch to insert mode when an editable element was clicked.
|
||||
|
||||
@ -196,13 +160,13 @@ class WebView(QWebView):
|
||||
if hitresult.isNull():
|
||||
# For some reason, the whole hit result can be null sometimes (e.g.
|
||||
# on doodle menu links). If this is the case, we schedule a check
|
||||
# later (in mouseReleaseEvent) which uses webelem.focus_elem.
|
||||
# later (in mouseReleaseEvent) which uses webkitelem.focus_elem.
|
||||
log.mouse.debug("Hitresult is null!")
|
||||
self._check_insertmode = True
|
||||
return
|
||||
try:
|
||||
elem = webelem.WebElementWrapper(hitresult.element())
|
||||
except webelem.IsNullError:
|
||||
elem = webkitelem.WebKitElement(hitresult.element())
|
||||
except webkitelem.IsNullError:
|
||||
# For some reason, the hit result element can be a null element
|
||||
# sometimes (e.g. when clicking the timetable fields on
|
||||
# http://www.sbb.ch/ ). If this is the case, we schedule a check
|
||||
@ -223,12 +187,13 @@ class WebView(QWebView):
|
||||
|
||||
def mouserelease_insertmode(self):
|
||||
"""If we have an insertmode check scheduled, handle it."""
|
||||
# FIXME:qtwebengine Use tab.find_focus_element here
|
||||
if not self._check_insertmode:
|
||||
return
|
||||
self._check_insertmode = False
|
||||
try:
|
||||
elem = webelem.focus_elem(self.page().currentFrame())
|
||||
except (webelem.IsNullError, RuntimeError):
|
||||
elem = webkitelem.focus_elem(self.page().currentFrame())
|
||||
except (webkitelem.IsNullError, RuntimeError):
|
||||
log.mouse.debug("Element/page vanished!")
|
||||
return
|
||||
if elem.is_editable():
|
||||
@ -325,8 +290,8 @@ class WebView(QWebView):
|
||||
return
|
||||
frame = self.page().currentFrame()
|
||||
try:
|
||||
elem = webelem.WebElementWrapper(frame.findFirstElement(':focus'))
|
||||
except webelem.IsNullError:
|
||||
elem = webkitelem.focus_elem(frame)
|
||||
except webkitelem.IsNullError:
|
||||
log.webview.debug("Focused element is null!")
|
||||
return
|
||||
log.modes.debug("focus element: {}".format(repr(elem)))
|
||||
@ -421,13 +386,6 @@ class WebView(QWebView):
|
||||
Return:
|
||||
The superclass return value.
|
||||
"""
|
||||
is_rocker_gesture = (config.get('input', 'rocker-gestures') and
|
||||
e.buttons() == Qt.LeftButton | Qt.RightButton)
|
||||
|
||||
if e.button() in [Qt.XButton1, Qt.XButton2] or is_rocker_gesture:
|
||||
self._mousepress_backforward(e)
|
||||
super().mousePressEvent(e)
|
||||
return
|
||||
self._mousepress_insertmode(e)
|
||||
self._mousepress_opentarget(e)
|
||||
self._ignore_wheel_event = True
|
||||
|
@ -75,13 +75,13 @@ class Command:
|
||||
deprecated: False, or a string to describe why a command is deprecated.
|
||||
desc: The description of the command.
|
||||
handler: The handler function to call.
|
||||
completion: Completions to use for arguments, as a list of strings.
|
||||
debug: Whether this is a debugging command (only shown with --debug).
|
||||
parser: The ArgumentParser to use to parse this command.
|
||||
flags_with_args: A list of flags which take an argument.
|
||||
no_cmd_split: If true, ';;' to split sub-commands is ignored.
|
||||
backend: Which backend the command works with (or None if it works with
|
||||
both)
|
||||
no_replace_variables: Don't replace variables like {url}
|
||||
_qute_args: The saved data from @cmdutils.argument
|
||||
_needs_js: Whether the command needs javascript enabled
|
||||
_modes: The modes the command can be executed in.
|
||||
@ -95,7 +95,7 @@ class Command:
|
||||
hide=False, modes=None, not_modes=None, needs_js=False,
|
||||
debug=False, ignore_args=False, deprecated=False,
|
||||
no_cmd_split=False, star_args_optional=False, scope='global',
|
||||
backend=None):
|
||||
backend=None, no_replace_variables=False):
|
||||
# I really don't know how to solve this in a better way, I tried.
|
||||
# pylint: disable=too-many-locals
|
||||
if modes is not None and not_modes is not None:
|
||||
@ -127,6 +127,7 @@ class Command:
|
||||
self.handler = handler
|
||||
self.no_cmd_split = no_cmd_split
|
||||
self.backend = backend
|
||||
self.no_replace_variables = no_replace_variables
|
||||
|
||||
self.docparser = docutils.DocstringParser(handler)
|
||||
self.parser = argparser.ArgumentParser(
|
||||
@ -148,13 +149,7 @@ class Command:
|
||||
self._qute_args = getattr(self.handler, 'qute_args', {})
|
||||
self.handler.qute_args = None
|
||||
|
||||
args = self._inspect_func()
|
||||
|
||||
self.completion = []
|
||||
for arg in args:
|
||||
arg_completion = self.get_arg_info(arg).completion
|
||||
if arg_completion is not None:
|
||||
self.completion.append(arg_completion)
|
||||
self._inspect_func()
|
||||
|
||||
def _check_prerequisites(self, win_id):
|
||||
"""Check if the command is permitted to run currently.
|
||||
@ -208,6 +203,11 @@ class Command:
|
||||
"""Get an ArgInfo tuple for the given inspect.Parameter."""
|
||||
return self._qute_args.get(param.name, ArgInfo())
|
||||
|
||||
def get_pos_arg_info(self, pos):
|
||||
"""Get an ArgInfo tuple for the given positional parameter."""
|
||||
name = self.pos_args[pos][0]
|
||||
return self._qute_args.get(name, ArgInfo())
|
||||
|
||||
def _inspect_special_param(self, param):
|
||||
"""Check if the given parameter is a special one.
|
||||
|
||||
|
@ -26,7 +26,7 @@ from PyQt5.QtCore import pyqtSlot, QUrl, QObject
|
||||
|
||||
from qutebrowser.config import config, configexc
|
||||
from qutebrowser.commands import cmdexc, cmdutils
|
||||
from qutebrowser.utils import message, objreg, qtutils
|
||||
from qutebrowser.utils import message, objreg, qtutils, utils
|
||||
from qutebrowser.misc import split
|
||||
|
||||
|
||||
@ -49,21 +49,29 @@ def _current_url(tabbed_browser):
|
||||
|
||||
def replace_variables(win_id, arglist):
|
||||
"""Utility function to replace variables like {url} in a list of args."""
|
||||
variables = {
|
||||
'{url}': lambda: _current_url(tabbed_browser).toString(
|
||||
QUrl.FullyEncoded | QUrl.RemovePassword),
|
||||
'{url:pretty}': lambda: _current_url(tabbed_browser).toString(
|
||||
QUrl.RemovePassword),
|
||||
'{clipboard}': utils.get_clipboard,
|
||||
'{primary}': lambda: utils.get_clipboard(selection=True),
|
||||
}
|
||||
values = {}
|
||||
args = []
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
if '{url}' in arglist:
|
||||
url = _current_url(tabbed_browser).toString(QUrl.FullyEncoded |
|
||||
QUrl.RemovePassword)
|
||||
if '{url:pretty}' in arglist:
|
||||
pretty_url = _current_url(tabbed_browser).toString(QUrl.RemovePassword)
|
||||
for arg in arglist:
|
||||
if arg == '{url}':
|
||||
args.append(url)
|
||||
elif arg == '{url:pretty}':
|
||||
args.append(pretty_url)
|
||||
else:
|
||||
|
||||
try:
|
||||
for arg in arglist:
|
||||
for var, func in variables.items():
|
||||
if var in arg:
|
||||
if var not in values:
|
||||
values[var] = func()
|
||||
arg = arg.replace(var, values[var])
|
||||
args.append(arg)
|
||||
except utils.ClipboardError as e:
|
||||
raise cmdexc.CommandError(e)
|
||||
return args
|
||||
|
||||
|
||||
@ -279,7 +287,10 @@ class CommandRunner(QObject):
|
||||
window=self._win_id)
|
||||
cur_mode = mode_manager.mode
|
||||
|
||||
args = replace_variables(self._win_id, result.args)
|
||||
if result.cmd.no_replace_variables:
|
||||
args = result.args
|
||||
else:
|
||||
args = replace_variables(self._win_id, result.args)
|
||||
if count is not None:
|
||||
if result.count is not None:
|
||||
raise cmdexc.CommandMetaError("Got count via command and "
|
||||
|
@ -204,25 +204,18 @@ class Completer(QObject):
|
||||
return sortfilter.CompletionFilterModel(source=model, parent=self)
|
||||
# delegate completion to command
|
||||
try:
|
||||
completions = cmdutils.cmd_dict[parts[0]].completion
|
||||
cmd = cmdutils.cmd_dict[parts[0]]
|
||||
except KeyError:
|
||||
# entering an unknown command
|
||||
return None
|
||||
if completions is None:
|
||||
# command without any available completions
|
||||
return None
|
||||
dbg_completions = [c.name for c in completions]
|
||||
try:
|
||||
idx = cursor_part - 1
|
||||
completion = completions[idx]
|
||||
completion = cmd.get_pos_arg_info(idx).completion
|
||||
except IndexError:
|
||||
# More arguments than completions
|
||||
log.completion.debug("completions: {}".format(
|
||||
', '.join(dbg_completions)))
|
||||
# user provided more positional arguments than the command takes
|
||||
return None
|
||||
if completion is None:
|
||||
return None
|
||||
dbg_completions[idx] = '*' + dbg_completions[idx] + '*'
|
||||
log.completion.debug("completions: {}".format(
|
||||
', '.join(dbg_completions)))
|
||||
model = self._get_completion_model(completion, parts, cursor_part)
|
||||
return model
|
||||
|
||||
|
@ -181,16 +181,14 @@ class CompletionView(QTreeView):
|
||||
# Item is a real item, not a category header -> success
|
||||
return idx
|
||||
|
||||
def _next_prev_item(self, prev):
|
||||
"""Handle a tab press for the CompletionView.
|
||||
|
||||
Select the previous/next item and write the new text to the
|
||||
statusbar.
|
||||
|
||||
Helper for completion_item_next and completion_item_prev.
|
||||
@cmdutils.register(instance='completion', hide=True,
|
||||
modes=[usertypes.KeyMode.command], scope='window')
|
||||
@cmdutils.argument('which', choices=['next', 'prev'])
|
||||
def completion_item_focus(self, which):
|
||||
"""Shift the focus of the completion menu to another item.
|
||||
|
||||
Args:
|
||||
prev: True for prev item, False for next one.
|
||||
which: 'next' or 'prev'
|
||||
"""
|
||||
# selmodel can be None if 'show' and 'auto-open' are set to False
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/1731
|
||||
@ -198,7 +196,7 @@ class CompletionView(QTreeView):
|
||||
if selmodel is None:
|
||||
return
|
||||
|
||||
idx = self._next_idx(prev)
|
||||
idx = self._next_idx(which == 'prev')
|
||||
if not idx.isValid():
|
||||
return
|
||||
|
||||
@ -278,18 +276,6 @@ class CompletionView(QTreeView):
|
||||
scrollbar.setValue(scrollbar.minimum())
|
||||
super().showEvent(e)
|
||||
|
||||
@cmdutils.register(instance='completion', hide=True,
|
||||
modes=[usertypes.KeyMode.command], scope='window')
|
||||
def completion_item_prev(self):
|
||||
"""Select the previous completion item."""
|
||||
self._next_prev_item(True)
|
||||
|
||||
@cmdutils.register(instance='completion', hide=True,
|
||||
modes=[usertypes.KeyMode.command], scope='window')
|
||||
def completion_item_next(self):
|
||||
"""Select the next completion item."""
|
||||
self._next_prev_item(False)
|
||||
|
||||
@cmdutils.register(instance='completion', hide=True,
|
||||
modes=[usertypes.KeyMode.command], scope='window')
|
||||
def completion_item_del(self):
|
||||
|
@ -27,10 +27,9 @@ Module attributes:
|
||||
|
||||
import functools
|
||||
|
||||
from qutebrowser.completion.models import (miscmodels, urlmodel, configmodel,
|
||||
base)
|
||||
from qutebrowser.completion.models import miscmodels, urlmodel, configmodel
|
||||
from qutebrowser.utils import objreg, usertypes, log, debug
|
||||
from qutebrowser.config import configdata
|
||||
from qutebrowser.config import configdata, config
|
||||
|
||||
|
||||
_instances = {}
|
||||
@ -115,11 +114,11 @@ def init_session_completion():
|
||||
_instances[usertypes.Completion.sessions] = model
|
||||
|
||||
|
||||
def _init_empty_completion():
|
||||
"""Initialize empty completion model."""
|
||||
log.completion.debug("Initializing empty completion.")
|
||||
if usertypes.Completion.empty not in _instances:
|
||||
_instances[usertypes.Completion.empty] = base.BaseCompletionModel()
|
||||
def _init_bind_completion():
|
||||
"""Initialize the command completion model."""
|
||||
log.completion.debug("Initializing bind completion.")
|
||||
model = miscmodels.BindCompletionModel()
|
||||
_instances[usertypes.Completion.bind] = model
|
||||
|
||||
|
||||
INITIALIZERS = {
|
||||
@ -133,7 +132,7 @@ INITIALIZERS = {
|
||||
usertypes.Completion.quickmark_by_name: init_quickmark_completions,
|
||||
usertypes.Completion.bookmark_by_url: init_bookmark_completions,
|
||||
usertypes.Completion.sessions: init_session_completion,
|
||||
usertypes.Completion.empty: _init_empty_completion,
|
||||
usertypes.Completion.bind: _init_bind_completion,
|
||||
}
|
||||
|
||||
|
||||
@ -164,6 +163,12 @@ def update(completions):
|
||||
did_run.append(func)
|
||||
|
||||
|
||||
@config.change_filter('aliases', function=True)
|
||||
def _update_aliases():
|
||||
"""Update completions that include command aliases."""
|
||||
update([usertypes.Completion.command])
|
||||
|
||||
|
||||
def init():
|
||||
"""Initialize completions. Note this only connects signals."""
|
||||
quickmark_manager = objreg.get('quickmark-manager')
|
||||
@ -185,3 +190,7 @@ def init():
|
||||
keyconf = objreg.get('key-config')
|
||||
keyconf.changed.connect(
|
||||
functools.partial(update, [usertypes.Completion.command]))
|
||||
keyconf.changed.connect(
|
||||
functools.partial(update, [usertypes.Completion.bind]))
|
||||
|
||||
objreg.get('config').changed.connect(_update_aliases)
|
||||
|
@ -30,7 +30,7 @@ from qutebrowser.completion.models import base
|
||||
|
||||
class CommandCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
"""A CompletionModel filled with all commands and descriptions."""
|
||||
"""A CompletionModel filled with non-hidden commands and descriptions."""
|
||||
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/545
|
||||
# pylint: disable=abstract-method
|
||||
@ -39,23 +39,11 @@ class CommandCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
assert cmdutils.cmd_dict
|
||||
cmdlist = []
|
||||
for obj in set(cmdutils.cmd_dict.values()):
|
||||
if (obj.hide or (obj.debug and not objreg.get('args').debug) or
|
||||
obj.deprecated):
|
||||
pass
|
||||
else:
|
||||
cmdlist.append((obj.name, obj.desc))
|
||||
for name, cmd in config.section('aliases').items():
|
||||
cmdlist.append((name, "Alias for '{}'".format(cmd)))
|
||||
cmdlist = _get_cmd_completions(include_aliases=True,
|
||||
include_hidden=False)
|
||||
cat = self.new_category("Commands")
|
||||
|
||||
# map each command to its bound keys and show these in the misc column
|
||||
key_config = objreg.get('key-config')
|
||||
cmd_to_keys = key_config.get_reverse_bindings_for('normal')
|
||||
for (name, desc) in sorted(cmdlist):
|
||||
self.new_item(cat, name, desc, ', '.join(cmd_to_keys[name]))
|
||||
for (name, desc, misc) in cmdlist:
|
||||
self.new_item(cat, name, desc, misc)
|
||||
|
||||
|
||||
class HelpCompletionModel(base.BaseCompletionModel):
|
||||
@ -72,17 +60,11 @@ class HelpCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
def _init_commands(self):
|
||||
"""Fill completion with :command entries."""
|
||||
assert cmdutils.cmd_dict
|
||||
cmdlist = []
|
||||
for obj in set(cmdutils.cmd_dict.values()):
|
||||
if (obj.hide or (obj.debug and not objreg.get('args').debug) or
|
||||
obj.deprecated):
|
||||
pass
|
||||
else:
|
||||
cmdlist.append((':' + obj.name, obj.desc))
|
||||
cmdlist = _get_cmd_completions(include_aliases=False,
|
||||
include_hidden=True, prefix=':')
|
||||
cat = self.new_category("Commands")
|
||||
for (name, desc) in sorted(cmdlist):
|
||||
self.new_item(cat, name, desc)
|
||||
for (name, desc, misc) in cmdlist:
|
||||
self.new_item(cat, name, desc, misc)
|
||||
|
||||
def _init_settings(self):
|
||||
"""Fill completion with section->option entries."""
|
||||
@ -166,7 +148,8 @@ class TabCompletionModel(base.BaseCompletionModel):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.columns_to_filter = [self.URL_COLUMN, self.TEXT_COLUMN]
|
||||
self.columns_to_filter = [self.IDX_COLUMN, self.URL_COLUMN,
|
||||
self.TEXT_COLUMN]
|
||||
|
||||
for win_id in objreg.window_registry:
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
@ -174,6 +157,7 @@ class TabCompletionModel(base.BaseCompletionModel):
|
||||
for i in range(tabbed_browser.count()):
|
||||
tab = tabbed_browser.widget(i)
|
||||
tab.url_changed.connect(self.rebuild)
|
||||
tab.title_changed.connect(self.rebuild)
|
||||
tab.shutting_down.connect(self.delayed_rebuild)
|
||||
tabbed_browser.new_tab.connect(self.on_new_tab)
|
||||
objreg.get("app").new_window.connect(self.on_new_window)
|
||||
@ -187,6 +171,7 @@ class TabCompletionModel(base.BaseCompletionModel):
|
||||
def on_new_tab(self, tab):
|
||||
"""Add hooks to new tabs."""
|
||||
tab.url_changed.connect(self.rebuild)
|
||||
tab.title_changed.connect(self.rebuild)
|
||||
tab.shutting_down.connect(self.delayed_rebuild)
|
||||
self.rebuild()
|
||||
|
||||
@ -257,3 +242,49 @@ class TabCompletionModel(base.BaseCompletionModel):
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=int(win_id))
|
||||
tabbed_browser.on_tab_close_requested(int(tab_index) - 1)
|
||||
|
||||
|
||||
class BindCompletionModel(base.BaseCompletionModel):
|
||||
|
||||
"""A CompletionModel filled with all bindable commands and descriptions."""
|
||||
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/545
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
COLUMN_WIDTHS = (20, 60, 20)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
cmdlist = _get_cmd_completions(include_hidden=True,
|
||||
include_aliases=True)
|
||||
cat = self.new_category("Commands")
|
||||
for (name, desc, misc) in cmdlist:
|
||||
self.new_item(cat, name, desc, misc)
|
||||
|
||||
|
||||
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 cmdlist
|
||||
|
@ -135,8 +135,8 @@ class CompletionFilterModel(QSortFilterProxyModel):
|
||||
|
||||
for col in self.srcmodel.columns_to_filter:
|
||||
idx = self.srcmodel.index(row, col, parent)
|
||||
if not idx.isValid():
|
||||
# No entries in parent model
|
||||
if not idx.isValid(): # pragma: no cover
|
||||
# this is a sanity check not hit by any test case
|
||||
continue
|
||||
data = self.srcmodel.data(idx)
|
||||
if not data:
|
||||
|
@ -36,7 +36,8 @@ import collections.abc
|
||||
from PyQt5.QtCore import pyqtSignal, QObject, QUrl, QSettings
|
||||
|
||||
from qutebrowser.config import configdata, configexc, textwrapper
|
||||
from qutebrowser.config.parsers import ini, keyconf
|
||||
from qutebrowser.config.parsers import keyconf
|
||||
from qutebrowser.config.parsers import ini
|
||||
from qutebrowser.commands import cmdexc, cmdutils
|
||||
from qutebrowser.utils import (message, objreg, utils, standarddir, log,
|
||||
qtutils, error, usertypes)
|
||||
@ -350,6 +351,7 @@ class ConfigManager(QObject):
|
||||
('tabs', 'auto-hide'),
|
||||
('tabs', 'hide-always'),
|
||||
('ui', 'display-statusbar-messages'),
|
||||
('ui', 'hide-mouse-cursor'),
|
||||
('general', 'wrap-search'),
|
||||
]
|
||||
CHANGED_OPTIONS = {
|
||||
|
@ -154,7 +154,7 @@ def data(readonly=False):
|
||||
"Whether to save the config automatically on quit."),
|
||||
|
||||
('auto-save-interval',
|
||||
SettingValue(typ.Int(minval=0), '15000'),
|
||||
SettingValue(typ.Int(minval=0, maxval=MAXVALS['int']), '15000'),
|
||||
"How often (in milliseconds) to auto-save config/cookies/etc."),
|
||||
|
||||
('editor',
|
||||
@ -227,13 +227,25 @@ def data(readonly=False):
|
||||
"How to open links in an existing instance if a new one is "
|
||||
"launched."),
|
||||
|
||||
('new-instance-open-target.window',
|
||||
SettingValue(typ.String(
|
||||
valid_values=typ.ValidValues(
|
||||
('last-opened', "Open new tabs in the last opened "
|
||||
"window."),
|
||||
('last-focused', "Open new tabs in the most recently "
|
||||
"focused window."),
|
||||
('last-visible', "Open new tabs in the most recently "
|
||||
"visible window.")
|
||||
)), 'last-focused'),
|
||||
"Which window to choose when opening links as new tabs."),
|
||||
|
||||
('log-javascript-console',
|
||||
SettingValue(typ.String(
|
||||
valid_values=typ.ValidValues(
|
||||
('none', "Don't log messages."),
|
||||
('debug', "Log messages with debug level."),
|
||||
('info', "Log messages with info level.")
|
||||
)), 'debug', backends=[usertypes.Backend.QtWebKit]),
|
||||
)), 'debug'),
|
||||
"How to log javascript console messages."),
|
||||
|
||||
('save-session',
|
||||
@ -346,10 +358,6 @@ def data(readonly=False):
|
||||
"* `{scroll_pos}`: The page scroll position.\n"
|
||||
"* `{host}`: The host of the current web page."),
|
||||
|
||||
('hide-mouse-cursor',
|
||||
SettingValue(typ.Bool(), 'false'),
|
||||
"Whether to hide the mouse cursor."),
|
||||
|
||||
('modal-js-dialog',
|
||||
SettingValue(typ.Bool(), 'false'),
|
||||
"Use standard JavaScript modal dialog for alert() and confirm()"),
|
||||
@ -488,13 +496,13 @@ def data(readonly=False):
|
||||
('input', sect.KeyValue(
|
||||
('timeout',
|
||||
SettingValue(typ.Int(minval=0, maxval=MAXVALS['int']), '500'),
|
||||
"Timeout for ambiguous key bindings.\n\n"
|
||||
"Timeout (in milliseconds) for ambiguous key bindings.\n\n"
|
||||
"If the current input forms both a complete match and a partial "
|
||||
"match, the complete match will be executed after this time."),
|
||||
|
||||
('partial-timeout',
|
||||
SettingValue(typ.Int(minval=0, maxval=MAXVALS['int']), '5000'),
|
||||
"Timeout for partially typed key bindings.\n\n"
|
||||
"Timeout (in milliseconds) for partially typed key bindings.\n\n"
|
||||
"If the current input forms only partial matches, the keystring "
|
||||
"will be cleared after this time."),
|
||||
|
||||
@ -933,8 +941,8 @@ def data(readonly=False):
|
||||
|
||||
('auto-follow-timeout',
|
||||
SettingValue(typ.Int(), '0'),
|
||||
"A timeout to inhibit normal-mode key bindings after a successful"
|
||||
"auto-follow."),
|
||||
"A timeout (in milliseconds) to inhibit normal-mode key bindings "
|
||||
"after a successful auto-follow."),
|
||||
|
||||
('next-regexes',
|
||||
SettingValue(typ.List(typ.Regex(flags=re.IGNORECASE)),
|
||||
@ -956,6 +964,10 @@ def data(readonly=False):
|
||||
)), 'python'),
|
||||
"Which implementation to use to find elements to hint."),
|
||||
|
||||
('hide-unmatched-rapid-hints',
|
||||
SettingValue(typ.Bool(), 'true'),
|
||||
"Controls hiding unmatched hints in rapid mode."),
|
||||
|
||||
readonly=readonly
|
||||
)),
|
||||
|
||||
@ -1266,7 +1278,7 @@ def data(readonly=False):
|
||||
"Font used in the completion widget."),
|
||||
|
||||
('completion.category',
|
||||
SettingValue(typ.Font(), 'bold ${completion}'),
|
||||
SettingValue(typ.Font(), 'bold ${completion}'),
|
||||
"Font used in the completion categories."),
|
||||
|
||||
('tabbar',
|
||||
@ -1415,8 +1427,7 @@ KEY_SECTION_DESC = {
|
||||
"Useful hidden commands to map in this section:\n\n"
|
||||
" * `command-history-prev`: Switch to previous command in history.\n"
|
||||
" * `command-history-next`: Switch to next command in history.\n"
|
||||
" * `completion-item-prev`: Select previous item in completion.\n"
|
||||
" * `completion-item-next`: Select next item in completion.\n"
|
||||
" * `completion-item-focus`: Select another item in completion.\n"
|
||||
" * `command-accept`: Execute the command currently in the "
|
||||
"commandline."),
|
||||
'prompt': (
|
||||
@ -1506,18 +1517,18 @@ KEY_DATA = collections.OrderedDict([
|
||||
('enter-mode jump_mark', ["'"]),
|
||||
('yank', ['yy']),
|
||||
('yank -s', ['yY']),
|
||||
('yank -t', ['yt']),
|
||||
('yank -ts', ['yT']),
|
||||
('yank -d', ['yd']),
|
||||
('yank -ds', ['yD']),
|
||||
('yank -p', ['yp']),
|
||||
('yank -ps', ['yP']),
|
||||
('paste', ['pp']),
|
||||
('paste -s', ['pP']),
|
||||
('paste -t', ['Pp']),
|
||||
('paste -ts', ['PP']),
|
||||
('paste -w', ['wp']),
|
||||
('paste -ws', ['wP']),
|
||||
('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']),
|
||||
@ -1589,8 +1600,8 @@ KEY_DATA = collections.OrderedDict([
|
||||
('command', collections.OrderedDict([
|
||||
('command-history-prev', ['<Ctrl-P>']),
|
||||
('command-history-next', ['<Ctrl-N>']),
|
||||
('completion-item-prev', ['<Shift-Tab>', '<Up>']),
|
||||
('completion-item-next', ['<Tab>', '<Down>']),
|
||||
('completion-item-focus prev', ['<Shift-Tab>', '<Up>']),
|
||||
('completion-item-focus next', ['<Tab>', '<Down>']),
|
||||
('completion-item-del', ['<Ctrl-D>']),
|
||||
('command-accept', RETURN_KEYS),
|
||||
])),
|
||||
@ -1638,8 +1649,8 @@ KEY_DATA = collections.OrderedDict([
|
||||
('move-to-end-of-line', ['$']),
|
||||
('move-to-start-of-document', ['gg']),
|
||||
('move-to-end-of-document', ['G']),
|
||||
('yank-selected -p', ['Y']),
|
||||
('yank-selected', ['y'] + RETURN_KEYS),
|
||||
('yank selection -s', ['Y']),
|
||||
('yank selection', ['y'] + RETURN_KEYS),
|
||||
('scroll left', ['H']),
|
||||
('scroll down', ['J']),
|
||||
('scroll up', ['K']),
|
||||
@ -1677,4 +1688,21 @@ CHANGED_KEY_COMMANDS = [
|
||||
(re.compile(r'^download-remove --all$'), r'download-clear'),
|
||||
|
||||
(re.compile(r'^hint links fill "([^"]*)"$'), r'hint links fill \1'),
|
||||
|
||||
(re.compile(r'^yank -t(\S+)'), r'yank title -\1'),
|
||||
(re.compile(r'^yank -t'), r'yank title'),
|
||||
(re.compile(r'^yank -d(\S+)'), r'yank domain -\1'),
|
||||
(re.compile(r'^yank -d'), r'yank domain'),
|
||||
(re.compile(r'^yank -p(\S+)'), r'yank pretty-url -\1'),
|
||||
(re.compile(r'^yank -p'), r'yank pretty-url'),
|
||||
(re.compile(r'^yank-selected -p'), r'yank selection -s'),
|
||||
(re.compile(r'^yank-selected'), r'yank selection'),
|
||||
|
||||
(re.compile(r'^paste$'), r'open {clipboard}'),
|
||||
(re.compile(r'^paste -([twb])$'), r'open -\1 {clipboard}'),
|
||||
(re.compile(r'^paste -([twb])s$'), r'open -\1 {primary}'),
|
||||
(re.compile(r'^paste -s([twb])$'), r'open -\1 {primary}'),
|
||||
|
||||
(re.compile(r'^completion-item-next'), r'completion-item-focus next'),
|
||||
(re.compile(r'^completion-item-prev'), r'completion-item-focus prev'),
|
||||
]
|
||||
|
@ -125,11 +125,11 @@ class BaseType:
|
||||
self.valid_values = None
|
||||
|
||||
def get_name(self):
|
||||
"""Get a name for the type for documentation"""
|
||||
"""Get a name for the type for documentation."""
|
||||
return self.__class__.__name__
|
||||
|
||||
def get_valid_values(self):
|
||||
"""Get the type's valid values for documentation"""
|
||||
"""Get the type's valid values for documentation."""
|
||||
return self.valid_values
|
||||
|
||||
def _basic_validation(self, value):
|
||||
|
@ -150,10 +150,10 @@ class KeyConfigParser(QObject):
|
||||
data = str(self)
|
||||
f.write(data)
|
||||
|
||||
@cmdutils.register(instance='key-config', maxsplit=1, no_cmd_split=True)
|
||||
@cmdutils.register(instance='key-config', maxsplit=1, no_cmd_split=True,
|
||||
no_replace_variables=True)
|
||||
@cmdutils.argument('win_id', win_id=True)
|
||||
@cmdutils.argument('key', completion=usertypes.Completion.empty)
|
||||
@cmdutils.argument('command', completion=usertypes.Completion.command)
|
||||
@cmdutils.argument('command', completion=usertypes.Completion.bind)
|
||||
def bind(self, key, win_id, command=None, *, mode='normal', force=False):
|
||||
"""Bind a key to a command.
|
||||
|
||||
@ -335,6 +335,7 @@ class KeyConfigParser(QObject):
|
||||
|
||||
def _validate_command(self, line):
|
||||
"""Check if a given command is valid."""
|
||||
from qutebrowser.config import config
|
||||
if line == self.UNBOUND_COMMAND:
|
||||
return
|
||||
commands = line.split(';;')
|
||||
@ -352,7 +353,8 @@ class KeyConfigParser(QObject):
|
||||
line))
|
||||
commands = [c.split(maxsplit=1)[0].strip() for c in commands]
|
||||
for cmd in commands:
|
||||
if cmd not in cmdutils.cmd_dict:
|
||||
aliases = config.section('aliases')
|
||||
if cmd not in cmdutils.cmd_dict and cmd not in aliases:
|
||||
raise KeyConfigError("Invalid command '{}'!".format(cmd))
|
||||
|
||||
def _read_command(self, line):
|
||||
@ -361,12 +363,12 @@ class KeyConfigParser(QObject):
|
||||
raise KeyConfigError("Got command '{}' without getting a "
|
||||
"section!".format(line))
|
||||
else:
|
||||
self._validate_command(line)
|
||||
for rgx, repl in configdata.CHANGED_KEY_COMMANDS:
|
||||
if rgx.match(line):
|
||||
line = rgx.sub(repl, line)
|
||||
self._mark_config_dirty()
|
||||
break
|
||||
self._validate_command(line)
|
||||
self._cur_command = line
|
||||
|
||||
def _read_keybinding(self, line):
|
||||
@ -422,8 +424,9 @@ class KeyConfigParser(QObject):
|
||||
|
||||
def get_reverse_bindings_for(self, section):
|
||||
"""Get a dict of commands to a list of bindings for the section."""
|
||||
cmd_to_keys = collections.defaultdict(list)
|
||||
cmd_to_keys = {}
|
||||
for key, cmd in self.get_bindings_for(section).items():
|
||||
cmd_to_keys.setdefault(cmd, [])
|
||||
# put special bindings last
|
||||
if utils.is_special_key(key):
|
||||
cmd_to_keys[cmd].append(key)
|
||||
|
35
qutebrowser/javascript/.eslintrc.yaml
Normal file
35
qutebrowser/javascript/.eslintrc.yaml
Normal file
@ -0,0 +1,35 @@
|
||||
env:
|
||||
browser: true
|
||||
|
||||
parserOptions:
|
||||
ecmaVersion: 3
|
||||
|
||||
extends:
|
||||
"eslint:all"
|
||||
|
||||
rules:
|
||||
strict: ["error", "global"]
|
||||
one-var: "off"
|
||||
padded-blocks: ["error", "never"]
|
||||
space-before-function-paren: ["error", "never"]
|
||||
no-underscore-dangle: "off"
|
||||
no-var: "off"
|
||||
vars-on-top: "off"
|
||||
newline-after-var: "off"
|
||||
camelcase: "off"
|
||||
require-jsdoc: "off"
|
||||
func-style: ["error", "declaration"]
|
||||
newline-before-return: "off"
|
||||
init-declarations: "off"
|
||||
no-plusplus: "off"
|
||||
no-extra-parens: off
|
||||
id-length: ["error", {"exceptions": ["i", "x", "y"]}]
|
||||
object-shorthand: "off"
|
||||
max-statements: ["error", {"max": 30}]
|
||||
quotes: ["error", "double", {"avoidEscape": true}]
|
||||
object-property-newline: ["error", {"allowMultiplePropertiesPerLine": true}]
|
||||
comma-dangle: ["error", "always-multiline"]
|
||||
no-magic-numbers: "off"
|
||||
no-undefined: "off"
|
||||
wrap-iife: ["error", "inside"]
|
||||
func-names: "off"
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Copyright 2015 Artur Shaik <ashaihullin@gmail.com>
|
||||
* Copyright 2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
* Copyright 2015-2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
*
|
||||
* This file is part of qutebrowser.
|
||||
*
|
||||
@ -32,79 +32,83 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
function isElementInViewport(node) {
|
||||
var i;
|
||||
var boundingRect = (node.getClientRects()[0] ||
|
||||
node.getBoundingClientRect());
|
||||
if (boundingRect.width <= 1 && boundingRect.height <= 1) {
|
||||
var rects = node.getClientRects();
|
||||
for (i = 0; i < rects.length; i++) {
|
||||
if (rects[i].width > rects[0].height &&
|
||||
rects[i].height > rects[0].height) {
|
||||
boundingRect = rects[i];
|
||||
(function() {
|
||||
function isElementInViewport(node) { // eslint-disable-line complexity
|
||||
var i;
|
||||
var boundingRect = (node.getClientRects()[0] ||
|
||||
node.getBoundingClientRect());
|
||||
|
||||
if (boundingRect.width <= 1 && boundingRect.height <= 1) {
|
||||
var rects = node.getClientRects();
|
||||
for (i = 0; i < rects.length; i++) {
|
||||
if (rects[i].width > rects[0].height &&
|
||||
rects[i].height > rects[0].height) {
|
||||
boundingRect = rects[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (boundingRect === undefined) {
|
||||
return null;
|
||||
}
|
||||
if (boundingRect.top > innerHeight || boundingRect.left > innerWidth) {
|
||||
return null;
|
||||
}
|
||||
if (boundingRect.width <= 1 || boundingRect.height <= 1) {
|
||||
var children = node.children;
|
||||
var visibleChildNode = false;
|
||||
for (i = 0; i < children.length; ++i) {
|
||||
boundingRect = (children[i].getClientRects()[0] ||
|
||||
children[i].getBoundingClientRect());
|
||||
if (boundingRect.width > 1 && boundingRect.height > 1) {
|
||||
visibleChildNode = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (visibleChildNode === false) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (boundingRect.top + boundingRect.height < 10 ||
|
||||
boundingRect.left + boundingRect.width < -10) {
|
||||
return null;
|
||||
}
|
||||
var computedStyle = window.getComputedStyle(node, null);
|
||||
if (computedStyle.visibility !== "visible" ||
|
||||
computedStyle.display === "none" ||
|
||||
node.hasAttribute("disabled") ||
|
||||
parseInt(computedStyle.width, 10) === 0 ||
|
||||
parseInt(computedStyle.height, 10) === 0) {
|
||||
return null;
|
||||
}
|
||||
return boundingRect.top >= -20;
|
||||
}
|
||||
if (boundingRect === undefined) {
|
||||
return null;
|
||||
}
|
||||
if (boundingRect.top > innerHeight || boundingRect.left > innerWidth) {
|
||||
return null;
|
||||
}
|
||||
if (boundingRect.width <= 1 || boundingRect.height <= 1) {
|
||||
var children = node.children;
|
||||
var visibleChildNode = false;
|
||||
var l = children.length;
|
||||
for (i = 0; i < l; ++i) {
|
||||
boundingRect = (children[i].getClientRects()[0] ||
|
||||
children[i].getBoundingClientRect());
|
||||
if (boundingRect.width > 1 && boundingRect.height > 1) {
|
||||
visibleChildNode = true;
|
||||
|
||||
function positionCaret() {
|
||||
var walker = document.createTreeWalker(document.body, 4, null);
|
||||
var node;
|
||||
var textNodes = [];
|
||||
var el;
|
||||
while ((node = walker.nextNode())) {
|
||||
if (node.nodeType === 3 && node.data.trim() !== "") {
|
||||
textNodes.push(node);
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < textNodes.length; i++) {
|
||||
var element = textNodes[i].parentElement;
|
||||
if (isElementInViewport(element.parentElement)) {
|
||||
el = element;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (visibleChildNode === false) {
|
||||
return null;
|
||||
if (el !== undefined) {
|
||||
var range = document.createRange();
|
||||
range.setStart(el, 0);
|
||||
range.setEnd(el, 0);
|
||||
var sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
}
|
||||
}
|
||||
if (boundingRect.top + boundingRect.height < 10 ||
|
||||
boundingRect.left + boundingRect.width < -10) {
|
||||
return null;
|
||||
}
|
||||
var computedStyle = window.getComputedStyle(node, null);
|
||||
if (computedStyle.visibility !== 'visible' ||
|
||||
computedStyle.display === 'none' ||
|
||||
node.hasAttribute('disabled') ||
|
||||
parseInt(computedStyle.width, 10) === 0 ||
|
||||
parseInt(computedStyle.height, 10) === 0) {
|
||||
return null;
|
||||
}
|
||||
return boundingRect.top >= -20;
|
||||
}
|
||||
|
||||
(function() {
|
||||
var walker = document.createTreeWalker(document.body, 4, null);
|
||||
var node;
|
||||
var textNodes = [];
|
||||
var el;
|
||||
while ((node = walker.nextNode())) {
|
||||
if (node.nodeType === 3 && node.data.trim() !== '') {
|
||||
textNodes.push(node);
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < textNodes.length; i++) {
|
||||
var element = textNodes[i].parentElement;
|
||||
if (isElementInViewport(element.parentElement)) {
|
||||
el = element;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (el !== undefined) {
|
||||
var range = document.createRange();
|
||||
range.setStart(el, 0);
|
||||
range.setEnd(el, 0);
|
||||
var sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
}
|
||||
positionCaret();
|
||||
})();
|
||||
|
@ -17,51 +17,59 @@
|
||||
* along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
function _qutebrowser_scroll_to_perc(x, y) {
|
||||
var elem = document.documentElement;
|
||||
var x_px = window.scrollX;
|
||||
var y_px = window.scrollY;
|
||||
"use strict";
|
||||
|
||||
if (x !== undefined) {
|
||||
x_px = (elem.scrollWidth - elem.clientWidth) / 100 * x;
|
||||
}
|
||||
window._qutebrowser.scroll = (function() {
|
||||
var funcs = {};
|
||||
|
||||
if (y !== undefined) {
|
||||
y_px = (elem.scrollHeight - elem.clientHeight) / 100 * y;
|
||||
}
|
||||
funcs.to_perc = function(x, y) {
|
||||
var elem = document.documentElement;
|
||||
var x_px = window.scrollX;
|
||||
var y_px = window.scrollY;
|
||||
|
||||
window.scroll(x_px, y_px);
|
||||
}
|
||||
if (x !== undefined) {
|
||||
x_px = (elem.scrollWidth - elem.clientWidth) / 100 * x;
|
||||
}
|
||||
|
||||
function _qutebrowser_scroll_delta_page(x, y) {
|
||||
var dx = document.documentElement.clientWidth * x;
|
||||
var dy = document.documentElement.clientHeight * y;
|
||||
window.scrollBy(dx, dy);
|
||||
}
|
||||
if (y !== undefined) {
|
||||
y_px = (elem.scrollHeight - elem.clientHeight) / 100 * y;
|
||||
}
|
||||
|
||||
function _qutebrowser_scroll_pos() {
|
||||
var elem = document.documentElement;
|
||||
var dx = (elem.scrollWidth - elem.clientWidth);
|
||||
var dy = (elem.scrollHeight - elem.clientHeight);
|
||||
window.scroll(x_px, y_px);
|
||||
};
|
||||
|
||||
var perc_x, perc_y;
|
||||
funcs.delta_page = function(x, y) {
|
||||
var dx = document.documentElement.clientWidth * x;
|
||||
var dy = document.documentElement.clientHeight * y;
|
||||
window.scrollBy(dx, dy);
|
||||
};
|
||||
|
||||
if (dx === 0) {
|
||||
perc_x = 0;
|
||||
} else {
|
||||
perc_x = 100 / dx * window.scrollX;
|
||||
}
|
||||
funcs.pos = function() {
|
||||
var elem = document.documentElement;
|
||||
var dx = elem.scrollWidth - elem.clientWidth;
|
||||
var dy = elem.scrollHeight - elem.clientHeight;
|
||||
var perc_x, perc_y;
|
||||
|
||||
if (dy === 0) {
|
||||
perc_y = 0;
|
||||
} else {
|
||||
perc_y = 100 / dy * window.scrollY;
|
||||
}
|
||||
if (dx === 0) {
|
||||
perc_x = 0;
|
||||
} else {
|
||||
perc_x = 100 / dx * window.scrollX;
|
||||
}
|
||||
|
||||
var pos_perc = {'x': perc_x, 'y': perc_y};
|
||||
var pos_px = {'x': window.scrollX, 'y': window.scrollY};
|
||||
var pos = {'perc': pos_perc, 'px': pos_px};
|
||||
if (dy === 0) {
|
||||
perc_y = 0;
|
||||
} else {
|
||||
perc_y = 100 / dy * window.scrollY;
|
||||
}
|
||||
|
||||
// console.log(JSON.stringify(pos));
|
||||
return pos;
|
||||
}
|
||||
var pos = {
|
||||
"perc": {"x": perc_x, "y": perc_y},
|
||||
"px": {"x": window.scrollX, "y": window.scrollY},
|
||||
};
|
||||
|
||||
// console.log(JSON.stringify(pos));
|
||||
return pos;
|
||||
};
|
||||
|
||||
return funcs;
|
||||
})();
|
||||
|
80
qutebrowser/javascript/webelem.js
Normal file
80
qutebrowser/javascript/webelem.js
Normal file
@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Copyright 2016 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/>.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
window._qutebrowser.webelem = (function() {
|
||||
var funcs = {};
|
||||
var elements = [];
|
||||
|
||||
function serialize_elem(elem, id) {
|
||||
var out = {
|
||||
"id": id,
|
||||
"text": elem.text,
|
||||
"tag_name": elem.tagName,
|
||||
"outer_xml": elem.outerHTML,
|
||||
};
|
||||
|
||||
var attributes = {};
|
||||
for (var i = 0; i < elem.attributes.length; ++i) {
|
||||
var attr = elem.attributes[i];
|
||||
attributes[attr.name] = attr.value;
|
||||
}
|
||||
out.attributes = attributes;
|
||||
|
||||
// console.log(JSON.stringify(out));
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
funcs.find_all = function(selector) {
|
||||
var elems = document.querySelectorAll(selector);
|
||||
var out = [];
|
||||
var id = elements.length;
|
||||
|
||||
for (var i = 0; i < elems.length; ++i) {
|
||||
var elem = elems[i];
|
||||
out.push(serialize_elem(elem, id));
|
||||
elements[id] = elem;
|
||||
id++;
|
||||
}
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
funcs.focus_element = function() {
|
||||
var elem = document.activeElement;
|
||||
|
||||
if (!elem || elem === document.body) {
|
||||
// "When there is no selection, the active element is the page's
|
||||
// <body> or null."
|
||||
return null;
|
||||
}
|
||||
|
||||
var id = elements.length;
|
||||
elements[id] = elem;
|
||||
return serialize_elem(elem, id);
|
||||
};
|
||||
|
||||
funcs.set_text = function(id, text) {
|
||||
elements[id].value = text;
|
||||
};
|
||||
|
||||
return funcs;
|
||||
})();
|
@ -189,7 +189,7 @@ class HintKeyParser(keyparser.CommandKeyParser):
|
||||
return True
|
||||
else:
|
||||
return super()._handle_special_key(e)
|
||||
elif config.get('hints', 'mode') != 'number':
|
||||
elif hintmanager.current_mode() != 'number':
|
||||
return super()._handle_special_key(e)
|
||||
elif not e.text():
|
||||
return super()._handle_special_key(e)
|
||||
|
@ -69,7 +69,13 @@ def get_window(via_ipc, force_window=False, force_tab=False,
|
||||
window_to_raise = window
|
||||
else:
|
||||
try:
|
||||
window = objreg.last_window()
|
||||
win_mode = config.get('general', 'new-instance-open-target.window')
|
||||
if win_mode == 'last-focused':
|
||||
window = objreg.last_focused_window()
|
||||
elif win_mode == 'last-opened':
|
||||
window = objreg.last_window()
|
||||
elif win_mode == 'last-visible':
|
||||
window = objreg.last_visible_window()
|
||||
except objreg.NoWindow:
|
||||
# There is no window left, so we open a new one
|
||||
window = MainWindow()
|
||||
@ -175,9 +181,6 @@ class MainWindow(QWidget):
|
||||
QTimer.singleShot(0, self._connect_resize_keyhint)
|
||||
objreg.get('config').changed.connect(self.on_config_changed)
|
||||
|
||||
if config.get('ui', 'hide-mouse-cursor'):
|
||||
self.setCursor(Qt.BlankCursor)
|
||||
|
||||
objreg.get("app").new_window.emit(self)
|
||||
|
||||
def _init_downloadmanager(self):
|
||||
@ -457,8 +460,23 @@ class MainWindow(QWidget):
|
||||
self._downloadview.updateGeometry()
|
||||
self.tabbed_browser.tabBar().refresh()
|
||||
|
||||
def showEvent(self, e):
|
||||
"""Extend showEvent to register us as the last-visible-main-window.
|
||||
|
||||
Args:
|
||||
e: The QShowEvent
|
||||
"""
|
||||
super().showEvent(e)
|
||||
objreg.register('last-visible-main-window', self, update=True)
|
||||
|
||||
def _do_close(self):
|
||||
"""Helper function for closeEvent."""
|
||||
last_visible = objreg.get('last-visible-main-window')
|
||||
if self is last_visible:
|
||||
try:
|
||||
objreg.delete('last-visible-main-window')
|
||||
except KeyError:
|
||||
pass
|
||||
objreg.get('session-manager').save_last_window_session()
|
||||
self._save_geometry()
|
||||
log.destroy.debug("Closing window {}".format(self.win_id))
|
||||
|
@ -23,8 +23,8 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QSize
|
||||
from PyQt5.QtWidgets import QSizePolicy
|
||||
|
||||
from qutebrowser.keyinput import modeman, modeparsers
|
||||
from qutebrowser.commands import cmdexc, cmdutils, runners
|
||||
from qutebrowser.misc import cmdhistory, split
|
||||
from qutebrowser.commands import cmdexc, cmdutils
|
||||
from qutebrowser.misc import cmdhistory
|
||||
from qutebrowser.misc import miscwidgets as misc
|
||||
from qutebrowser.utils import usertypes, log, objreg
|
||||
|
||||
@ -108,10 +108,6 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
||||
space: If given, a space is added to the end.
|
||||
append: If given, the text is appended to the current text.
|
||||
"""
|
||||
args = split.simple_split(text)
|
||||
args = runners.replace_variables(self._win_id, args)
|
||||
text = ' '.join(args)
|
||||
|
||||
if space:
|
||||
text += ' '
|
||||
if append:
|
||||
|
@ -34,7 +34,7 @@ from qutebrowser.utils import (log, usertypes, utils, qtutils, objreg,
|
||||
urlutils, message)
|
||||
|
||||
|
||||
UndoEntry = collections.namedtuple('UndoEntry', ['url', 'history'])
|
||||
UndoEntry = collections.namedtuple('UndoEntry', ['url', 'history', 'index'])
|
||||
|
||||
|
||||
class TabDeletedError(Exception):
|
||||
@ -198,6 +198,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
tab.window_close_requested.connect(
|
||||
functools.partial(self.on_window_close_requested, tab))
|
||||
tab.new_tab_requested.connect(self.tabopen)
|
||||
tab.add_history_item.connect(objreg.get('web-history').add_from_tab)
|
||||
|
||||
def current_url(self):
|
||||
"""Get the URL of the current tab.
|
||||
@ -260,7 +261,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
window=self._win_id)
|
||||
if tab.url().isValid():
|
||||
history_data = tab.history.serialize()
|
||||
entry = UndoEntry(tab.url(), history_data)
|
||||
entry = UndoEntry(tab.url(), history_data, idx)
|
||||
self._undo_stack.append(entry)
|
||||
elif tab.url().isEmpty():
|
||||
# There are some good reasons why a URL could be empty
|
||||
@ -297,13 +298,13 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
use_current_tab = (only_one_tab_open and no_history and
|
||||
last_close_url_used)
|
||||
|
||||
url, history_data = self._undo_stack.pop()
|
||||
url, history_data, idx = self._undo_stack.pop()
|
||||
|
||||
if use_current_tab:
|
||||
self.openurl(url, newtab=False)
|
||||
newtab = self.widget(0)
|
||||
else:
|
||||
newtab = self.tabopen(url, background=False)
|
||||
newtab = self.tabopen(url, background=False, idx=idx)
|
||||
|
||||
newtab.history.deserialize(history_data)
|
||||
|
||||
@ -342,7 +343,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
|
||||
@pyqtSlot('QUrl')
|
||||
@pyqtSlot('QUrl', bool)
|
||||
def tabopen(self, url=None, background=None, explicit=False):
|
||||
def tabopen(self, url=None, background=None, explicit=False, idx=None):
|
||||
"""Open a new tab with a given URL.
|
||||
|
||||
Inner logic for open-tab and open-tab-bg.
|
||||
@ -358,6 +359,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
- Tabs from clicked links etc. are to the right of
|
||||
the current.
|
||||
- Explicitly opened tabs are at the very right.
|
||||
idx: The index where the new tab should be opened.
|
||||
|
||||
Return:
|
||||
The opened WebView instance.
|
||||
@ -376,7 +378,8 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
tab = browsertab.create(win_id=self._win_id, parent=self)
|
||||
self._connect_tab_signals(tab)
|
||||
|
||||
idx = self._get_new_tab_idx(explicit)
|
||||
if idx is None:
|
||||
idx = self._get_new_tab_idx(explicit)
|
||||
self.insertTab(idx, tab, "")
|
||||
|
||||
if url is not None:
|
||||
|
@ -60,11 +60,11 @@ def _missing_str(name, *, windows=None, pip=None, webengine=False):
|
||||
blocks.append('<br />'.join(lines))
|
||||
if webengine:
|
||||
lines = [
|
||||
'Note QtWebEngine is not available for some distributions '
|
||||
('Note QtWebEngine is not available for some distributions '
|
||||
'(like Debian/Ubuntu), so you need to start without '
|
||||
'--backend webengine there.',
|
||||
'QtWebEngine is currently unsupported with the OS X .app, see '
|
||||
'https://github.com/The-Compiler/qutebrowser/issues/1692',
|
||||
'--backend webengine there.'),
|
||||
('QtWebEngine is currently unsupported with the OS X .app, see '
|
||||
'https://github.com/The-Compiler/qutebrowser/issues/1692'),
|
||||
]
|
||||
else:
|
||||
lines = ['<b>If you installed a qutebrowser package for your '
|
||||
@ -301,6 +301,13 @@ def init_log(args):
|
||||
log.init.debug("Log initialized.")
|
||||
|
||||
|
||||
def check_optimize_flag():
|
||||
from qutebrowser.utils import log
|
||||
if sys.flags.optimize >= 2:
|
||||
log.init.warning("Running on optimize level higher than 1, "
|
||||
"unexpected behavior may occur.")
|
||||
|
||||
|
||||
def earlyinit(args):
|
||||
"""Do all needed early initialization.
|
||||
|
||||
@ -327,3 +334,4 @@ def earlyinit(args):
|
||||
remove_inputhook()
|
||||
check_libraries(args)
|
||||
check_ssl_support()
|
||||
check_optimize_flag()
|
||||
|
@ -35,8 +35,8 @@ class ExternalEditor(QObject):
|
||||
|
||||
Attributes:
|
||||
_text: The current text before the editor is opened.
|
||||
_oshandle: The OS level handle to the tmpfile.
|
||||
_filehandle: The file handle to the tmpfile.
|
||||
_file: The file handle as tempfile.NamedTemporaryFile. Note that this
|
||||
handle will be closed after the initial file has been created.
|
||||
_proc: The GUIProcess of the editor.
|
||||
_win_id: The window ID the ExternalEditor is associated with.
|
||||
"""
|
||||
@ -46,20 +46,18 @@ class ExternalEditor(QObject):
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
self._text = None
|
||||
self._oshandle = None
|
||||
self._filename = None
|
||||
self._file = None
|
||||
self._proc = None
|
||||
self._win_id = win_id
|
||||
|
||||
def _cleanup(self):
|
||||
"""Clean up temporary files after the editor closed."""
|
||||
if self._oshandle is None or self._filename is None:
|
||||
if self._file is None:
|
||||
# Could not create initial file.
|
||||
return
|
||||
try:
|
||||
os.close(self._oshandle)
|
||||
if self._proc.exit_status() != QProcess.CrashExit:
|
||||
os.remove(self._filename)
|
||||
os.remove(self._file.name)
|
||||
except OSError as e:
|
||||
# NOTE: Do not replace this with "raise CommandError" as it's
|
||||
# executed async.
|
||||
@ -82,7 +80,7 @@ class ExternalEditor(QObject):
|
||||
return
|
||||
encoding = config.get('general', 'editor-encoding')
|
||||
try:
|
||||
with open(self._filename, 'r', encoding=encoding) as f:
|
||||
with open(self._file.name, 'r', encoding=encoding) as f:
|
||||
text = f.read()
|
||||
except OSError as e:
|
||||
# NOTE: Do not replace this with "raise CommandError" as it's
|
||||
@ -108,13 +106,18 @@ class ExternalEditor(QObject):
|
||||
if self._text is not None:
|
||||
raise ValueError("Already editing a file!")
|
||||
self._text = text
|
||||
encoding = config.get('general', 'editor-encoding')
|
||||
try:
|
||||
self._oshandle, self._filename = tempfile.mkstemp(
|
||||
text=True, prefix='qutebrowser-editor-')
|
||||
if text:
|
||||
encoding = config.get('general', 'editor-encoding')
|
||||
with open(self._filename, 'w', encoding=encoding) as f:
|
||||
f.write(text)
|
||||
# Close while the external process is running, as otherwise systems
|
||||
# with exclusive write access (e.g. Windows) may fail to update
|
||||
# the file from the external editor, see
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/1767
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode='w', prefix='qutebrowser-editor-', encoding=encoding,
|
||||
delete=False) as fobj:
|
||||
if text:
|
||||
fobj.write(text)
|
||||
self._file = fobj
|
||||
except OSError as e:
|
||||
message.error(self._win_id, "Failed to create initial file: "
|
||||
"{}".format(e))
|
||||
@ -125,6 +128,6 @@ class ExternalEditor(QObject):
|
||||
self._proc.error.connect(self.on_proc_error)
|
||||
editor = config.get('general', 'editor')
|
||||
executable = editor[0]
|
||||
args = [arg.replace('{}', self._filename) 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))
|
||||
self._proc.start(executable, args)
|
||||
|
@ -47,7 +47,7 @@ class MinimalLineEditMixin:
|
||||
if e.key() == Qt.Key_Insert and e.modifiers() == Qt.ShiftModifier:
|
||||
try:
|
||||
text = utils.get_clipboard(selection=True)
|
||||
except utils.SelectionUnsupportedError:
|
||||
except utils.ClipboardError:
|
||||
pass
|
||||
else:
|
||||
e.accept()
|
||||
|
@ -39,7 +39,7 @@ from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtWidgets import QApplication # pylint: disable=unused-import
|
||||
|
||||
|
||||
@cmdutils.register(maxsplit=1, no_cmd_split=True)
|
||||
@cmdutils.register(maxsplit=1, no_cmd_split=True, no_replace_variables=True)
|
||||
@cmdutils.argument('win_id', win_id=True)
|
||||
def later(ms: int, command, win_id):
|
||||
"""Execute a command after some time.
|
||||
@ -69,7 +69,7 @@ def later(ms: int, command, win_id):
|
||||
raise
|
||||
|
||||
|
||||
@cmdutils.register(maxsplit=1, no_cmd_split=True)
|
||||
@cmdutils.register(maxsplit=1, no_cmd_split=True, no_replace_variables=True)
|
||||
@cmdutils.argument('win_id', win_id=True)
|
||||
def repeat(times: int, command, win_id):
|
||||
"""Repeat a given command.
|
||||
|
@ -26,7 +26,7 @@ import os.path
|
||||
import collections
|
||||
|
||||
import qutebrowser
|
||||
from qutebrowser.utils import usertypes
|
||||
from qutebrowser.utils import usertypes, log, utils
|
||||
|
||||
|
||||
def is_git_repo():
|
||||
@ -98,6 +98,15 @@ class DocstringParser:
|
||||
self.State.arg_inside: self._parse_arg_inside,
|
||||
self.State.misc: self._skip,
|
||||
}
|
||||
if doc is None:
|
||||
if sys.flags.optimize < 2:
|
||||
log.commands.warning(
|
||||
"Function {}() from {} has no docstring".format(
|
||||
utils.qualname(func),
|
||||
inspect.getsourcefile(func)))
|
||||
self.long_desc = ""
|
||||
self.short_desc = ""
|
||||
return
|
||||
for line in doc.splitlines():
|
||||
handler = handlers[self._state]
|
||||
stop = handler(line)
|
||||
|
@ -20,9 +20,6 @@
|
||||
"""Utilities related to javascript interaction."""
|
||||
|
||||
|
||||
from qutebrowser.utils import utils
|
||||
|
||||
|
||||
def string_escape(text):
|
||||
"""Escape values special to javascript in strings.
|
||||
|
||||
@ -55,18 +52,19 @@ def _convert_js_arg(arg):
|
||||
return 'undefined'
|
||||
elif isinstance(arg, str):
|
||||
return '"{}"'.format(string_escape(arg))
|
||||
elif isinstance(arg, int):
|
||||
elif isinstance(arg, (int, float)):
|
||||
return str(arg)
|
||||
else:
|
||||
raise TypeError("Don't know how to handle {!r} of type {}!".format(
|
||||
arg, type(arg).__name__))
|
||||
|
||||
|
||||
def assemble(name, function, *args):
|
||||
def assemble(module, function, *args):
|
||||
"""Assemble a javascript file and a function call."""
|
||||
code = "{code}\n_qutebrowser_{function}({args});".format(
|
||||
code=utils.read_file('javascript/{}.js'.format(name)),
|
||||
function=function,
|
||||
args=', '.join(_convert_js_arg(arg) for arg in args),
|
||||
)
|
||||
js_args = ', '.join(_convert_js_arg(arg) for arg in args)
|
||||
if module == 'window':
|
||||
parts = ['window', function]
|
||||
else:
|
||||
parts = ['window', '_qutebrowser', module, function]
|
||||
code = '"use strict";\n{}({});'.format('.'.join(parts), js_args)
|
||||
return code
|
||||
|
@ -94,8 +94,15 @@ class ObjectRegistry(collections.UserDict):
|
||||
|
||||
def _disconnect_destroyed(self, name):
|
||||
"""Disconnect the destroyed slot if it was connected."""
|
||||
if name in self._partial_objs:
|
||||
func = self._partial_objs[name]
|
||||
try:
|
||||
partial_objs = self._partial_objs
|
||||
except AttributeError:
|
||||
# This sometimes seems to happen on Travis during
|
||||
# test_history.test_adding_item_during_async_read
|
||||
# and I have no idea why...
|
||||
return
|
||||
if name in partial_objs:
|
||||
func = partial_objs[name]
|
||||
try:
|
||||
self[name].destroyed.disconnect(func)
|
||||
except (RuntimeError, TypeError):
|
||||
@ -106,7 +113,7 @@ class ObjectRegistry(collections.UserDict):
|
||||
# pyqtSignal must be bound to a QObject" instead:
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/257
|
||||
pass
|
||||
del self._partial_objs[name]
|
||||
del partial_objs[name]
|
||||
|
||||
def on_destroyed(self, name):
|
||||
"""Schedule removing of a destroyed QObject.
|
||||
@ -121,6 +128,11 @@ class ObjectRegistry(collections.UserDict):
|
||||
def _on_destroyed(self, name):
|
||||
"""Remove a destroyed QObject."""
|
||||
log.destroy.debug("removed: {}".format(name))
|
||||
if not hasattr(self, 'data'):
|
||||
# This sometimes seems to happen on Travis during
|
||||
# test_history.test_adding_item_during_async_read
|
||||
# and I have no idea why...
|
||||
return
|
||||
try:
|
||||
del self[name]
|
||||
del self._partial_objs[name]
|
||||
@ -178,10 +190,7 @@ def _get_window_registry(window):
|
||||
app = get('app')
|
||||
win = app.activeWindow()
|
||||
elif window == 'last-focused':
|
||||
try:
|
||||
win = get('last-focused-main-window')
|
||||
except KeyError:
|
||||
win = last_window()
|
||||
win = last_focused_window()
|
||||
else:
|
||||
win = window_registry[window]
|
||||
except (KeyError, NoWindow):
|
||||
@ -276,6 +285,22 @@ def dump_objects():
|
||||
return lines
|
||||
|
||||
|
||||
def last_visible_window():
|
||||
"""Get the last visible window, or the last focused window if none."""
|
||||
try:
|
||||
return get('last-visible-main-window')
|
||||
except KeyError:
|
||||
return last_focused_window()
|
||||
|
||||
|
||||
def last_focused_window():
|
||||
"""Get the last focused window, or the last window if none."""
|
||||
try:
|
||||
return get('last-focused-main-window')
|
||||
except KeyError:
|
||||
return last_window()
|
||||
|
||||
|
||||
def last_window():
|
||||
"""Get the last opened window object."""
|
||||
if not window_registry:
|
||||
|
@ -239,7 +239,7 @@ KeyMode = enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt',
|
||||
Completion = enum('Completion', ['command', 'section', 'option', 'value',
|
||||
'helptopic', 'quickmark_by_name',
|
||||
'bookmark_by_url', 'url', 'tab', 'sessions',
|
||||
'empty'])
|
||||
'bind'])
|
||||
|
||||
|
||||
# Exit statuses for errors. Needs to be an int for sys.exit.
|
||||
|
@ -43,11 +43,21 @@ fake_clipboard = None
|
||||
log_clipboard = False
|
||||
|
||||
|
||||
class SelectionUnsupportedError(Exception):
|
||||
class ClipboardError(Exception):
|
||||
|
||||
"""Raised if the clipboard contents are unavailable for some reason."""
|
||||
|
||||
|
||||
class SelectionUnsupportedError(ClipboardError):
|
||||
|
||||
"""Raised if [gs]et_clipboard is used and selection=True is unsupported."""
|
||||
|
||||
|
||||
class ClipboardEmptyError(ClipboardError):
|
||||
|
||||
"""Raised if get_clipboard is used and the clipboard is empty."""
|
||||
|
||||
|
||||
def elide(text, length):
|
||||
"""Elide text so it uses a maximum of length chars."""
|
||||
if length < 1:
|
||||
@ -810,6 +820,11 @@ def get_clipboard(selection=False):
|
||||
mode = QClipboard.Selection if selection else QClipboard.Clipboard
|
||||
data = QApplication.clipboard().text(mode=mode)
|
||||
|
||||
target = "Primary selection" if selection else "Clipboard"
|
||||
if not data.strip():
|
||||
raise ClipboardEmptyError("{} is empty.".format(target))
|
||||
log.misc.debug("{} contained: {!r}".format(target, data))
|
||||
|
||||
return data
|
||||
|
||||
|
||||
|
@ -29,10 +29,14 @@ import importlib
|
||||
import collections
|
||||
|
||||
from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, qVersion
|
||||
from PyQt5.QtWebKit import qWebKitVersion
|
||||
from PyQt5.QtNetwork import QSslSocket
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
try:
|
||||
from PyQt5.QtWebKit import qWebKitVersion
|
||||
except ImportError: # pragma: no cover
|
||||
qWebKitVersion = None
|
||||
|
||||
import qutebrowser
|
||||
from qutebrowser.utils import log, utils
|
||||
from qutebrowser.browser import pdfjs
|
||||
@ -225,9 +229,14 @@ def version():
|
||||
|
||||
lines += _module_versions()
|
||||
|
||||
lines += ['pdf.js: {}'.format(_pdfjs_version())]
|
||||
|
||||
if qWebKitVersion is None:
|
||||
lines.append('Webkit: no')
|
||||
else:
|
||||
lines.append('Webkit: {}'.format(qWebKitVersion()))
|
||||
|
||||
lines += [
|
||||
'pdf.js: {}'.format(_pdfjs_version()),
|
||||
'Webkit: {}'.format(qWebKitVersion()),
|
||||
'Harfbuzz: {}'.format(os.environ.get('QT_HARFBUZZ', 'system')),
|
||||
'SSL: {}'.format(QSslSocket.sslLibraryVersionString()),
|
||||
'',
|
||||
|
@ -52,7 +52,9 @@ PERFECT_FILES = [
|
||||
('tests/unit/browser/webkit/test_cookies.py',
|
||||
'qutebrowser/browser/webkit/cookies.py'),
|
||||
('tests/unit/browser/webkit/test_history.py',
|
||||
'qutebrowser/browser/webkit/history.py'),
|
||||
'qutebrowser/browser/history.py'),
|
||||
('tests/unit/browser/webkit/test_history.py',
|
||||
'qutebrowser/browser/webkit/webkithistory.py'),
|
||||
('tests/unit/browser/webkit/test_tabhistory.py',
|
||||
'qutebrowser/browser/webkit/tabhistory.py'),
|
||||
('tests/unit/browser/webkit/http/test_http.py',
|
||||
@ -60,7 +62,9 @@ PERFECT_FILES = [
|
||||
('tests/unit/browser/webkit/http/test_content_disposition.py',
|
||||
'qutebrowser/browser/webkit/rfc6266.py'),
|
||||
('tests/unit/browser/webkit/test_webelem.py',
|
||||
'qutebrowser/browser/webkit/webelem.py'),
|
||||
'qutebrowser/browser/webkit/webkitelem.py'),
|
||||
('tests/unit/browser/webkit/test_webelem.py',
|
||||
'qutebrowser/browser/webelem.py'),
|
||||
('tests/unit/browser/webkit/network/test_schemehandler.py',
|
||||
'qutebrowser/browser/webkit/network/schemehandler.py'),
|
||||
('tests/unit/browser/webkit/network/test_filescheme.py',
|
||||
@ -150,12 +154,14 @@ PERFECT_FILES = [
|
||||
|
||||
('tests/unit/completion/test_models.py',
|
||||
'qutebrowser/completion/models/base.py'),
|
||||
('tests/unit/completion/test_sortfilter.py',
|
||||
'qutebrowser/completion/models/sortfilter.py'),
|
||||
|
||||
]
|
||||
|
||||
|
||||
# 100% coverage because of end2end tests, but no perfect unit tests yet.
|
||||
WHITELISTED_FILES = []
|
||||
WHITELISTED_FILES = ['qutebrowser/browser/webkit/webkitinspector.py']
|
||||
|
||||
|
||||
class Skipped(Exception):
|
||||
|
@ -42,6 +42,13 @@ from qutebrowser.commands import cmdutils, argparser
|
||||
from qutebrowser.config import configdata
|
||||
from qutebrowser.utils import docutils, usertypes
|
||||
|
||||
FILE_HEADER = """
|
||||
// DO NOT EDIT THIS FILE DIRECTLY!
|
||||
// It is autogenerated from docstrings by running:
|
||||
// $ python3 scripts/dev/src2asciidoc.py
|
||||
|
||||
""".lstrip()
|
||||
|
||||
|
||||
class UsageFormatter(argparse.HelpFormatter):
|
||||
|
||||
@ -239,7 +246,8 @@ def _get_command_doc_notes(cmd):
|
||||
Yield:
|
||||
Strings which should be added to the docs.
|
||||
"""
|
||||
if cmd.maxsplit is not None or cmd.no_cmd_split:
|
||||
if (cmd.maxsplit is not None or cmd.no_cmd_split or
|
||||
cmd.no_replace_variables and cmd.name != "spawn"):
|
||||
yield ""
|
||||
yield "==== note"
|
||||
if cmd.maxsplit is not None:
|
||||
@ -248,6 +256,8 @@ def _get_command_doc_notes(cmd):
|
||||
if cmd.no_cmd_split:
|
||||
yield ("* With this command, +;;+ is interpreted literally "
|
||||
"instead of splitting off a second command.")
|
||||
if cmd.no_replace_variables and cmd.name != "spawn":
|
||||
yield r"* This command does not replace variables like +\{url\}+."
|
||||
|
||||
|
||||
def _get_action_metavar(action, nargs=1):
|
||||
@ -309,6 +319,7 @@ def _format_action(action):
|
||||
def generate_commands(filename):
|
||||
"""Generate the complete commands section."""
|
||||
with _open_file(filename) as f:
|
||||
f.write(FILE_HEADER)
|
||||
f.write("= Commands\n")
|
||||
normal_cmds = []
|
||||
hidden_cmds = []
|
||||
@ -392,6 +403,7 @@ def _generate_setting_section(f, sectname, sect):
|
||||
def generate_settings(filename):
|
||||
"""Generate the complete settings section."""
|
||||
with _open_file(filename) as f:
|
||||
f.write(FILE_HEADER)
|
||||
f.write("= Settings\n")
|
||||
f.write(_get_setting_quickref() + "\n")
|
||||
for sectname, sect in configdata.DATA.items():
|
||||
|
@ -10,7 +10,7 @@ Feature: Caret mode
|
||||
Scenario: Selecting the entire document
|
||||
When I run :toggle-selection
|
||||
And I run :move-to-end-of-document
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain:
|
||||
one two three
|
||||
eins zwei drei
|
||||
@ -23,14 +23,14 @@ Feature: Caret mode
|
||||
And I run :move-to-start-of-document
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-end-of-word
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "one"
|
||||
|
||||
Scenario: Moving to end and to start of document (with selection)
|
||||
When I run :move-to-end-of-document
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-start-of-document
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain:
|
||||
one two three
|
||||
eins zwei drei
|
||||
@ -43,7 +43,7 @@ Feature: Caret mode
|
||||
Scenario: Selecting a block
|
||||
When I run :toggle-selection
|
||||
And I run :move-to-end-of-next-block
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain:
|
||||
one two three
|
||||
eins zwei drei
|
||||
@ -53,7 +53,7 @@ Feature: Caret mode
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-end-of-prev-block
|
||||
And I run :move-to-prev-word
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain:
|
||||
drei
|
||||
|
||||
@ -64,14 +64,14 @@ Feature: Caret mode
|
||||
And I run :move-to-end-of-prev-block
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-prev-word
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "drei"
|
||||
|
||||
Scenario: Moving back to the start of previous block (with selection)
|
||||
When I run :move-to-end-of-next-block with count 2
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-start-of-prev-block
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain:
|
||||
eins zwei drei
|
||||
|
||||
@ -82,20 +82,20 @@ Feature: Caret mode
|
||||
And I run :move-to-start-of-prev-block
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-next-word
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "eins "
|
||||
|
||||
Scenario: Moving to the start of next block (with selection)
|
||||
When I run :toggle-selection
|
||||
And I run :move-to-start-of-next-block
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "one two three\n"
|
||||
|
||||
Scenario: Moving to the start of next block
|
||||
When I run :move-to-start-of-next-block
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-end-of-word
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "eins"
|
||||
|
||||
# line
|
||||
@ -103,20 +103,20 @@ Feature: Caret mode
|
||||
Scenario: Selecting a line
|
||||
When I run :toggle-selection
|
||||
And I run :move-to-end-of-line
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "one two three"
|
||||
|
||||
Scenario: Moving and selecting a line
|
||||
When I run :move-to-next-line
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-end-of-line
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "eins zwei drei"
|
||||
|
||||
Scenario: Selecting next line
|
||||
When I run :toggle-selection
|
||||
And I run :move-to-next-line
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "one two three\n"
|
||||
|
||||
Scenario: Moving to end and to start of line
|
||||
@ -124,21 +124,21 @@ Feature: Caret mode
|
||||
And I run :move-to-start-of-line
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-end-of-word
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "one"
|
||||
|
||||
Scenario: Selecting a line (backwards)
|
||||
When I run :move-to-end-of-line
|
||||
And I run :toggle-selection
|
||||
When I run :move-to-start-of-line
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "one two three"
|
||||
|
||||
Scenario: Selecting previous line
|
||||
When I run :move-to-next-line
|
||||
And I run :toggle-selection
|
||||
When I run :move-to-prev-line
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "one two three\n"
|
||||
|
||||
Scenario: Moving to previous line
|
||||
@ -146,7 +146,7 @@ Feature: Caret mode
|
||||
When I run :move-to-prev-line
|
||||
And I run :toggle-selection
|
||||
When I run :move-to-next-line
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "one two three\n"
|
||||
|
||||
# word
|
||||
@ -154,35 +154,35 @@ Feature: Caret mode
|
||||
Scenario: Selecting a word
|
||||
When I run :toggle-selection
|
||||
And I run :move-to-end-of-word
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "one"
|
||||
|
||||
Scenario: Moving to end and selecting a word
|
||||
When I run :move-to-end-of-word
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-end-of-word
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain " two"
|
||||
|
||||
Scenario: Moving to next word and selecting a word
|
||||
When I run :move-to-next-word
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-end-of-word
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "two"
|
||||
|
||||
Scenario: Moving to next word and selecting until next word
|
||||
When I run :move-to-next-word
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-next-word
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "two "
|
||||
|
||||
Scenario: Moving to previous word and selecting a word
|
||||
When I run :move-to-end-of-word
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-prev-word
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "one"
|
||||
|
||||
Scenario: Moving to previous word
|
||||
@ -190,7 +190,7 @@ Feature: Caret mode
|
||||
And I run :move-to-prev-word
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-end-of-word
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "one"
|
||||
|
||||
# char
|
||||
@ -198,21 +198,21 @@ Feature: Caret mode
|
||||
Scenario: Selecting a char
|
||||
When I run :toggle-selection
|
||||
And I run :move-to-next-char
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "o"
|
||||
|
||||
Scenario: Moving and selecting a char
|
||||
When I run :move-to-next-char
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-next-char
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "n"
|
||||
|
||||
Scenario: Selecting previous char
|
||||
When I run :move-to-end-of-word
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-prev-char
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "e"
|
||||
|
||||
Scenario: Moving to previous char
|
||||
@ -220,41 +220,41 @@ Feature: Caret mode
|
||||
And I run :move-to-prev-char
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-end-of-word
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "e"
|
||||
|
||||
# :yank-selected
|
||||
# :yank selection
|
||||
|
||||
Scenario: :yank-selected without selection
|
||||
When I run :yank-selected
|
||||
Scenario: :yank selection without selection
|
||||
When I run :yank selection
|
||||
Then the message "Nothing to yank" should be shown.
|
||||
|
||||
Scenario: :yank-selected message
|
||||
Scenario: :yank selection message
|
||||
When I run :toggle-selection
|
||||
And I run :move-to-end-of-word
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the message "3 chars yanked to clipboard" should be shown.
|
||||
|
||||
Scenario: :yank-selected message with one char
|
||||
Scenario: :yank selection message with one char
|
||||
When I run :toggle-selection
|
||||
And I run :move-to-next-char
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the message "1 char yanked to clipboard" should be shown.
|
||||
|
||||
Scenario: :yank-selected with primary selection
|
||||
Scenario: :yank selection with primary selection
|
||||
When selection is supported
|
||||
And I run :toggle-selection
|
||||
And I run :move-to-end-of-word
|
||||
And I run :yank-selected --sel
|
||||
And I run :yank selection --sel
|
||||
Then the message "3 chars yanked to primary selection" should be shown.
|
||||
And the primary selection should contain "one"
|
||||
|
||||
Scenario: :yank-selected with --keep
|
||||
Scenario: :yank selection with --keep
|
||||
When I run :toggle-selection
|
||||
And I run :move-to-end-of-word
|
||||
And I run :yank-selected --keep
|
||||
And I run :yank selection --keep
|
||||
And I run :move-to-end-of-word
|
||||
And I run :yank-selected --keep
|
||||
And I run :yank selection --keep
|
||||
Then the message "3 chars yanked to clipboard" should be shown.
|
||||
And the message "7 chars yanked to clipboard" should be shown.
|
||||
And the clipboard should contain "one two"
|
||||
@ -265,7 +265,7 @@ Feature: Caret mode
|
||||
When I run :toggle-selection
|
||||
And I run :move-to-end-of-word
|
||||
And I run :drop-selection
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the message "Nothing to yank" should be shown.
|
||||
|
||||
# :follow-selected
|
||||
|
@ -257,3 +257,11 @@ Feature: Using hints
|
||||
And I run :hint --rapid
|
||||
And I run :follow-hint 00
|
||||
Then data/numbers/1.txt should be loaded
|
||||
|
||||
Scenario: Using a specific hints mode
|
||||
When I open data/hints/number.html
|
||||
And I set hints -> mode to letter
|
||||
And I run :hint --mode number all
|
||||
And I press the key "s"
|
||||
And I run :follow-hint 1
|
||||
Then data/numbers/7.txt should be loaded
|
||||
|
@ -61,6 +61,18 @@ Feature: Keyboard input
|
||||
And I run :bind <ctrl-test23>
|
||||
Then the message "<ctrl-test23> is bound to 'message-info bar' in normal mode" should be shown
|
||||
|
||||
Scenario: Binding to an alias
|
||||
When I run :set aliases 'mib' 'message-info baz'
|
||||
And I run :bind test25 mib
|
||||
And I press the keys "test25"
|
||||
Then the message "baz" should be shown
|
||||
|
||||
Scenario: Printing a bound alias
|
||||
When I run :set aliases 'mib' 'message-info baz'
|
||||
And I run :bind <test26> mib
|
||||
And I run :bind <test26>
|
||||
Then the message "<test26> is bound to 'mib' in normal mode" should be shown
|
||||
|
||||
# :unbind
|
||||
|
||||
Scenario: Binding and unbinding a keychain
|
||||
|
@ -524,3 +524,16 @@ Feature: Various utility commands.
|
||||
Then the following tabs should be open:
|
||||
- data/hints/link_blank.html
|
||||
- data/hello.txt (active)
|
||||
|
||||
## Variables
|
||||
|
||||
Scenario: {url} as part of an argument
|
||||
When I open data/hello.txt
|
||||
And I run :message-info foo{url}
|
||||
Then the message "foohttp://localhost:*/hello.txt" should be shown
|
||||
|
||||
Scenario: Multiple variables in an argument
|
||||
When I open data/hello.txt
|
||||
And I put "foo" into the clipboard
|
||||
And I run :message-info {clipboard}bar{url}
|
||||
Then the message "foobarhttp://localhost:*/hello.txt" should be shown
|
||||
|
@ -9,19 +9,19 @@ Feature: Searching on a page
|
||||
|
||||
Scenario: Searching text
|
||||
When I run :search foo
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "foo"
|
||||
|
||||
Scenario: Searching twice
|
||||
When I run :search foo
|
||||
And I run :search bar
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "Bar"
|
||||
|
||||
Scenario: Searching with --reverse
|
||||
When I set general -> ignore-case to true
|
||||
And I run :search -r foo
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "Foo"
|
||||
|
||||
Scenario: Searching without matches
|
||||
@ -32,13 +32,13 @@ Feature: Searching on a page
|
||||
Scenario: Searching with / and spaces at the end (issue 874)
|
||||
When I run :set-cmd-text -s /space
|
||||
And I run :command-accept
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "space "
|
||||
|
||||
Scenario: Searching with / and slash in search term (issue 507)
|
||||
When I run :set-cmd-text -s //slash
|
||||
And I run :command-accept
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "/slash"
|
||||
|
||||
# This doesn't work because this is QtWebKit behavior.
|
||||
@ -52,25 +52,25 @@ Feature: Searching on a page
|
||||
Scenario: Searching text with ignore-case = true
|
||||
When I set general -> ignore-case to true
|
||||
And I run :search bar
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "Bar"
|
||||
|
||||
Scenario: Searching text with ignore-case = false
|
||||
When I set general -> ignore-case to false
|
||||
And I run :search bar
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "bar"
|
||||
|
||||
Scenario: Searching text with ignore-case = smart (lower-case)
|
||||
When I set general -> ignore-case to smart
|
||||
And I run :search bar
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "Bar"
|
||||
|
||||
Scenario: Searching text with ignore-case = smart (upper-case)
|
||||
When I set general -> ignore-case to smart
|
||||
And I run :search Foo
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "Foo" # even though foo was first
|
||||
|
||||
## :search-next
|
||||
@ -79,21 +79,21 @@ Feature: Searching on a page
|
||||
When I set general -> ignore-case to true
|
||||
And I run :search foo
|
||||
And I run :search-next
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "Foo"
|
||||
|
||||
Scenario: Jumping to next match with count
|
||||
When I set general -> ignore-case to true
|
||||
And I run :search baz
|
||||
And I run :search-next with count 2
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "BAZ"
|
||||
|
||||
Scenario: Jumping to next match with --reverse
|
||||
When I set general -> ignore-case to true
|
||||
And I run :search --reverse foo
|
||||
And I run :search-next
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "foo"
|
||||
|
||||
Scenario: Jumping to next match without search
|
||||
@ -107,7 +107,7 @@ Feature: Searching on a page
|
||||
And I run :search foo
|
||||
And I run :tab-prev
|
||||
And I run :search-next
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "foo"
|
||||
|
||||
## :search-prev
|
||||
@ -117,7 +117,7 @@ Feature: Searching on a page
|
||||
And I run :search foo
|
||||
And I run :search-next
|
||||
And I run :search-prev
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "foo"
|
||||
|
||||
Scenario: Jumping to previous match with count
|
||||
@ -126,7 +126,7 @@ Feature: Searching on a page
|
||||
And I run :search-next
|
||||
And I run :search-next
|
||||
And I run :search-prev with count 2
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "baz"
|
||||
|
||||
Scenario: Jumping to previous match with --reverse
|
||||
@ -134,7 +134,7 @@ Feature: Searching on a page
|
||||
And I run :search --reverse foo
|
||||
And I run :search-next
|
||||
And I run :search-prev
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "Foo"
|
||||
|
||||
Scenario: Jumping to previous match without search
|
||||
@ -149,14 +149,14 @@ Feature: Searching on a page
|
||||
When I run :search foo
|
||||
And I run :search-next
|
||||
And I run :search-next
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "foo"
|
||||
|
||||
Scenario: Wrapping around page with --reverse
|
||||
When I run :search --reverse foo
|
||||
And I run :search-next
|
||||
And I run :search-next
|
||||
And I run :yank-selected
|
||||
And I run :yank selection
|
||||
Then the clipboard should contain "Foo"
|
||||
|
||||
# TODO: wrapping message with scrolling
|
||||
|
@ -703,6 +703,53 @@ Feature: Tab management
|
||||
Then the error "Nothing to undo!" should be shown
|
||||
And the error "Nothing to undo!" should be shown
|
||||
|
||||
Scenario: Undo a tab closed by index
|
||||
When I open data/numbers/1.txt
|
||||
And I open data/numbers/2.txt in a new tab
|
||||
And I open data/numbers/3.txt in a new tab
|
||||
And I run :tab-close with count 1
|
||||
And I run :undo
|
||||
Then the following tabs should be open:
|
||||
- data/numbers/1.txt (active)
|
||||
- data/numbers/2.txt
|
||||
- data/numbers/3.txt
|
||||
|
||||
Scenario: Undo a tab closed after switching tabs
|
||||
When I open data/numbers/1.txt
|
||||
And I open data/numbers/2.txt in a new tab
|
||||
And I open data/numbers/3.txt in a new tab
|
||||
And I run :tab-close with count 1
|
||||
And I run :tab-focus 2
|
||||
And I run :undo
|
||||
Then the following tabs should be open:
|
||||
- data/numbers/1.txt (active)
|
||||
- data/numbers/2.txt
|
||||
- data/numbers/3.txt
|
||||
|
||||
Scenario: Undo a tab closed after rearranging tabs
|
||||
When I open data/numbers/1.txt
|
||||
And I open data/numbers/2.txt in a new tab
|
||||
And I open data/numbers/3.txt in a new tab
|
||||
And I run :tab-close with count 1
|
||||
And I run :tab-focus 2
|
||||
And I run :tab-move with count 1
|
||||
And I run :undo
|
||||
Then the following tabs should be open:
|
||||
- data/numbers/1.txt (active)
|
||||
- data/numbers/3.txt
|
||||
- data/numbers/2.txt
|
||||
|
||||
Scenario: Undo a tab closed after new tab opened
|
||||
When I open data/numbers/1.txt
|
||||
And I open data/numbers/2.txt in a new tab
|
||||
And I run :tab-close with count 1
|
||||
And I open data/numbers/3.txt in a new tab
|
||||
And I run :undo
|
||||
Then the following tabs should be open:
|
||||
- data/numbers/1.txt (active)
|
||||
- data/numbers/2.txt
|
||||
- data/numbers/3.txt
|
||||
|
||||
# last-close
|
||||
|
||||
Scenario: last-close = blank
|
||||
|
@ -77,6 +77,7 @@ def download_open(quteproc):
|
||||
cmd = '{} -c pass'.format(shlex.quote(sys.executable))
|
||||
quteproc.send_cmd(':download-open {}'.format(cmd))
|
||||
|
||||
|
||||
@bdd.when("I directly open the download")
|
||||
def download_open_with_prompt(quteproc):
|
||||
cmd = '{} -c pass'.format(shlex.quote(sys.executable))
|
||||
|
@ -1,6 +1,6 @@
|
||||
Feature: Yanking and pasting.
|
||||
:yank and :paste can be used to copy/paste the URL or title from/to the
|
||||
clipboard and primary selection.
|
||||
:yank, {clipboard} and {primary} can be used to copy/paste the URL or title
|
||||
from/to the clipboard and primary selection.
|
||||
|
||||
Background:
|
||||
Given I run :tab-only
|
||||
@ -23,13 +23,13 @@ Feature: Yanking and pasting.
|
||||
Scenario: Yanking title to clipboard
|
||||
When I open data/title.html
|
||||
And I wait for regex "Changing title for idx \d to 'Test title'" in the log
|
||||
And I run :yank --title
|
||||
And I run :yank title
|
||||
Then the message "Yanked title to clipboard: Test title" should be shown
|
||||
And the clipboard should contain "Test title"
|
||||
|
||||
Scenario: Yanking domain to clipboard
|
||||
When I open data/title.html
|
||||
And I run :yank --domain
|
||||
And I run :yank domain
|
||||
Then the message "Yanked domain to clipboard: http://localhost:(port)" should be shown
|
||||
And the clipboard should contain "http://localhost:(port)"
|
||||
|
||||
@ -41,15 +41,15 @@ Feature: Yanking and pasting.
|
||||
|
||||
Scenario: Yanking pretty decoded URL
|
||||
When I open data/title with spaces.html
|
||||
And I run :yank --pretty
|
||||
And I run :yank pretty-url
|
||||
Then the message "Yanked URL to clipboard: http://localhost:(port)/data/title with spaces.html" should be shown
|
||||
And the clipboard should contain "http://localhost:(port)/data/title with spaces.html"
|
||||
|
||||
#### :paste
|
||||
#### {clipboard} and {primary}
|
||||
|
||||
Scenario: Pasting a URL
|
||||
When I put "http://localhost:(port)/data/hello.txt" into the clipboard
|
||||
And I run :paste
|
||||
And I run :open {clipboard}
|
||||
And I wait until data/hello.txt is loaded
|
||||
Then the requests should be:
|
||||
data/hello.txt
|
||||
@ -57,32 +57,32 @@ Feature: Yanking and pasting.
|
||||
Scenario: Pasting a URL from primary selection
|
||||
When selection is supported
|
||||
And I put "http://localhost:(port)/data/hello2.txt" into the primary selection
|
||||
And I run :paste --sel
|
||||
And I run :open {primary}
|
||||
And I wait until data/hello2.txt is loaded
|
||||
Then the requests should be:
|
||||
data/hello2.txt
|
||||
|
||||
Scenario: Pasting with empty clipboard
|
||||
When I put "" into the clipboard
|
||||
And I run :paste
|
||||
And I run :open {clipboard} (invalid command)
|
||||
Then the error "Clipboard is empty." should be shown
|
||||
|
||||
Scenario: Pasting with empty selection
|
||||
When selection is supported
|
||||
And I put "" into the primary selection
|
||||
And I run :paste --sel
|
||||
And I run :open {primary} (invalid command)
|
||||
Then the error "Primary selection is empty." should be shown
|
||||
|
||||
Scenario: Pasting with a space in clipboard
|
||||
When I put " " into the clipboard
|
||||
And I run :paste
|
||||
And I run :open {clipboard} (invalid command)
|
||||
Then the error "Clipboard is empty." should be shown
|
||||
|
||||
Scenario: Pasting in a new tab
|
||||
Given I open about:blank
|
||||
When I run :tab-only
|
||||
And I put "http://localhost:(port)/data/hello.txt" into the clipboard
|
||||
And I run :paste -t
|
||||
And I run :open -t {clipboard}
|
||||
And I wait until data/hello.txt is loaded
|
||||
Then the following tabs should be open:
|
||||
- about:blank
|
||||
@ -92,7 +92,7 @@ Feature: Yanking and pasting.
|
||||
Given I open about:blank
|
||||
When I run :tab-only
|
||||
And I put "http://localhost:(port)/data/hello.txt" into the clipboard
|
||||
And I run :paste -b
|
||||
And I run :open -b {clipboard}
|
||||
And I wait until data/hello.txt is loaded
|
||||
Then the following tabs should be open:
|
||||
- about:blank (active)
|
||||
@ -101,7 +101,7 @@ Feature: Yanking and pasting.
|
||||
Scenario: Pasting in a new window
|
||||
Given I have a fresh instance
|
||||
When I put "http://localhost:(port)/data/hello.txt" into the clipboard
|
||||
And I run :paste -w
|
||||
And I run :open -w {clipboard}
|
||||
And I wait until data/hello.txt is loaded
|
||||
Then the session should look like:
|
||||
windows:
|
||||
@ -119,7 +119,7 @@ Feature: Yanking and pasting.
|
||||
Scenario: Pasting an invalid URL
|
||||
When I set general -> auto-search to false
|
||||
And I put "foo bar" into the clipboard
|
||||
And I run :paste
|
||||
And I run :open {clipboard}
|
||||
Then the error "Invalid URL" should be shown
|
||||
|
||||
Scenario: Pasting multiple urls in a new tab
|
||||
@ -128,7 +128,7 @@ Feature: Yanking and pasting.
|
||||
http://localhost:(port)/data/hello.txt
|
||||
http://localhost:(port)/data/hello2.txt
|
||||
http://localhost:(port)/data/hello3.txt
|
||||
And I run :paste -t
|
||||
And I run :open -t {clipboard}
|
||||
And I wait until data/hello.txt is loaded
|
||||
And I wait until data/hello2.txt is loaded
|
||||
And I wait until data/hello3.txt is loaded
|
||||
@ -145,7 +145,7 @@ Feature: Yanking and pasting.
|
||||
this url:
|
||||
http://qutebrowser.org
|
||||
should not open
|
||||
And I run :paste -t
|
||||
And I run :open -t {clipboard}
|
||||
And I wait until data/hello.txt?q=this%20url%3A%0Ahttp%3A//qutebrowser.org%0Ashould%20not%20open is loaded
|
||||
Then the following tabs should be open:
|
||||
- about:blank
|
||||
@ -159,7 +159,7 @@ Feature: Yanking and pasting.
|
||||
text:
|
||||
should open
|
||||
as search
|
||||
And I run :paste -t
|
||||
And I run :open -t {clipboard}
|
||||
And I wait until data/hello.txt?q=text%3A%0Ashould%20open%0Aas%20search is loaded
|
||||
Then the following tabs should be open:
|
||||
- about:blank
|
||||
@ -172,7 +172,7 @@ Feature: Yanking and pasting.
|
||||
http://localhost:(port)/data/hello.txt
|
||||
http://localhost:(port)/data/hello2.txt
|
||||
http://localhost:(port)/data/hello3.txt
|
||||
And I run :paste -b
|
||||
And I run :open -b {clipboard}
|
||||
And I wait until data/hello.txt is loaded
|
||||
And I wait until data/hello2.txt is loaded
|
||||
And I wait until data/hello3.txt is loaded
|
||||
@ -188,7 +188,7 @@ Feature: Yanking and pasting.
|
||||
http://localhost:(port)/data/hello.txt
|
||||
http://localhost:(port)/data/hello2.txt
|
||||
http://localhost:(port)/data/hello3.txt
|
||||
And I run :paste -w
|
||||
And I run :open -w {clipboard}
|
||||
And I wait until data/hello.txt is loaded
|
||||
And I wait until data/hello2.txt is loaded
|
||||
And I wait until data/hello3.txt is loaded
|
||||
@ -218,13 +218,13 @@ Feature: Yanking and pasting.
|
||||
Scenario: Pasting multiple urls with an empty one
|
||||
When I open about:blank
|
||||
And I put "http://localhost:(port)/data/hello.txt\n\nhttp://localhost:(port)/data/hello2.txt" into the clipboard
|
||||
And I run :paste -t
|
||||
And I run :open -t {clipboard}
|
||||
Then no crash should happen
|
||||
|
||||
Scenario: Pasting multiple urls with an almost empty one
|
||||
When I open about:blank
|
||||
And I put "http://localhost:(port)/data/hello.txt\n \nhttp://localhost:(port)/data/hello2.txt" into the clipboard
|
||||
And I run :paste -t
|
||||
And I run :open -t {clipboard}
|
||||
Then no crash should happen
|
||||
|
||||
#### :paste-primary
|
||||
|
@ -329,9 +329,12 @@ class QuteProc(testprocess.Process):
|
||||
"""Adjust some qutebrowser settings after starting."""
|
||||
settings = [
|
||||
('ui', 'message-timeout', '0'),
|
||||
('network', 'ssl-strict', 'false'),
|
||||
('general', 'auto-save-interval', '0'),
|
||||
('general', 'new-instance-open-target.window', 'last-opened')
|
||||
]
|
||||
if not self._webengine:
|
||||
settings.append(('network', 'ssl-strict', 'false'))
|
||||
|
||||
for sect, opt, value in settings:
|
||||
self.set_setting(sect, opt, value)
|
||||
|
||||
@ -621,3 +624,4 @@ def quteproc_new(qapp, httpbin, request):
|
||||
# Not calling before_test here as that would start the process
|
||||
yield proc
|
||||
proc.after_test(did_fail=request.node.rep_call.failed)
|
||||
proc.terminate()
|
||||
|
@ -159,12 +159,12 @@ def test_custom_environment(pyproc):
|
||||
|
||||
|
||||
@pytest.mark.posix
|
||||
def test_custom_environment_no_system(monkeypatch, pyproc):
|
||||
"""When env=... is given, no system environment should be present."""
|
||||
monkeypatch.setenv('QUTE_TEST_ENV', 'blah')
|
||||
pyproc.code = 'import os; print(os.environ.get("QUTE_TEST_ENV", "None"))'
|
||||
def test_custom_environment_system_env(monkeypatch, pyproc):
|
||||
"""When env=... is given, the system environment should be present."""
|
||||
monkeypatch.setenv('QUTE_TEST_ENV', 'blubb')
|
||||
pyproc.code = 'import os; print(os.environ["QUTE_TEST_ENV"])'
|
||||
pyproc.start(env={})
|
||||
pyproc.wait_for(data='None')
|
||||
pyproc.wait_for(data='blubb')
|
||||
|
||||
|
||||
class TestWaitFor:
|
||||
|
@ -251,18 +251,11 @@ class Process(QObject):
|
||||
if args is None:
|
||||
args = self._default_args()
|
||||
|
||||
if env is None:
|
||||
procenv = QProcessEnvironment.systemEnvironment()
|
||||
else:
|
||||
procenv = QProcessEnvironment()
|
||||
procenv = QProcessEnvironment.systemEnvironment()
|
||||
if env is not None:
|
||||
for k, v in env.items():
|
||||
procenv.insert(k, v)
|
||||
|
||||
passthrough_vars = ['DISPLAY', 'HOME'] # so --no-xvfb works
|
||||
for var in passthrough_vars:
|
||||
if var in os.environ:
|
||||
procenv.insert(var, os.environ[var])
|
||||
|
||||
self.proc.readyRead.connect(self.read_log)
|
||||
self.proc.setProcessEnvironment(procenv)
|
||||
self.proc.start(executable, exec_args + args)
|
||||
|
@ -54,7 +54,7 @@ def test_insert_mode(file_name, source, input_text, auto_insert, quteproc):
|
||||
quteproc.send_cmd(':enter-mode caret')
|
||||
quteproc.send_cmd(':toggle-selection')
|
||||
quteproc.send_cmd(':move-to-prev-word')
|
||||
quteproc.send_cmd(':yank-selected')
|
||||
quteproc.send_cmd(':yank selection')
|
||||
|
||||
expected_message = '{} chars yanked to clipboard'.format(len(input_text))
|
||||
quteproc.mark_expected(category='message',
|
||||
|
@ -98,9 +98,6 @@ def test_ascii_locale(httpbin, tmpdir, quteproc_new):
|
||||
quteproc_new.wait_for(category='downloads',
|
||||
message='Opening * with [*python*]')
|
||||
|
||||
quteproc_new.send_cmd(':quit')
|
||||
quteproc_new.wait_for_quit()
|
||||
|
||||
assert len(tmpdir.listdir()) == 1
|
||||
assert (tmpdir / '?-issue908.bin').exists()
|
||||
|
||||
@ -110,3 +107,19 @@ def test_no_loglines(quteproc_new):
|
||||
quteproc_new.start(args=['--temp-basedir', '--loglines=0'] + BASE_ARGS)
|
||||
quteproc_new.open_path('qute:log')
|
||||
assert quteproc_new.get_content() == 'Log output was disabled.'
|
||||
|
||||
|
||||
@pytest.mark.not_frozen
|
||||
@pytest.mark.parametrize('level', ['1', '2'])
|
||||
def test_optimize(quteproc_new, capfd, level):
|
||||
quteproc_new.start(args=['--temp-basedir'] + BASE_ARGS,
|
||||
env={'PYTHONOPTIMIZE': level})
|
||||
if level == '2':
|
||||
msg = ("Running on optimize level higher than 1, unexpected behavior "
|
||||
"may occur.")
|
||||
line = quteproc_new.wait_for(message=msg)
|
||||
line.expected = True
|
||||
|
||||
# Waiting for quit to make sure no other warning is emitted
|
||||
quteproc_new.send_cmd(':quit')
|
||||
quteproc_new.wait_for_quit()
|
||||
|
@ -26,6 +26,7 @@ import signal
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize('cmd', [':quit', ':later 500 quit'])
|
||||
def test_smoke(cmd, capfd):
|
||||
if hasattr(sys, 'frozen'):
|
||||
|
@ -29,8 +29,7 @@ from PyQt5.QtNetwork import (QNetworkRequest, QAbstractNetworkCache,
|
||||
QNetworkCacheMetaData)
|
||||
from PyQt5.QtWidgets import QCommonStyle, QLineEdit, QWidget
|
||||
|
||||
from qutebrowser.browser import browsertab
|
||||
from qutebrowser.browser.webkit import history
|
||||
from qutebrowser.browser import browsertab, history
|
||||
from qutebrowser.config import configexc
|
||||
from qutebrowser.utils import usertypes, utils
|
||||
from qutebrowser.mainwindow import mainwindow
|
||||
@ -256,7 +255,8 @@ class FakeWebTab(browsertab.AbstractTab):
|
||||
wrapped = QWidget()
|
||||
self._layout.wrap(self, wrapped)
|
||||
|
||||
def url(self):
|
||||
def url(self, requested=False):
|
||||
assert not requested
|
||||
return self._url
|
||||
|
||||
def title(self):
|
||||
|
11
tests/manual/completion/changing_title.html
Normal file
11
tests/manual/completion/changing_title.html
Normal file
@ -0,0 +1,11 @@
|
||||
<head>
|
||||
<title>Old title</title>
|
||||
<script type="text/javascript">
|
||||
setTimeout(function(){ document.title = "New title"; }, 3000);
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p>This page should change its title after 3s.</p>
|
||||
<p>When opening the :buffer completion ("gt"), the title should update while it's open.</p>
|
||||
</body>
|
25
tests/manual/hints/hide_unmatched_rapid_hints.html
Normal file
25
tests/manual/hints/hide_unmatched_rapid_hints.html
Normal file
@ -0,0 +1,25 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Hide unmatched rapid hints</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>When <code>hints -> hide-unmatched-rapid-hints</code> is set to true (default), rapid hints behave like normal hints, i.e. unmatched hints will be hidden as you type. Setting the option to false will disable hiding in rapid mode, which is sometimes useful (see <a href="https://github.com/The-Compiler/qutebrowser/issues/1799">#1799</a>).</p>
|
||||
<p>Note that when hinting in number mode, the <code>hints -> hide-unmatched-rapid-hints</code> option affects typing the hint string (number), but not the filter (letters).</p>
|
||||
<p>Here is couple of invalid links to test the behaviour:</p>
|
||||
<p><a href="#foo">one</a></p>
|
||||
<p><a href="#foo">two</a></p>
|
||||
<p><a href="#foo">three</a></p>
|
||||
<p><a href="#foo">four</a></p>
|
||||
<p><a href="#foo">five</a></p>
|
||||
<p><a href="#foo">six</a></p>
|
||||
<p><a href="#foo">seven</a></p>
|
||||
<p><a href="#foo">eight</a></p>
|
||||
<p><a href="#foo">nine</a></p>
|
||||
<p><a href="#foo">ten</a></p>
|
||||
<p><a href="#foo">eleven</a></p>
|
||||
<p><a href="#foo">twelve</a></p>
|
||||
<p><a href="#foo">thirteen</a></p>
|
||||
</body>
|
||||
</html>
|
16
tests/manual/mouse.html
Normal file
16
tests/manual/mouse.html
Normal file
@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Mouse control</title>
|
||||
</head>
|
||||
<body>
|
||||
<ul>
|
||||
<li>Middle- or Ctrl-click on a <a href="https://www.qutebrowser.org">link</a> should open it in a new tab (fg/bg according to <code>tabs -> background-tabs</code>)</li>
|
||||
<li>When clicking the link with shift, <code>background-tabs</code> should be reversed accordingly.</li>
|
||||
<li>Ctrl + Mousewheel should zoom in/out</li>
|
||||
<li>Back/forward keys on mouse should navigate back/forward</li>
|
||||
<li>With <code>input -> rocker-gestures</code> set, no context menu should be shown, but pressing left+right/right+left buttons should navigate back/forward</li>
|
||||
<li>When setting <code>input -> rocker-gestures</code> dynamically, the context menu should be hidden/shown accordingly.</li>
|
||||
</body>
|
||||
</html>
|
@ -91,26 +91,35 @@ def tab(request, default_config, qtbot, tab_registry, cookiejar_and_cache):
|
||||
objreg.delete('mode-manager', scope='window', window=0)
|
||||
|
||||
|
||||
class Tab(browsertab.AbstractTab):
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(win_id, parent)
|
||||
mode_manager = modeman.ModeManager(0)
|
||||
self.history = browsertab.AbstractHistory(self)
|
||||
self.scroller = browsertab.AbstractScroller(self, parent=self)
|
||||
self.caret = browsertab.AbstractCaret(win_id=self.win_id,
|
||||
mode_manager=mode_manager,
|
||||
tab=self, parent=self)
|
||||
self.zoom = browsertab.AbstractZoom(win_id=self.win_id)
|
||||
self.search = browsertab.AbstractSearch(parent=self)
|
||||
self.printing = browsertab.AbstractPrinting()
|
||||
|
||||
def _install_event_filter(self):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skipif(PYQT_VERSION < 0x050600,
|
||||
reason='Causes segfaults, see #1638')
|
||||
def test_tab(qtbot, view, config_stub, tab_registry):
|
||||
tab_w = browsertab.AbstractTab(win_id=0)
|
||||
tab_w = Tab(win_id=0)
|
||||
qtbot.add_widget(tab_w)
|
||||
|
||||
assert tab_w.win_id == 0
|
||||
assert tab_w._widget is None
|
||||
|
||||
mode_manager = modeman.ModeManager(0)
|
||||
|
||||
tab_w.history = browsertab.AbstractHistory(tab_w)
|
||||
tab_w.scroller = browsertab.AbstractScroller(tab_w, parent=tab_w)
|
||||
tab_w.caret = browsertab.AbstractCaret(win_id=tab_w.win_id,
|
||||
mode_manager=mode_manager,
|
||||
tab=tab_w, parent=tab_w)
|
||||
tab_w.zoom = browsertab.AbstractZoom(win_id=tab_w.win_id)
|
||||
tab_w.search = browsertab.AbstractSearch(parent=tab_w)
|
||||
tab_w.printing = browsertab.AbstractPrinting()
|
||||
|
||||
tab_w._set_widget(view)
|
||||
assert tab_w._widget is view
|
||||
assert tab_w.history._tab is tab_w
|
||||
|
@ -28,7 +28,8 @@ from hypothesis import strategies
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtWebKit import QWebHistoryInterface
|
||||
|
||||
from qutebrowser.browser.webkit import history
|
||||
from qutebrowser.browser import history
|
||||
from qutebrowser.browser.webkit import webkithistory
|
||||
from qutebrowser.utils import objreg
|
||||
|
||||
|
||||
@ -371,7 +372,7 @@ def hist_interface():
|
||||
title='example')
|
||||
history_dict = {'http://www.example.com/': entry}
|
||||
fake_hist = FakeWebHistory(history_dict)
|
||||
interface = history.WebHistoryInterface(fake_hist)
|
||||
interface = webkithistory.WebHistoryInterface(fake_hist)
|
||||
QWebHistoryInterface.setDefaultInterface(interface)
|
||||
yield
|
||||
QWebHistoryInterface.setDefaultInterface(None)
|
||||
@ -385,11 +386,23 @@ def test_history_interface(qtbot, webview, hist_interface):
|
||||
webview.load(url)
|
||||
|
||||
|
||||
def test_init(qapp, tmpdir, monkeypatch, fake_save_manager):
|
||||
@pytest.mark.parametrize('backend', ['webengine', 'webkit'])
|
||||
def test_init(backend, qapp, tmpdir, monkeypatch, fake_save_manager,
|
||||
fake_args):
|
||||
fake_args.backend = backend
|
||||
monkeypatch.setattr(history.standarddir, 'data', lambda: str(tmpdir))
|
||||
history.init(qapp)
|
||||
hist = objreg.get('web-history')
|
||||
assert hist.parent() is qapp
|
||||
assert QWebHistoryInterface.defaultInterface()._history is hist
|
||||
default_interface = QWebHistoryInterface.defaultInterface()
|
||||
|
||||
if backend == 'webkit':
|
||||
assert default_interface._history is hist
|
||||
else:
|
||||
assert backend == 'webengine'
|
||||
# For this to work, nothing can ever have called setDefaultInterface
|
||||
# before (so we need to test webengine before webkit)
|
||||
assert default_interface is None
|
||||
|
||||
assert fake_save_manager.add_saveable.called
|
||||
objreg.delete('web-history')
|
||||
|
@ -28,13 +28,14 @@ from PyQt5.QtCore import QRect, QPoint, QUrl
|
||||
from PyQt5.QtWebKit import QWebElement
|
||||
import pytest
|
||||
|
||||
from qutebrowser.browser.webkit import webelem
|
||||
from qutebrowser.browser import webelem
|
||||
from qutebrowser.browser.webkit import webkitelem
|
||||
|
||||
|
||||
def get_webelem(geometry=None, frame=None, *, null=False, style=None,
|
||||
attributes=None, tagname=None, classes=None,
|
||||
parent=None, js_rect_return=None, zoom_text_only=False):
|
||||
"""Factory for WebElementWrapper objects based on a mock.
|
||||
"""Factory for WebKitElement objects based on a mock.
|
||||
|
||||
Args:
|
||||
geometry: The geometry of the QWebElement as QRect.
|
||||
@ -117,7 +118,7 @@ def get_webelem(geometry=None, frame=None, *, null=False, style=None,
|
||||
return style_dict[name]
|
||||
|
||||
elem.styleProperty.side_effect = _style_property
|
||||
wrapped = webelem.WebElementWrapper(elem)
|
||||
wrapped = webkitelem.WebKitElement(elem)
|
||||
return wrapped
|
||||
|
||||
|
||||
@ -187,7 +188,7 @@ class SelectionAndFilterTests:
|
||||
webelem.Group.url]),
|
||||
]
|
||||
|
||||
GROUPS = [e for e in webelem.Group if e != webelem.Group.focus]
|
||||
GROUPS = list(webelem.Group)
|
||||
|
||||
COMBINATIONS = list(itertools.product(TESTS, GROUPS))
|
||||
|
||||
@ -215,15 +216,15 @@ class TestSelectorsAndFilters:
|
||||
# Make sure setting HTML succeeded and there's a new element
|
||||
assert len(webframe.findAllElements('*')) == 3
|
||||
elems = webframe.findAllElements(webelem.SELECTORS[group])
|
||||
elems = [webelem.WebElementWrapper(e) for e in elems]
|
||||
elems = [webkitelem.WebKitElement(e) for e in elems]
|
||||
filterfunc = webelem.FILTERS.get(group, lambda e: True)
|
||||
elems = [e for e in elems if filterfunc(e)]
|
||||
assert bool(elems) == matching
|
||||
|
||||
|
||||
class TestWebElementWrapper:
|
||||
class TestWebKitElement:
|
||||
|
||||
"""Generic tests for WebElementWrapper.
|
||||
"""Generic tests for WebKitElement.
|
||||
|
||||
Note: For some methods, there's a dedicated test class with more involved
|
||||
tests.
|
||||
@ -235,13 +236,13 @@ class TestWebElementWrapper:
|
||||
|
||||
def test_nullelem(self):
|
||||
"""Test __init__ with a null element."""
|
||||
with pytest.raises(webelem.IsNullError):
|
||||
with pytest.raises(webkitelem.IsNullError):
|
||||
get_webelem(null=True)
|
||||
|
||||
def test_double_wrap(self, elem):
|
||||
"""Test wrapping a WebElementWrapper."""
|
||||
"""Test wrapping a WebKitElement."""
|
||||
with pytest.raises(TypeError) as excinfo:
|
||||
webelem.WebElementWrapper(elem)
|
||||
webkitelem.WebKitElement(elem)
|
||||
assert str(excinfo.value) == "Trying to wrap a wrapper!"
|
||||
|
||||
@pytest.mark.parametrize('code', [
|
||||
@ -257,7 +258,7 @@ class TestWebElementWrapper:
|
||||
lambda e: e.document_element(),
|
||||
lambda e: e.create_inside('span'),
|
||||
lambda e: e.find_first('span'),
|
||||
lambda e: e.style_property('visibility', QWebElement.ComputedStyle),
|
||||
lambda e: e.style_property('visibility', strategy='computed'),
|
||||
lambda e: e.text(),
|
||||
lambda e: e.set_text('foo'),
|
||||
lambda e: e.set_inner_xml(''),
|
||||
@ -285,16 +286,16 @@ class TestWebElementWrapper:
|
||||
"""Make sure methods check if the element is vanished."""
|
||||
elem._elem.isNull.return_value = True
|
||||
elem._elem.tagName.return_value = 'span'
|
||||
with pytest.raises(webelem.IsNullError):
|
||||
with pytest.raises(webkitelem.IsNullError):
|
||||
code(elem)
|
||||
|
||||
def test_str(self, elem):
|
||||
assert str(elem) == 'text'
|
||||
|
||||
@pytest.mark.parametrize('is_null, expected', [
|
||||
(False, "<qutebrowser.browser.webkit.webelem.WebElementWrapper "
|
||||
(False, "<qutebrowser.browser.webkit.webkitelem.WebKitElement "
|
||||
"html='<fakeelem/>'>"),
|
||||
(True, '<qutebrowser.browser.webkit.webelem.WebElementWrapper '
|
||||
(True, '<qutebrowser.browser.webkit.webkitelem.WebKitElement '
|
||||
'html=None>'),
|
||||
])
|
||||
def test_repr(self, elem, is_null, expected):
|
||||
@ -334,7 +335,7 @@ class TestWebElementWrapper:
|
||||
|
||||
def test_eq(self):
|
||||
one = get_webelem()
|
||||
two = webelem.WebElementWrapper(one._elem)
|
||||
two = webkitelem.WebKitElement(one._elem)
|
||||
assert one == two
|
||||
|
||||
def test_eq_other_type(self):
|
||||
@ -402,7 +403,6 @@ class TestWebElementWrapper:
|
||||
('webFrame', lambda e: e.frame()),
|
||||
('geometry', lambda e: e.geometry()),
|
||||
('toOuterXml', lambda e: e.outer_xml()),
|
||||
('tagName', lambda e: e.tag_name()),
|
||||
])
|
||||
def test_simple_getters(self, elem, attribute, code):
|
||||
sentinel = object()
|
||||
@ -421,8 +421,12 @@ class TestWebElementWrapper:
|
||||
mock = getattr(elem._elem, method)
|
||||
mock.assert_called_with(*args)
|
||||
|
||||
def test_tag_name(self, elem):
|
||||
elem._elem.tagName.return_value = 'SPAN'
|
||||
assert elem.tag_name() == 'span'
|
||||
|
||||
def test_style_property(self, elem):
|
||||
assert elem.style_property('foo', QWebElement.ComputedStyle) == 'bar'
|
||||
assert elem.style_property('foo', strategy='computed') == 'bar'
|
||||
|
||||
def test_document_element(self, stubs):
|
||||
doc_elem = get_webelem()
|
||||
@ -430,14 +434,14 @@ class TestWebElementWrapper:
|
||||
elem = get_webelem(frame=frame)
|
||||
|
||||
doc_elem_ret = elem.document_element()
|
||||
assert isinstance(doc_elem_ret, webelem.WebElementWrapper)
|
||||
assert isinstance(doc_elem_ret, webkitelem.WebKitElement)
|
||||
assert doc_elem_ret == doc_elem
|
||||
|
||||
def test_find_first(self, elem):
|
||||
result = get_webelem()
|
||||
elem._elem.findFirst.return_value = result._elem
|
||||
find_result = elem.find_first('')
|
||||
assert isinstance(find_result, webelem.WebElementWrapper)
|
||||
assert isinstance(find_result, webkitelem.WebKitElement)
|
||||
assert find_result == result
|
||||
|
||||
def test_create_inside(self, elem):
|
||||
@ -727,7 +731,7 @@ def test_focus_element(stubs):
|
||||
frame = stubs.FakeWebFrame(QRect(0, 0, 100, 100))
|
||||
elem = get_webelem()
|
||||
frame.focus_elem = elem._elem
|
||||
assert webelem.focus_elem(frame)._elem is elem._elem
|
||||
assert webkitelem.focus_elem(frame)._elem is elem._elem
|
||||
|
||||
|
||||
class TestRectOnView:
|
||||
@ -739,7 +743,7 @@ class TestRectOnView:
|
||||
This is needed for all the tests calling rect_on_view or is_visible.
|
||||
"""
|
||||
config_stub.data = {'ui': {'zoom-text-only': 'true'}}
|
||||
monkeypatch.setattr('qutebrowser.browser.webkit.webelem.config',
|
||||
monkeypatch.setattr('qutebrowser.browser.webkit.webkitelem.config',
|
||||
config_stub)
|
||||
return config_stub
|
||||
|
||||
@ -821,7 +825,7 @@ class TestGetChildFrames:
|
||||
def test_single_frame(self, stubs):
|
||||
"""Test get_child_frames with a single frame without children."""
|
||||
frame = stubs.FakeChildrenFrame()
|
||||
children = webelem.get_child_frames(frame)
|
||||
children = webkitelem.get_child_frames(frame)
|
||||
assert len(children) == 1
|
||||
assert children[0] is frame
|
||||
frame.childFrames.assert_called_once_with()
|
||||
@ -836,7 +840,7 @@ class TestGetChildFrames:
|
||||
child1 = stubs.FakeChildrenFrame()
|
||||
child2 = stubs.FakeChildrenFrame()
|
||||
parent = stubs.FakeChildrenFrame([child1, child2])
|
||||
children = webelem.get_child_frames(parent)
|
||||
children = webkitelem.get_child_frames(parent)
|
||||
assert len(children) == 3
|
||||
assert children[0] is parent
|
||||
assert children[1] is child1
|
||||
@ -858,7 +862,7 @@ class TestGetChildFrames:
|
||||
first = [stubs.FakeChildrenFrame(second[0:2]),
|
||||
stubs.FakeChildrenFrame(second[2:4])]
|
||||
root = stubs.FakeChildrenFrame(first)
|
||||
children = webelem.get_child_frames(root)
|
||||
children = webkitelem.get_child_frames(root)
|
||||
assert len(children) == 7
|
||||
assert children[0] is root
|
||||
for frame in [root] + first + second:
|
||||
@ -873,7 +877,7 @@ class TestIsEditable:
|
||||
def stubbed_config(self, config_stub, monkeypatch):
|
||||
"""Fixture to create a config stub with an input section."""
|
||||
config_stub.data = {'input': {}}
|
||||
monkeypatch.setattr('qutebrowser.browser.webkit.webelem.config',
|
||||
monkeypatch.setattr('qutebrowser.browser.webkit.webkitelem.config',
|
||||
config_stub)
|
||||
return config_stub
|
||||
|
@ -21,6 +21,10 @@
|
||||
|
||||
"""Tests for qutebrowser.commands.cmdutils."""
|
||||
|
||||
import sys
|
||||
import logging
|
||||
import types
|
||||
|
||||
import pytest
|
||||
|
||||
from qutebrowser.commands import cmdutils, cmdexc, argparser, command
|
||||
@ -291,6 +295,21 @@ class TestRegister:
|
||||
else:
|
||||
assert cmd._get_call_args(win_id=0) == ([expected], {})
|
||||
|
||||
def test_pos_arg_info(self):
|
||||
@cmdutils.register()
|
||||
@cmdutils.argument('foo', choices=('a', 'b'))
|
||||
@cmdutils.argument('bar', choices=('x', 'y'))
|
||||
@cmdutils.argument('opt')
|
||||
def fun(foo, bar, opt=False):
|
||||
"""Blah."""
|
||||
pass
|
||||
|
||||
cmd = cmdutils.cmd_dict['fun']
|
||||
assert cmd.get_pos_arg_info(0) == command.ArgInfo(choices=('a', 'b'))
|
||||
assert cmd.get_pos_arg_info(1) == command.ArgInfo(choices=('x', 'y'))
|
||||
with pytest.raises(IndexError):
|
||||
cmd.get_pos_arg_info(2)
|
||||
|
||||
|
||||
class TestArgument:
|
||||
|
||||
@ -339,6 +358,25 @@ class TestArgument:
|
||||
|
||||
assert str(excinfo.value) == "Argument marked as both count/win_id!"
|
||||
|
||||
def test_no_docstring(self, caplog):
|
||||
with caplog.at_level(logging.WARNING):
|
||||
@cmdutils.register()
|
||||
def fun():
|
||||
# no docstring
|
||||
pass
|
||||
assert len(caplog.records) == 1
|
||||
msg = caplog.records[0].message
|
||||
assert msg.endswith('test_cmdutils.py has no docstring')
|
||||
|
||||
def test_no_docstring_with_optimize(self, monkeypatch):
|
||||
"""With -OO we'd get a warning on start, but no warning afterwards."""
|
||||
monkeypatch.setattr(sys, 'flags', types.SimpleNamespace(optimize=2))
|
||||
|
||||
@cmdutils.register()
|
||||
def fun():
|
||||
# no docstring
|
||||
pass
|
||||
|
||||
|
||||
class TestRun:
|
||||
|
||||
|
@ -27,6 +27,7 @@ from PyQt5.QtGui import QStandardItemModel
|
||||
|
||||
from qutebrowser.completion import completer
|
||||
from qutebrowser.utils import usertypes
|
||||
from qutebrowser.commands import command, cmdutils
|
||||
|
||||
|
||||
class FakeCompletionModel(QStandardItemModel):
|
||||
@ -91,24 +92,48 @@ def instances(monkeypatch):
|
||||
@pytest.fixture(autouse=True)
|
||||
def cmdutils_patch(monkeypatch, stubs):
|
||||
"""Patch the cmdutils module to provide fake commands."""
|
||||
@cmdutils.argument('section_', completion=usertypes.Completion.section)
|
||||
@cmdutils.argument('option', completion=usertypes.Completion.option)
|
||||
@cmdutils.argument('value', completion=usertypes.Completion.value)
|
||||
def set_command(section_=None, option=None, value=None):
|
||||
"""docstring."""
|
||||
pass
|
||||
|
||||
@cmdutils.argument('topic', completion=usertypes.Completion.helptopic)
|
||||
def show_help(tab=False, bg=False, window=False, topic=None):
|
||||
"""docstring."""
|
||||
pass
|
||||
|
||||
@cmdutils.argument('url', completion=usertypes.Completion.url)
|
||||
@cmdutils.argument('count', count=True)
|
||||
def openurl(url=None, implicit=False, bg=False, tab=False, window=False,
|
||||
count=None):
|
||||
"""docstring."""
|
||||
pass
|
||||
|
||||
@cmdutils.argument('win_id', win_id=True)
|
||||
@cmdutils.argument('command', completion=usertypes.Completion.command)
|
||||
def bind(key, win_id, command=None, *, mode='normal', force=False):
|
||||
"""docstring."""
|
||||
# pylint: disable=unused-variable
|
||||
pass
|
||||
|
||||
def tab_detach():
|
||||
"""docstring."""
|
||||
pass
|
||||
|
||||
cmds = {
|
||||
'set': [usertypes.Completion.section, usertypes.Completion.option,
|
||||
usertypes.Completion.value],
|
||||
'help': [usertypes.Completion.helptopic],
|
||||
'quickmark-load': [usertypes.Completion.quickmark_by_name],
|
||||
'bookmark-load': [usertypes.Completion.bookmark_by_url],
|
||||
'open': [usertypes.Completion.url],
|
||||
'buffer': [usertypes.Completion.tab],
|
||||
'session-load': [usertypes.Completion.sessions],
|
||||
'bind': [usertypes.Completion.empty, usertypes.Completion.command],
|
||||
'tab-detach': None,
|
||||
'set': set_command,
|
||||
'help': show_help,
|
||||
'open': openurl,
|
||||
'bind': bind,
|
||||
'tab-detach': tab_detach,
|
||||
}
|
||||
cmd_utils = stubs.FakeCmdUtils({
|
||||
name: stubs.FakeCommand(completion=compl)
|
||||
for name, compl in cmds.items()
|
||||
name: command.Command(name=name, handler=fn)
|
||||
for name, fn in cmds.items()
|
||||
})
|
||||
monkeypatch.setattr('qutebrowser.completion.completer.cmdutils',
|
||||
cmd_utils)
|
||||
monkeypatch.setattr('qutebrowser.completion.completer.cmdutils', cmd_utils)
|
||||
|
||||
|
||||
def _set_cmd_prompt(cmd, txt):
|
||||
@ -143,21 +168,17 @@ def _validate_cmd_prompt(cmd, txt):
|
||||
(':set general ignore-case |', usertypes.Completion.value),
|
||||
(':set general huh |', None),
|
||||
(':help |', usertypes.Completion.helptopic),
|
||||
(':quickmark-load |', usertypes.Completion.quickmark_by_name),
|
||||
(':bookmark-load |', usertypes.Completion.bookmark_by_url),
|
||||
(':help |', usertypes.Completion.helptopic),
|
||||
(':open |', usertypes.Completion.url),
|
||||
(':buffer |', usertypes.Completion.tab),
|
||||
(':session-load |', usertypes.Completion.sessions),
|
||||
(':bind |', usertypes.Completion.empty),
|
||||
(':bind |', None),
|
||||
(':bind <c-x> |', usertypes.Completion.command),
|
||||
(':bind <c-x> foo|', usertypes.Completion.command),
|
||||
(':bind <c-x>| foo', usertypes.Completion.empty),
|
||||
(':bind <c-x>| foo', None),
|
||||
(':set| general ', usertypes.Completion.command),
|
||||
(':|set general ', usertypes.Completion.command),
|
||||
(':set gene|ral ignore-case', usertypes.Completion.section),
|
||||
(':|', usertypes.Completion.command),
|
||||
(': |', usertypes.Completion.command),
|
||||
(':bookmark-load |', usertypes.Completion.bookmark_by_url),
|
||||
('/|', None),
|
||||
(':open -t|', None),
|
||||
(':open --tab|', None),
|
||||
|
@ -122,7 +122,7 @@ def test_maybe_resize_completion(completionview, config_stub, qtbot):
|
||||
([[]], 1, None),
|
||||
([[]], -1, None),
|
||||
])
|
||||
def test_completion_item_next_prev(tree, count, expected, completionview):
|
||||
def test_completion_item_focus(tree, count, expected, completionview):
|
||||
"""Test that on_next_prev_item moves the selection properly.
|
||||
|
||||
Args:
|
||||
@ -140,21 +140,18 @@ def test_completion_item_next_prev(tree, count, expected, completionview):
|
||||
filtermodel = sortfilter.CompletionFilterModel(model,
|
||||
parent=completionview)
|
||||
completionview.set_model(filtermodel)
|
||||
if count < 0:
|
||||
for _ in range(-count):
|
||||
completionview.completion_item_prev()
|
||||
else:
|
||||
for _ in range(count):
|
||||
completionview.completion_item_next()
|
||||
direction = 'prev' if count < 0 else 'next'
|
||||
for _ in range(abs(count)):
|
||||
completionview.completion_item_focus(direction)
|
||||
idx = completionview.selectionModel().currentIndex()
|
||||
assert filtermodel.data(idx) == expected
|
||||
|
||||
|
||||
def test_completion_item_next_prev_no_model(completionview):
|
||||
def test_completion_item_focus_no_model(completionview):
|
||||
"""Test that next/prev won't crash with no model set.
|
||||
|
||||
This can happen if completion.show and completion.auto-open are False.
|
||||
Regression test for issue #1722.
|
||||
"""
|
||||
completionview.completion_item_prev()
|
||||
completionview.completion_item_next()
|
||||
completionview.completion_item_focus('prev')
|
||||
completionview.completion_item_focus('next')
|
||||
|
@ -27,21 +27,22 @@ from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtWidgets import QTreeView
|
||||
|
||||
from qutebrowser.completion.models import miscmodels, urlmodel, configmodel
|
||||
from qutebrowser.browser.webkit import history
|
||||
from qutebrowser.browser import history
|
||||
from qutebrowser.config import sections, value
|
||||
|
||||
|
||||
def _get_completions(model):
|
||||
"""Collect all the completion entries of a model, organized by category.
|
||||
def _check_completions(model, expected):
|
||||
"""Check that a model contains the expected items in any order.
|
||||
|
||||
The result is a list of form:
|
||||
[
|
||||
(CategoryName: [(name, desc, misc), ...]),
|
||||
(CategoryName: [(name, desc, misc), ...]),
|
||||
...
|
||||
]
|
||||
Args:
|
||||
expected: A dict of form
|
||||
{
|
||||
CategoryName: [(name, desc, misc), ...],
|
||||
CategoryName: [(name, desc, misc), ...],
|
||||
...
|
||||
}
|
||||
"""
|
||||
completions = []
|
||||
actual = {}
|
||||
for i in range(0, model.rowCount()):
|
||||
category = model.item(i)
|
||||
entries = []
|
||||
@ -50,8 +51,12 @@ def _get_completions(model):
|
||||
desc = category.child(j, 1)
|
||||
misc = category.child(j, 2)
|
||||
entries.append((name.text(), desc.text(), misc.text()))
|
||||
completions.append((category.text(), entries))
|
||||
return completions
|
||||
actual[category.text()] = entries
|
||||
for cat_name, expected_entries in expected.items():
|
||||
assert cat_name in actual
|
||||
actual_items = actual[cat_name]
|
||||
for expected_item in expected_entries:
|
||||
assert expected_item in actual_items
|
||||
|
||||
|
||||
def _patch_cmdutils(monkeypatch, stubs, symbol):
|
||||
@ -165,7 +170,6 @@ def test_command_completion(qtmodeltester, monkeypatch, stubs, config_stub,
|
||||
|
||||
Validates that:
|
||||
- only non-hidden and non-deprecated commands are included
|
||||
- commands are sorted by name
|
||||
- the command description is shown in the desc column
|
||||
- the binding (if any) is shown in the misc column
|
||||
- aliases are included
|
||||
@ -173,55 +177,56 @@ def test_command_completion(qtmodeltester, monkeypatch, stubs, config_stub,
|
||||
_patch_cmdutils(monkeypatch, stubs,
|
||||
'qutebrowser.completion.models.miscmodels.cmdutils')
|
||||
config_stub.data['aliases'] = {'rock': 'roll'}
|
||||
key_config_stub.set_bindings_for('normal', {'s': 'stop', 'rr': 'roll'})
|
||||
key_config_stub.set_bindings_for('normal', {'s': 'stop',
|
||||
'rr': 'roll',
|
||||
'ro': 'rock'})
|
||||
model = miscmodels.CommandCompletionModel()
|
||||
qtmodeltester.data_display_may_return_none = True
|
||||
qtmodeltester.check(model)
|
||||
|
||||
actual = _get_completions(model)
|
||||
assert actual == [
|
||||
("Commands", [
|
||||
_check_completions(model, {
|
||||
"Commands": [
|
||||
('stop', 'stop qutebrowser', 's'),
|
||||
('drop', 'drop all user data', ''),
|
||||
('rock', "Alias for 'roll'", ''),
|
||||
('roll', 'never gonna give you up', 'rr'),
|
||||
('stop', 'stop qutebrowser', 's')
|
||||
])
|
||||
]
|
||||
('rock', "Alias for 'roll'", 'ro'),
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
def test_help_completion(qtmodeltester, monkeypatch, stubs):
|
||||
def test_help_completion(qtmodeltester, monkeypatch, stubs, key_config_stub):
|
||||
"""Test the results of command completion.
|
||||
|
||||
Validates that:
|
||||
- only non-hidden and non-deprecated commands are included
|
||||
- commands are sorted by name
|
||||
- only non-deprecated commands are included
|
||||
- the command description is shown in the desc column
|
||||
- the binding (if any) is shown in the misc column
|
||||
- aliases are included
|
||||
- only the first line of a multiline description is shown
|
||||
"""
|
||||
module = 'qutebrowser.completion.models.miscmodels'
|
||||
key_config_stub.set_bindings_for('normal', {'s': 'stop', 'rr': 'roll'})
|
||||
_patch_cmdutils(monkeypatch, stubs, module + '.cmdutils')
|
||||
_patch_configdata(monkeypatch, stubs, module + '.configdata.DATA')
|
||||
model = miscmodels.HelpCompletionModel()
|
||||
qtmodeltester.data_display_may_return_none = True
|
||||
qtmodeltester.check(model)
|
||||
|
||||
actual = _get_completions(model)
|
||||
assert actual == [
|
||||
("Commands", [
|
||||
_check_completions(model, {
|
||||
"Commands": [
|
||||
(':stop', 'stop qutebrowser', 's'),
|
||||
(':drop', 'drop all user data', ''),
|
||||
(':roll', 'never gonna give you up', ''),
|
||||
(':stop', 'stop qutebrowser', '')
|
||||
]),
|
||||
("Settings", [
|
||||
(':roll', 'never gonna give you up', 'rr'),
|
||||
(':hide', '', ''),
|
||||
],
|
||||
"Settings": [
|
||||
('general->time', 'Is an illusion.', ''),
|
||||
('general->volume', 'Goes to 11', ''),
|
||||
('ui->gesture', 'Waggle your hands to control qutebrowser', ''),
|
||||
('ui->mind', 'Enable mind-control ui (experimental)', ''),
|
||||
('ui->voice', 'Whether to respond to voice commands', ''),
|
||||
])
|
||||
]
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
def test_quickmark_completion(qtmodeltester, quickmarks):
|
||||
@ -230,14 +235,13 @@ def test_quickmark_completion(qtmodeltester, quickmarks):
|
||||
qtmodeltester.data_display_may_return_none = True
|
||||
qtmodeltester.check(model)
|
||||
|
||||
actual = _get_completions(model)
|
||||
assert actual == [
|
||||
("Quickmarks", [
|
||||
_check_completions(model, {
|
||||
"Quickmarks": [
|
||||
('aw', 'https://wiki.archlinux.org', ''),
|
||||
('ddg', 'https://duckduckgo.com', ''),
|
||||
('wiki', 'https://wikipedia.org', ''),
|
||||
])
|
||||
]
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
def test_bookmark_completion(qtmodeltester, bookmarks):
|
||||
@ -246,14 +250,13 @@ def test_bookmark_completion(qtmodeltester, bookmarks):
|
||||
qtmodeltester.data_display_may_return_none = True
|
||||
qtmodeltester.check(model)
|
||||
|
||||
actual = _get_completions(model)
|
||||
assert actual == [
|
||||
("Bookmarks", [
|
||||
_check_completions(model, {
|
||||
"Bookmarks": [
|
||||
('https://github.com', 'GitHub', ''),
|
||||
('https://python.org', 'Welcome to Python.org', ''),
|
||||
('http://qutebrowser.org', 'qutebrowser | qutebrowser', ''),
|
||||
])
|
||||
]
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
def test_url_completion(qtmodeltester, config_stub, web_history, quickmarks,
|
||||
@ -271,23 +274,22 @@ def test_url_completion(qtmodeltester, config_stub, web_history, quickmarks,
|
||||
qtmodeltester.data_display_may_return_none = True
|
||||
qtmodeltester.check(model)
|
||||
|
||||
actual = _get_completions(model)
|
||||
assert actual == [
|
||||
("Quickmarks", [
|
||||
_check_completions(model, {
|
||||
"Quickmarks": [
|
||||
('https://wiki.archlinux.org', 'aw', ''),
|
||||
('https://duckduckgo.com', 'ddg', ''),
|
||||
('https://wikipedia.org', 'wiki', ''),
|
||||
]),
|
||||
("Bookmarks", [
|
||||
],
|
||||
"Bookmarks": [
|
||||
('https://github.com', 'GitHub', ''),
|
||||
('https://python.org', 'Welcome to Python.org', ''),
|
||||
('http://qutebrowser.org', 'qutebrowser | qutebrowser', ''),
|
||||
]),
|
||||
("History", [
|
||||
],
|
||||
"History": [
|
||||
('https://python.org', 'Welcome to Python.org', '2016-03-08'),
|
||||
('https://github.com', 'GitHub', '2016-05-01'),
|
||||
]),
|
||||
]
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
def test_url_completion_delete_bookmark(qtmodeltester, config_stub,
|
||||
@ -332,10 +334,9 @@ def test_session_completion(qtmodeltester, session_manager_stub):
|
||||
qtmodeltester.data_display_may_return_none = True
|
||||
qtmodeltester.check(model)
|
||||
|
||||
actual = _get_completions(model)
|
||||
assert actual == [
|
||||
("Sessions", [('default', '', ''), ('1', '', ''), ('2', '', '')])
|
||||
]
|
||||
_check_completions(model, {
|
||||
"Sessions": [('default', '', ''), ('1', '', ''), ('2', '', '')]
|
||||
})
|
||||
|
||||
|
||||
def test_tab_completion(qtmodeltester, fake_web_tab, app_stub, win_registry,
|
||||
@ -352,17 +353,16 @@ def test_tab_completion(qtmodeltester, fake_web_tab, app_stub, win_registry,
|
||||
qtmodeltester.data_display_may_return_none = True
|
||||
qtmodeltester.check(model)
|
||||
|
||||
actual = _get_completions(model)
|
||||
assert actual == [
|
||||
('0', [
|
||||
_check_completions(model, {
|
||||
'0': [
|
||||
('0/1', 'https://github.com', 'GitHub'),
|
||||
('0/2', 'https://wikipedia.org', 'Wikipedia'),
|
||||
('0/3', 'https://duckduckgo.com', 'DuckDuckGo')
|
||||
]),
|
||||
('1', [
|
||||
],
|
||||
'1': [
|
||||
('1/1', 'https://wiki.archlinux.org', 'ArchWiki'),
|
||||
])
|
||||
]
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
def test_tab_completion_delete(qtmodeltester, fake_web_tab, qtbot, app_stub,
|
||||
@ -397,13 +397,12 @@ def test_setting_section_completion(qtmodeltester, monkeypatch, stubs):
|
||||
qtmodeltester.data_display_may_return_none = True
|
||||
qtmodeltester.check(model)
|
||||
|
||||
actual = _get_completions(model)
|
||||
assert actual == [
|
||||
("Sections", [
|
||||
_check_completions(model, {
|
||||
"Sections": [
|
||||
('general', 'General/miscellaneous options.', ''),
|
||||
('ui', 'General options related to the user interface.', ''),
|
||||
])
|
||||
]
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
def test_setting_option_completion(qtmodeltester, monkeypatch, stubs,
|
||||
@ -417,14 +416,13 @@ def test_setting_option_completion(qtmodeltester, monkeypatch, stubs,
|
||||
qtmodeltester.data_display_may_return_none = True
|
||||
qtmodeltester.check(model)
|
||||
|
||||
actual = _get_completions(model)
|
||||
assert actual == [
|
||||
("ui", [
|
||||
_check_completions(model, {
|
||||
"ui": [
|
||||
('gesture', 'Waggle your hands to control qutebrowser', 'off'),
|
||||
('mind', 'Enable mind-control ui (experimental)', 'on'),
|
||||
('voice', 'Whether to respond to voice commands', 'sometimes'),
|
||||
])
|
||||
]
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
def test_setting_value_completion(qtmodeltester, monkeypatch, stubs,
|
||||
@ -436,14 +434,43 @@ def test_setting_value_completion(qtmodeltester, monkeypatch, stubs,
|
||||
qtmodeltester.data_display_may_return_none = True
|
||||
qtmodeltester.check(model)
|
||||
|
||||
actual = _get_completions(model)
|
||||
assert actual == [
|
||||
("Current/Default", [
|
||||
_check_completions(model, {
|
||||
"Current/Default": [
|
||||
('0', 'Current value', ''),
|
||||
('11', 'Default value', ''),
|
||||
]),
|
||||
("Completions", [
|
||||
],
|
||||
"Completions": [
|
||||
('0', '', ''),
|
||||
('11', '', ''),
|
||||
])
|
||||
]
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
def test_bind_completion(qtmodeltester, monkeypatch, stubs, config_stub,
|
||||
key_config_stub):
|
||||
"""Test the results of keybinding command completion.
|
||||
|
||||
Validates that:
|
||||
- only non-hidden and non-deprecated commands are included
|
||||
- the command description is shown in the desc column
|
||||
- the binding (if any) is shown in the misc column
|
||||
- aliases are included
|
||||
"""
|
||||
_patch_cmdutils(monkeypatch, stubs,
|
||||
'qutebrowser.completion.models.miscmodels.cmdutils')
|
||||
config_stub.data['aliases'] = {'rock': 'roll'}
|
||||
key_config_stub.set_bindings_for('normal', {'s': 'stop',
|
||||
'rr': 'roll',
|
||||
'ro': 'rock'})
|
||||
model = miscmodels.BindCompletionModel()
|
||||
qtmodeltester.data_display_may_return_none = True
|
||||
qtmodeltester.check(model)
|
||||
|
||||
_check_completions(model, {
|
||||
"Commands": [
|
||||
('stop', 'stop qutebrowser', 's'),
|
||||
('drop', 'drop all user data', ''),
|
||||
('hide', '', ''),
|
||||
('rock', "Alias for 'roll'", 'ro'),
|
||||
]
|
||||
})
|
||||
|
@ -21,9 +21,45 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
from qutebrowser.completion.models import base, sortfilter
|
||||
|
||||
|
||||
def _create_model(data):
|
||||
"""Create a completion model populated with the given data.
|
||||
|
||||
data: A list of lists, where each sub-list represents a category, each
|
||||
tuple in the sub-list represents an item, and each value in the
|
||||
tuple represents the item data for that column
|
||||
"""
|
||||
model = base.BaseCompletionModel()
|
||||
for catdata in data:
|
||||
cat = model.new_category('')
|
||||
for itemdata in catdata:
|
||||
model.new_item(cat, *itemdata)
|
||||
return model
|
||||
|
||||
|
||||
def _extract_model_data(model):
|
||||
"""Express a model's data as a list for easier comparison.
|
||||
|
||||
Return: A list of lists, where each sub-list represents a category, each
|
||||
tuple in the sub-list represents an item, and each value in the
|
||||
tuple represents the item data for that column
|
||||
"""
|
||||
data = []
|
||||
for i in range(0, model.rowCount()):
|
||||
cat_idx = model.index(i, 0)
|
||||
row = []
|
||||
for j in range(0, model.rowCount(cat_idx)):
|
||||
row.append((model.data(cat_idx.child(j, 0)),
|
||||
model.data(cat_idx.child(j, 1)),
|
||||
model.data(cat_idx.child(j, 2))))
|
||||
data.append(row)
|
||||
return data
|
||||
|
||||
|
||||
@pytest.mark.parametrize('pattern, data, expected', [
|
||||
('foo', 'barfoobar', True),
|
||||
('foo', 'barFOObar', True),
|
||||
@ -46,3 +82,145 @@ def test_filter_accepts_row(pattern, data, expected):
|
||||
|
||||
row_count = filter_model.rowCount(idx)
|
||||
assert row_count == (1 if expected else 0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('tree, first, last', [
|
||||
([[('Aa',)]], 'Aa', 'Aa'),
|
||||
([[('Aa',)], [('Ba',)]], 'Aa', 'Ba'),
|
||||
([[('Aa',), ('Ab',), ('Ac',)], [('Ba',), ('Bb',)], [('Ca',)]],
|
||||
'Aa', 'Ca'),
|
||||
([[], [('Ba',)]], 'Ba', 'Ba'),
|
||||
([[], [], [('Ca',)]], 'Ca', 'Ca'),
|
||||
([[], [], [('Ca',), ('Cb',)]], 'Ca', 'Cb'),
|
||||
([[('Aa',)], []], 'Aa', 'Aa'),
|
||||
([[('Aa',)], []], 'Aa', 'Aa'),
|
||||
([[('Aa',)], [], []], 'Aa', 'Aa'),
|
||||
([[('Aa',)], [], [('Ca',)]], 'Aa', 'Ca'),
|
||||
([[], []], None, None),
|
||||
])
|
||||
def test_first_last_item(tree, first, last):
|
||||
"""Test that first() and last() return indexes to the first and last items.
|
||||
|
||||
Args:
|
||||
tree: Each list represents a completion category, with each string
|
||||
being an item under that category.
|
||||
first: text of the first item
|
||||
last: text of the last item
|
||||
"""
|
||||
model = _create_model(tree)
|
||||
filter_model = sortfilter.CompletionFilterModel(model)
|
||||
assert filter_model.data(filter_model.first_item()) == first
|
||||
assert filter_model.data(filter_model.last_item()) == last
|
||||
|
||||
|
||||
def test_set_source_model():
|
||||
"""Ensure setSourceModel sets source_model and clears the pattern."""
|
||||
model1 = base.BaseCompletionModel()
|
||||
model2 = base.BaseCompletionModel()
|
||||
filter_model = sortfilter.CompletionFilterModel(model1)
|
||||
filter_model.set_pattern('foo')
|
||||
# sourceModel() is cached as srcmodel, so make sure both match
|
||||
assert filter_model.srcmodel is model1
|
||||
assert filter_model.sourceModel() is model1
|
||||
assert filter_model.pattern == 'foo'
|
||||
filter_model.setSourceModel(model2)
|
||||
assert filter_model.srcmodel is model2
|
||||
assert filter_model.sourceModel() is model2
|
||||
assert not filter_model.pattern
|
||||
|
||||
|
||||
@pytest.mark.parametrize('tree, expected', [
|
||||
([[('Aa',)]], 1),
|
||||
([[('Aa',)], [('Ba',)]], 2),
|
||||
([[('Aa',), ('Ab',), ('Ac',)], [('Ba',), ('Bb',)], [('Ca',)]], 6),
|
||||
([[], [('Ba',)]], 1),
|
||||
([[], [], [('Ca',)]], 1),
|
||||
([[], [], [('Ca',), ('Cb',)]], 2),
|
||||
([[('Aa',)], []], 1),
|
||||
([[('Aa',)], []], 1),
|
||||
([[('Aa',)], [], []], 1),
|
||||
([[('Aa',)], [], [('Ca',)]], 2),
|
||||
])
|
||||
def test_count(tree, expected):
|
||||
model = _create_model(tree)
|
||||
filter_model = sortfilter.CompletionFilterModel(model)
|
||||
assert filter_model.count() == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize('pattern, dumb_sort, filter_cols, before, after', [
|
||||
('foo', None, [0],
|
||||
[[('foo', '', ''), ('bar', '', '')]],
|
||||
[[('foo', '', '')]]),
|
||||
|
||||
('foo', None, [0],
|
||||
[[('foob', '', ''), ('fooc', '', ''), ('fooa', '', '')]],
|
||||
[[('fooa', '', ''), ('foob', '', ''), ('fooc', '', '')]]),
|
||||
|
||||
('foo', None, [0],
|
||||
[[('foo', '', '')], [('bar', '', '')]],
|
||||
[[('foo', '', '')], []]),
|
||||
|
||||
# prefer foobar as it starts with the pattern
|
||||
('foo', None, [0],
|
||||
[[('barfoo', '', ''), ('foobar', '', '')]],
|
||||
[[('foobar', '', ''), ('barfoo', '', '')]]),
|
||||
|
||||
# however, don't rearrange categories
|
||||
('foo', None, [0],
|
||||
[[('barfoo', '', '')], [('foobar', '', '')]],
|
||||
[[('barfoo', '', '')], [('foobar', '', '')]]),
|
||||
|
||||
('foo', None, [1],
|
||||
[[('foo', 'bar', ''), ('bar', 'foo', '')]],
|
||||
[[('bar', 'foo', '')]]),
|
||||
|
||||
('foo', None, [0, 1],
|
||||
[[('foo', 'bar', ''), ('bar', 'foo', ''), ('bar', 'bar', '')]],
|
||||
[[('foo', 'bar', ''), ('bar', 'foo', '')]]),
|
||||
|
||||
('foo', None, [0, 1, 2],
|
||||
[[('foo', '', ''), ('bar', '')]],
|
||||
[[('foo', '', '')]]),
|
||||
|
||||
# the fourth column is the sort role, which overrides data-based sorting
|
||||
('', None, [0],
|
||||
[[('two', '', '', 2), ('one', '', '', 1), ('three', '', '', 3)]],
|
||||
[[('one', '', ''), ('two', '', ''), ('three', '', '')]]),
|
||||
|
||||
('', Qt.AscendingOrder, [0],
|
||||
[[('two', '', '', 2), ('one', '', '', 1), ('three', '', '', 3)]],
|
||||
[[('one', '', ''), ('two', '', ''), ('three', '', '')]]),
|
||||
|
||||
('', Qt.DescendingOrder, [0],
|
||||
[[('two', '', '', 2), ('one', '', '', 1), ('three', '', '', 3)]],
|
||||
[[('three', '', ''), ('two', '', ''), ('one', '', '')]]),
|
||||
])
|
||||
def test_set_pattern(pattern, dumb_sort, filter_cols, before, after):
|
||||
"""Validate the filtering and sorting results of set_pattern."""
|
||||
model = _create_model(before)
|
||||
model.DUMB_SORT = dumb_sort
|
||||
model.columns_to_filter = filter_cols
|
||||
filter_model = sortfilter.CompletionFilterModel(model)
|
||||
filter_model.set_pattern(pattern)
|
||||
actual = _extract_model_data(filter_model)
|
||||
assert actual == after
|
||||
|
||||
|
||||
def test_sort():
|
||||
"""Ensure that a sort argument passed to sort overrides DUMB_SORT.
|
||||
|
||||
While test_set_pattern above covers most of the sorting logic, this
|
||||
particular case is easier to test separately.
|
||||
"""
|
||||
model = _create_model([[('B', '', '', 1),
|
||||
('C', '', '', 2),
|
||||
('A', '', '', 0)]])
|
||||
filter_model = sortfilter.CompletionFilterModel(model)
|
||||
|
||||
filter_model.sort(0, Qt.AscendingOrder)
|
||||
actual = _extract_model_data(filter_model)
|
||||
assert actual == [[('A', '', ''), ('B', '', ''), ('C', '', '')]]
|
||||
|
||||
filter_model.sort(0, Qt.DescendingOrder)
|
||||
actual = _extract_model_data(filter_model)
|
||||
assert actual == [[('C', '', ''), ('B', '', ''), ('A', '', '')]]
|
||||
|
@ -214,7 +214,7 @@ class TestKeyConfigParser:
|
||||
|
||||
"""Test config.parsers.keyconf.KeyConfigParser."""
|
||||
|
||||
def test_cmd_binding(self, cmdline_test):
|
||||
def test_cmd_binding(self, cmdline_test, config_stub):
|
||||
"""Test various command bindings.
|
||||
|
||||
See https://github.com/The-Compiler/qutebrowser/issues/615
|
||||
@ -222,6 +222,7 @@ class TestKeyConfigParser:
|
||||
Args:
|
||||
cmdline_test: A pytest fixture which provides testcases.
|
||||
"""
|
||||
config_stub.data = {'aliases': []}
|
||||
kcp = keyconf.KeyConfigParser(None, None)
|
||||
kcp._cur_section = 'normal'
|
||||
if cmdline_test.valid:
|
||||
@ -283,9 +284,24 @@ class TestKeyConfigParser:
|
||||
('download-remove --all', 'download-clear'),
|
||||
|
||||
('hint links fill ":open {hint-url}"',
|
||||
'hint links fill :open {hint-url}'),
|
||||
'hint links fill :open {hint-url}'),
|
||||
('hint links fill ":open -t {hint-url}"',
|
||||
'hint links fill :open -t {hint-url}'),
|
||||
|
||||
('yank-selected', 'yank selection'),
|
||||
('yank-selected --sel', 'yank selection --sel'),
|
||||
('yank-selected -p', 'yank selection -s'),
|
||||
|
||||
('yank -t', 'yank title'),
|
||||
('yank -ts', 'yank title -s'),
|
||||
('yank -d', 'yank domain'),
|
||||
('yank -ds', 'yank domain -s'),
|
||||
('yank -p', 'yank pretty-url'),
|
||||
('yank -ps', 'yank pretty-url -s'),
|
||||
|
||||
('paste', 'open {clipboard}'),
|
||||
('paste -t', 'open -t {clipboard}'),
|
||||
('paste -ws', 'open -w {primary}'),
|
||||
]
|
||||
)
|
||||
def test_migrations(self, old, new_expected):
|
||||
|
@ -69,14 +69,14 @@ class TestArg:
|
||||
config_stub.data['general']['editor'] = ['bin', 'foo', '{}', 'bar']
|
||||
editor.edit("")
|
||||
editor._proc._proc.start.assert_called_with(
|
||||
"bin", ["foo", editor._filename, "bar"])
|
||||
"bin", ["foo", editor._file.name, "bar"])
|
||||
|
||||
def test_placeholder_inline(self, config_stub, editor):
|
||||
"""Test starting editor with placeholder arg inside of another arg."""
|
||||
config_stub.data['general']['editor'] = ['bin', 'foo{}', 'bar']
|
||||
editor.edit("")
|
||||
editor._proc._proc.start.assert_called_with(
|
||||
"bin", ["foo" + editor._filename, "bar"])
|
||||
"bin", ["foo" + editor._file.name, "bar"])
|
||||
|
||||
|
||||
class TestFileHandling:
|
||||
@ -86,7 +86,7 @@ class TestFileHandling:
|
||||
def test_ok(self, editor):
|
||||
"""Test file handling when closing with an exit status == 0."""
|
||||
editor.edit("")
|
||||
filename = editor._filename
|
||||
filename = editor._file.name
|
||||
assert os.path.exists(filename)
|
||||
assert os.path.basename(filename).startswith('qutebrowser-editor-')
|
||||
editor._proc.finished.emit(0, QProcess.NormalExit)
|
||||
@ -95,7 +95,7 @@ class TestFileHandling:
|
||||
def test_error(self, editor):
|
||||
"""Test file handling when closing with an exit status != 0."""
|
||||
editor.edit("")
|
||||
filename = editor._filename
|
||||
filename = editor._file.name
|
||||
assert os.path.exists(filename)
|
||||
|
||||
editor._proc._proc.exitStatus = mock.Mock(
|
||||
@ -109,7 +109,7 @@ class TestFileHandling:
|
||||
def test_crash(self, editor):
|
||||
"""Test file handling when closing with a crash."""
|
||||
editor.edit("")
|
||||
filename = editor._filename
|
||||
filename = editor._file.name
|
||||
assert os.path.exists(filename)
|
||||
|
||||
editor._proc._proc.exitStatus = mock.Mock(
|
||||
@ -125,7 +125,7 @@ class TestFileHandling:
|
||||
def test_unreadable(self, message_mock, editor):
|
||||
"""Test file handling when closing with an unreadable file."""
|
||||
editor.edit("")
|
||||
filename = editor._filename
|
||||
filename = editor._file.name
|
||||
assert os.path.exists(filename)
|
||||
os.chmod(filename, 0o077)
|
||||
editor._proc.finished.emit(0, QProcess.NormalExit)
|
||||
@ -160,10 +160,10 @@ def test_modify(editor, initial_text, edited_text):
|
||||
"""Test if inputs get modified correctly."""
|
||||
editor.edit(initial_text)
|
||||
|
||||
with open(editor._filename, 'r', encoding='utf-8') as f:
|
||||
with open(editor._file.name, 'r', encoding='utf-8') as f:
|
||||
assert f.read() == initial_text
|
||||
|
||||
with open(editor._filename, 'w', encoding='utf-8') as f:
|
||||
with open(editor._file.name, 'w', encoding='utf-8') as f:
|
||||
f.write(edited_text)
|
||||
|
||||
editor._proc.finished.emit(0, QProcess.NormalExit)
|
||||
|
@ -126,6 +126,7 @@ class TestStringEscape:
|
||||
('foobar', '"foobar"'),
|
||||
('foo\\bar', r'"foo\\bar"'),
|
||||
(42, '42'),
|
||||
(23.42, '23.42'),
|
||||
(None, 'undefined'),
|
||||
(object(), TypeError),
|
||||
])
|
||||
@ -137,8 +138,10 @@ def test_convert_js_arg(arg, expected):
|
||||
assert javascript._convert_js_arg(arg) == expected
|
||||
|
||||
|
||||
def test_assemble(monkeypatch):
|
||||
monkeypatch.setattr(javascript.utils, 'read_file',
|
||||
'<code from {}>'.format)
|
||||
expected = '<code from javascript/foo.js>\n_qutebrowser_func(23);'
|
||||
assert javascript.assemble('foo', 'func', 23) == expected
|
||||
@pytest.mark.parametrize('base, expected_base', [
|
||||
('window', 'window'),
|
||||
('foo', 'window._qutebrowser.foo'),
|
||||
])
|
||||
def test_assemble(base, expected_base):
|
||||
expected = '"use strict";\n{}.func(23);'.format(expected_base)
|
||||
assert javascript.assemble(base, 'func', 23) == expected
|
||||
|
@ -614,16 +614,18 @@ class FakeQSslSocket:
|
||||
return self._version
|
||||
|
||||
|
||||
@pytest.mark.parametrize('git_commit, harfbuzz, frozen, style, equal_qt', [
|
||||
(True, True, False, True, True), # normal
|
||||
(False, True, False, True, True), # no git commit
|
||||
(True, False, False, True, True), # HARFBUZZ unset
|
||||
(True, True, True, True, True), # frozen
|
||||
(True, True, True, False, True), # no style
|
||||
(True, True, False, True, False), # different Qt
|
||||
@pytest.mark.parametrize(['git_commit', 'harfbuzz', 'frozen', 'style',
|
||||
'equal_qt', 'with_webkit'], [
|
||||
(True, True, False, True, True, True), # normal
|
||||
(False, True, False, True, True, True), # no git commit
|
||||
(True, False, False, True, True, True), # HARFBUZZ unset
|
||||
(True, True, True, True, True, True), # frozen
|
||||
(True, True, True, False, True, True), # no style
|
||||
(True, True, False, True, False, True), # different Qt
|
||||
(True, True, False, True, True, False), # no webkit
|
||||
])
|
||||
def test_version_output(git_commit, harfbuzz, frozen, style, equal_qt,
|
||||
stubs, monkeypatch):
|
||||
with_webkit, stubs, monkeypatch):
|
||||
"""Test version.version()."""
|
||||
import_path = os.path.abspath('/IMPORTPATH')
|
||||
patches = {
|
||||
@ -638,7 +640,7 @@ def test_version_output(git_commit, harfbuzz, frozen, style, equal_qt,
|
||||
'QT VERSION' if equal_qt else 'QT RUNTIME VERSION'),
|
||||
'_module_versions': lambda: ['MODULE VERSION 1', 'MODULE VERSION 2'],
|
||||
'_pdfjs_version': lambda: 'PDFJS VERSION',
|
||||
'qWebKitVersion': lambda: 'WEBKIT VERSION',
|
||||
'qWebKitVersion': (lambda: 'WEBKIT VERSION') if with_webkit else None,
|
||||
'QSslSocket': FakeQSslSocket('SSL VERSION'),
|
||||
'platform.platform': lambda: 'PLATFORM',
|
||||
'platform.architecture': lambda: ('ARCHITECTURE', ''),
|
||||
@ -672,7 +674,7 @@ def test_version_output(git_commit, harfbuzz, frozen, style, equal_qt,
|
||||
MODULE VERSION 1
|
||||
MODULE VERSION 2
|
||||
pdf.js: PDFJS VERSION
|
||||
Webkit: WEBKIT VERSION
|
||||
Webkit: {webkit}
|
||||
Harfbuzz: {harfbuzz}
|
||||
SSL: SSL VERSION
|
||||
{style}
|
||||
@ -692,6 +694,7 @@ def test_version_output(git_commit, harfbuzz, frozen, style, equal_qt,
|
||||
'harfbuzz': 'HARFBUZZ' if harfbuzz else 'system',
|
||||
'frozen': str(frozen),
|
||||
'import_path': import_path,
|
||||
'webkit': 'WEBKIT VERSION' if with_webkit else 'no'
|
||||
}
|
||||
|
||||
expected = template.rstrip('\n').format(**substitutions)
|
||||
|
26
tox.ini
26
tox.ini
@ -6,6 +6,7 @@
|
||||
[tox]
|
||||
envlist = py34,py35-cov,misc,vulture,flake8,pylint,pyroma,check-manifest
|
||||
distshare = {toxworkdir}
|
||||
skipsdist = true
|
||||
|
||||
[testenv]
|
||||
# https://bitbucket.org/hpk42/tox/issue/246/ - only needed for Windows though
|
||||
@ -61,7 +62,6 @@ deps = {[testenv:mkvenv]deps}
|
||||
# cx_Freeze doesn't support Python 3.5 yet
|
||||
basepython = python3.4
|
||||
passenv = {[testenv]passenv}
|
||||
skip_install = true
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
-r{toxinidir}/misc/requirements/requirements-cxfreeze.txt
|
||||
@ -75,7 +75,7 @@ ignore_errors = true
|
||||
basepython = python3
|
||||
# For global .gitignore files
|
||||
passenv = HOME
|
||||
deps = -r{toxinidir}/misc/requirements/requirements-pip.txt
|
||||
deps =
|
||||
commands =
|
||||
{envpython} scripts/dev/misc_checks.py git
|
||||
{envpython} scripts/dev/misc_checks.py vcs
|
||||
@ -85,7 +85,9 @@ commands =
|
||||
basepython = python3
|
||||
deps =
|
||||
-r{toxinidir}/misc/requirements/requirements-pip.txt
|
||||
-r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/misc/requirements/requirements-vulture.txt
|
||||
setenv = PYTHONPATH={toxinidir}
|
||||
commands =
|
||||
{envpython} scripts/link_pyqt.py --tox {envdir}
|
||||
{envpython} scripts/dev/run_vulture.py
|
||||
@ -120,27 +122,28 @@ deps =
|
||||
-r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/misc/requirements/requirements-flake8.txt
|
||||
commands =
|
||||
{envpython} -m flake8
|
||||
{envpython} -m flake8 {posargs:qutebrowser tests scripts}
|
||||
|
||||
[testenv:pyroma]
|
||||
basepython = python3
|
||||
skip_install = true
|
||||
passenv =
|
||||
deps = -r{toxinidir}/misc/requirements/requirements-pyroma.txt
|
||||
deps =
|
||||
-r{toxinidir}/misc/requirements/requirements-pip.txt
|
||||
-r{toxinidir}/misc/requirements/requirements-pyroma.txt
|
||||
commands =
|
||||
{envdir}/bin/pyroma .
|
||||
|
||||
[testenv:check-manifest]
|
||||
basepython = python3
|
||||
skip_install = true
|
||||
passenv =
|
||||
deps = -r{toxinidir}/misc/requirements/requirements-check-manifest.txt
|
||||
deps =
|
||||
-r{toxinidir}/misc/requirements/requirements-pip.txt
|
||||
-r{toxinidir}/misc/requirements/requirements-check-manifest.txt
|
||||
commands =
|
||||
{envdir}/bin/check-manifest --ignore 'qutebrowser/git-commit-id,qutebrowser/html/doc,qutebrowser/html/doc/*,*/__pycache__'
|
||||
|
||||
[testenv:docs]
|
||||
basepython = python3
|
||||
skip_install = true
|
||||
whitelist_externals = git
|
||||
passenv = TRAVIS_PULL_REQUEST
|
||||
deps =
|
||||
@ -156,7 +159,6 @@ commands =
|
||||
# PYTHON is actually required when using this env, but the entire tox.ini would
|
||||
# fail if we didn't have a fallback defined.
|
||||
basepython = {env:PYTHON:}/python.exe
|
||||
skip_install = true
|
||||
deps =
|
||||
-r{toxinidir}/misc/requirements/requirements-pip.txt
|
||||
-r{toxinidir}/requirements.txt
|
||||
@ -168,7 +170,6 @@ commands =
|
||||
|
||||
[testenv:pyinstaller]
|
||||
basepython = python3
|
||||
skip_install = true
|
||||
deps =
|
||||
-r{toxinidir}/misc/requirements/requirements-pip.txt
|
||||
-r{toxinidir}/requirements.txt
|
||||
@ -178,7 +179,6 @@ commands =
|
||||
{envbindir}/pyinstaller --noconfirm misc/qutebrowser.spec
|
||||
|
||||
[testenv:eslint]
|
||||
skip_install = True
|
||||
deps = -r{toxinidir}/misc/requirements/requirements-pip.txt
|
||||
deps =
|
||||
whitelist_externals = eslint
|
||||
commands = eslint qutebrowser
|
||||
commands = eslint --color qutebrowser/javascript
|
||||
|
Loading…
Reference in New Issue
Block a user